Merge branch 'unstable' into ai/performance-events-cache-dirty-tracking-5412442767823742174

This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-01 12:00:52 -05:00
commit 1fc9859798
6 files changed
+100 -11

No files matched your search

+4 -3
View File
@@ -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.
+3 -3
View File
@@ -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
+37 -1
View File
@@ -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;
+17 -3
View File
@@ -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,
+29
View File
@@ -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
+10 -1
View File
@@ -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'");