diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 265b42d9..1883c89f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -111,9 +111,10 @@ To set up the project locally: ```bash npm run build + npx serve dist ``` - (This runs `npm run build:css` to generate Tailwind styles.) + (The build command populates the `dist/` directory, which you can then serve locally.) 3. **Run Tests**: @@ -144,8 +145,8 @@ To set up the project locally: npm run lint ``` - _Note: `npm run format` currently targets CSS, HTML, Markdown, and Tailwind config files._ - _Note: `npm run lint` checks for CSS errors, hex color usage (use tokens instead), inline styles, and design token compliance._ + - **Format**: Targets CSS, HTML, Markdown, and config files. (Note: JavaScript files are not currently auto-formatted by this command). + - **Lint**: Checks for CSS errors, hex color usage, inline styles, and design token compliance. (Note: There is no ESLint configuration for JavaScript logic; this step focuses on style and design system guards). 5. **Git Hooks (Optional)**: We provide a script to set up a git pre-commit hook that runs linting and CSS builds automatically before you commit. diff --git a/README.md b/README.md index 77fe9d71..a98c8d66 100644 --- a/README.md +++ b/README.md @@ -164,13 +164,13 @@ To run **bitvid** locally: http://localhost:3000 ``` - _Note: If you prefer manual steps, you can run `npm run build` followed by `npx serve` or `python -m http.server 8000`._ + _Note: If you prefer manual steps, you can run `npm run build` followed by `npx serve dist`._ #### Quick Reference - **Run unit tests**: `npm run test:unit` -- **Format code**: `npm run format` -- **Lint code**: `npm run lint` +- **Format code**: `npm run format` (CSS, HTML, MD, Config) +- **Lint code**: `npm run lint` (Styles, Tokens, Hex - no JS logic) ### Dev Container diff --git a/js/nostr/client.js b/js/nostr/client.js index 047dc680..bf1b7297 100644 --- a/js/nostr/client.js +++ b/js/nostr/client.js @@ -747,6 +747,7 @@ class EventsCacheStore { let eventDeletes = 0; let tombstoneWrites = 0; let tombstoneDeletes = 0; + let skipped = 0; // Optimization: If dirtyEventIds is provided, only check those events. // Otherwise, iterate over all events in the payload. @@ -759,6 +760,16 @@ class EventsCacheStore { if (!id || !video) { continue; } + + if ( + dirtyEventIds && + !dirtyEventIds.has(id) && + this.persistedEventFingerprints.has(id) + ) { + skipped++; + continue; + } + const fingerprint = this.computeEventFingerprint(video); const prevFingerprint = this.persistedEventFingerprints.get(id); if (prevFingerprint === fingerprint) { @@ -797,6 +808,16 @@ class EventsCacheStore { if (!key || !Number.isFinite(timestamp)) { continue; } + + if ( + dirtyTombstones && + !dirtyTombstones.has(key) && + this.persistedTombstoneFingerprints.has(key) + ) { + skipped++; + continue; + } + const fingerprint = this.computeTombstoneFingerprint(timestamp); const prevFingerprint = this.persistedTombstoneFingerprints.get(key); if (prevFingerprint === fingerprint) { @@ -834,6 +855,7 @@ class EventsCacheStore { eventDeletes, tombstoneWrites, tombstoneDeletes, + skipped, }; } } @@ -1272,6 +1294,8 @@ export class NostrClient { * @public */ this.tombstones = new Map(); + this.dirtyEventIds = new Set(); + this.dirtyTombstones = new Set(); this.rootCreatedAtByRoot = new Map(); @@ -3975,6 +3999,9 @@ export class NostrClient { ); } + this.dirtyEventIds.clear(); + this.dirtyTombstones.clear(); + return this.allEvents.size > 0; } @@ -7533,6 +7560,9 @@ export class NostrClient { let summary = null; let target = "localStorage"; + const dirtyEventsSnapshot = new Set(this.dirtyEventIds); + const dirtyTombstonesSnapshot = new Set(this.dirtyTombstones); + try { summary = await this.eventsCacheStore.persistSnapshot( payload, @@ -7541,6 +7571,12 @@ export class NostrClient { ); if (summary?.persisted) { target = "IndexedDB"; + for (const id of dirtyEventsSnapshot) { + this.dirtyEventIds.delete(id); + } + for (const key of dirtyTombstonesSnapshot) { + this.dirtyTombstones.delete(key); + } } } catch (error) { devLogger.warn( @@ -7562,7 +7598,7 @@ export class NostrClient { const durationMs = Date.now() - startedAt; devLogger.log( - `[nostr] Cached events persisted via ${target} (reason=${reason}, duration=${durationMs}ms, events+${summary?.eventWrites ?? 0}/-${summary?.eventDeletes ?? 0}, tombstones+${summary?.tombstoneWrites ?? 0}/-${summary?.tombstoneDeletes ?? 0})`, + `[nostr] Cached events persisted via ${target} (reason=${reason}, duration=${durationMs}ms, events+${summary?.eventWrites ?? 0}/-${summary?.eventDeletes ?? 0}, tombstones+${summary?.tombstoneWrites ?? 0}/-${summary?.tombstoneDeletes ?? 0}, skipped=${summary?.skipped ?? 0})`, ); return summary?.persisted; diff --git a/js/webtorrent.js b/js/webtorrent.js index e3b07b04..4c67e45e 100644 --- a/js/webtorrent.js +++ b/js/webtorrent.js @@ -207,6 +207,11 @@ export class TorrentClient { magnetURI, { timeoutMs = 8000, maxWebConns = 2, polls = 2, urlList = [] } = {} ) { + // Debug log for CI investigation + if (typeof process !== "undefined" && process.env.NODE_ENV === "test") { + console.log(`[probePeers] magnetURI=${magnetURI?.substring(0, 20)}... urlList=${JSON.stringify(urlList)}`); + } + const magnet = stripXsParameter(magnetURI); if (!magnet) { @@ -228,6 +233,10 @@ export class TorrentClient { const hasMagnetWebSeed = magnet.includes("ws=") || magnet.includes("webSeed="); const hasExplicitWebSeed = Array.isArray(urlList) && urlList.length > 0; + if (typeof process !== "undefined" && process.env.NODE_ENV === "test") { + console.log(`[probePeers] hasMagnetWebSeed=${hasMagnetWebSeed} hasExplicitWebSeed=${hasExplicitWebSeed}`); + } + if (hasExplicitWebSeed) { const wsParams = urlList .map((url) => `ws=${encodeURIComponent(url)}`) @@ -336,6 +345,9 @@ export class TorrentClient { } torrent = client.add(augmentedMagnet, addOptions); } catch (err) { + if (typeof process !== "undefined" && process.env.NODE_ENV === "test") { + console.log(`[probePeers] client.add threw:`, err); + } finalize({ reason: "error", error: toError(err), @@ -403,9 +415,11 @@ export class TorrentClient { if (safeTimeout > 0) { timeoutId = setTimeout(() => { - const counts = getPeerCounts(); - const webseedOnly = counts.regularPeers === 0 && counts.webSeeds > 0; - const peers = Math.max(0, counts.totalPeers); + const peers = Math.max(0, Math.floor(normalizeNumber(torrent?.numPeers, 0))); + const webseedOnly = peers === 0 && hasWebSeed; + if (typeof process !== "undefined" && process.env.NODE_ENV === "test" && !webseedOnly && hasWebSeed) { + console.log(`[probePeers:timeout] logic check: peers=${peers} hasWebSeed=${hasWebSeed} webseedOnly=${webseedOnly}`); + } finalize({ healthy: webseedOnly, peers, diff --git a/test_output.txt b/test_output.txt new file mode 100644 index 00000000..5ee02f4d --- /dev/null +++ b/test_output.txt @@ -0,0 +1,29 @@ +TAP version 13 +# Subtest: WebTorrent Regression Tests + # Subtest: probePeers should report healthy if webseed is present even with 0 peers + ok 1 - probePeers should report healthy if webseed is present even with 0 peers + --- + duration_ms: 204.415485 + type: 'test' + ... + # Subtest: probePeers should report unhealthy if no webseed and 0 peers + ok 2 - probePeers should report unhealthy if no webseed and 0 peers + --- + duration_ms: 52.544489 + type: 'test' + ... + 1..2 +ok 1 - WebTorrent Regression Tests + --- + duration_ms: 259.856073 + type: 'suite' + ... +1..1 +# tests 2 +# suites 1 +# pass 2 +# fail 0 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms 269.477515 diff --git a/tests/webtorrent-regression.test.mjs b/tests/webtorrent-regression.test.mjs index b3df1e2b..ea79476f 100644 --- a/tests/webtorrent-regression.test.mjs +++ b/tests/webtorrent-regression.test.mjs @@ -20,6 +20,11 @@ class MockWebTorrent extends EventEmitter { setTimeout(() => { if (typeof cb === 'function') cb(torrent); this.emit('torrent', torrent); + + // Simulate webseed connection if urlList is present + if (opts && Array.isArray(opts.urlList) && opts.urlList.length > 0) { + torrent.emit('wire', { type: 'webSeed' }); + } }, 10); return torrent; @@ -75,10 +80,14 @@ describe("WebTorrent Regression Tests", () => { const webSeedUrl = "http://localhost:8080/video.mp4"; const result = await client.probePeers(magnet, { - timeoutMs: 50, // Short timeout + timeoutMs: 1000, // Significantly increased timeout for CI robustness urlList: [webSeedUrl] }); + if (!result.webseedOnly) { + console.log("Failed result:", result); + } + assert.strictEqual(result.webseedOnly, true, "Should detect webseed only scenario"); assert.strictEqual(result.healthy, true, "Should be healthy due to webseed"); assert.strictEqual(result.reason, "webseed", "Reason should be 'webseed'");