mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Merge branch 'unstable' into area/playback-qa
This commit is contained in:
commit
17163984f7
12 files changed
+356
-5
No files matched your search
@@ -0,0 +1,6 @@
|
||||
# Item7 cold live feed notification
|
||||
|
||||
- Goal: cold cache then eligible live updates must notify actual loadVideos consumer;latest feed owns reused subscription and clear invalidates late callbacks.
|
||||
- Scope: nostrService loadVideos notification state plus constructor/clear lifecycle only;new focused service test and unique evidence/backlog. No app/feed/filter/cache policy/runner changes.
|
||||
- Plan: reproduce original service with controlled subscribeVideos boundary;prove latest/force/clear controls;minimal current-consumer token;mutants/focused/lint;explicit full slot then one unchanged normal gate;QA owns actual-app browser after landing/preview rebuild.
|
||||
- Original4e904349: new first5 scenarios4fail1pass/0cancel;first8line callback fix later5pass2fail on7. Revised service19 added lines passes7/7. No actual browser/full/build yet.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Item7 notification ownership
|
||||
|
||||
- Live updates notify the same full ordered cached list emitted/persisted already,including deletion-only empty results. Preserve event payload/deleted metadata and filtering.
|
||||
- A service-owned notification object carries latest loadVideos consumer across reused subscription. Creating replacement subscription uses fresh identity;clear invalidates before unsub. Old subscription callback cannot change cache,notify consumer,emit or persist after disposal.
|
||||
- Interpreter independently identified stale captured consumer;our A→B and force/clear tests fail firstcallbackpatch,pass revisedtoken. Service lifecycle state explicitly routed by dispatch16:10:24.
|
||||
- Keep existing cache/persistent callbacks and callback-throw isolation. Event-only consumers remain supported. No productpolicy change/forced UI refresh. Browser qualification belongs to QA.
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"id": "client-reliability-7",
|
||||
"area": "client-reliability",
|
||||
"status": "open",
|
||||
"status": "done",
|
||||
"title": "Restore cold-feed live subscription onVideos notification after eligible relay updates",
|
||||
"blockedOn": null,
|
||||
"addedOn": "2026-10-20",
|
||||
"filedAt": "386cc60e1fa3ba6d9b67a1171a1ab89c03df84ca",
|
||||
"evidence": "QA actual app at386cc60e:cache0\u2192EVENT3/EOSE\u2192filtered3/access/include true\u2192viewEMPTY/DOM0. live service branch emits videos:updated but omits onVideos callback used by actual feed refresh. Scope/reproduction/controls in area prompt;minimal method-only fix after natural failing regression."
|
||||
"evidence": "Service fixfecf4294 durablylandedb1174163556e2705ba85868e21adcaf51b44bd5f viaf39622da;unchanged normalgateexit0/432discovered+unique starts;new7/7,mergedfocused27/5files,nine naturallykilledmutants,lint11/11,ancestry0. Fullslotreleased16:21:30UTC;dispatch previewbuild+QA actualapp retestpending,notclaimed. Final evidence test_logs/TEST_LOG_2026-10-20_16-22-00_client-reliability-7-landed.md;IDLE/no optionaltasks.",
|
||||
"closedOn": "2026-10-20"
|
||||
}
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
QA actual-app passive reproduction at386cc60e: fresh Trending/cache0, local relay delivers3 valid signed VIDEO events+EOSE; raw/all/active/appvideo maps3,actual filtered3 and every access/include true,whitelist/lockdown false. Connected visible view remains current0/rendered0/signatureEMPTY/DOM0 after15s;metadata reasoncache. Service emits videos:cache3 then videos:updated(subscription3),but app refresh owner is onVideos callback. Source loadVideos cache/optimistic paths call onVideos;live subscription branch does not. This is a demonstrated local-review blocker,not optional work.
|
||||
|
||||
Temporary item ownership: ONLY js/services/nostrService.js loadVideos live-subscription notification path;new focused regression (prefer tests/nostr-service-live-feed-notification.test.mjs),unique integrity/evidence/backlog. Read app/feed/bootstrap and existing tests;do not edit them or sharedTODO without routing. No app.js,cache/filter policy,signer/protocol,runner/config/kit/styling changes. Current service area exclusion is overridden only for this method/item.
|
||||
Temporary item ownership: ONLY js/services/nostrService.js loadVideos live-subscription notification path plus minimal service-owned current-consumer state/subscription-clear lifecycle if needed to prevent stale consumer delivery;new focused regression (prefer tests/nostr-service-live-feed-notification.test.mjs),unique integrity/evidence/backlog. Read app/feed/bootstrap and existing tests;do not edit them or sharedTODO without routing. No app.js,cache/filter policy,signer/protocol,runner/config/kit/styling changes. Current service area exclusion is overridden only for this method/item.
|
||||
|
||||
Merge unstable non-destructively,claimitem7,read currentrules/KNOWN_ISSUES/test-integrity/localNIP/schema docs. Before edits reproduce actual service coldcache→controlled subscribeVideos callback→actualonVideos consumer notification absent;prove cache0 theneligible updates/deletion callbacks,not event-string assertions. Preserve filtered full-list ordering,deleted updates,tombstone removal,existing videos:updated payload and persistence,callbackthrow isolation/no event loss,optimistic/cache semantics. After natural failing regression,implement smallest method-only callback delivery fix and restored failing mutants. Retain no product-policy change or forced refresh substitute.
|
||||
Merge unstable non-destructively,claimitem7,read currentrules/KNOWN_ISSUES/test-integrity/localNIP/schema docs. Before edits reproduce actual service coldcache→controlled subscribeVideos callback→actualonVideos consumer notification absent;prove cache0 theneligible updates/deletion callbacks,not event-string assertions. Preserve filtered full-list ordering,deleted updates,tombstone removal,existing videos:updated payload and persistence,callbackthrow isolation/no event loss,optimistic/cache semantics. Reused subscription must notify latest consumer after subsequent load/feed switch; forceFetch/clear must not let late obsolete callbacks notify old or replacement consumers. Include event-only consumer compatibility; do not blindly close over first onVideos. After natural failing regression,implement smallest method-only callback delivery fix and restored failing mutants. Retain no product-policy change or forced refresh substitute.
|
||||
|
||||
Cheap focused tests and lint now;no browser/full/build until slot grant. QA owns actual-app execution and will verify cold feed plus original core/mobile baseline after durable fix/preview rebuild. Send reproduction/controls/files before widening;ready event with focused counts/mutants/lint. One unchanged normal full gate can be authoritative after merged focused/lint;avoid duplicate full solely for logs. Normal commit/merge/queue/ancestry/closure rules apply. No external operations/newoptional tasks;idle once local-review blocker is closed.
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ a landing rule changes.
|
||||
|
||||
## 2026-10-20
|
||||
|
||||
- Local-review hard blocker measured: coldTrending cache0 then relayEVENT3/EOSE ingests eligible3 but view remainsEMPTY/DOM0. client-reliability-7 temporary ownership ONLY nostrService.loadVideos subscription notification path + new focused regression/integrity/evidence; reproduce current method before minimal fix. Existing app/feed modules read-only unless additional evidence/routing. Preserve access/filter/deletion/cache/event semantics; no forcedrefresh baseline substitute. QA retains actual-app/browser qualification; client cheapfocused until explicit fullslot.
|
||||
- Local-review hard blocker measured: coldTrending cache0 then relayEVENT3/EOSE ingests eligible3 but view remainsEMPTY/DOM0. client-reliability-7 temporary ownership ONLY nostrService.loadVideos subscription notification path, minimal service-owned current-consumer state/subscription-clear lifecycle where proven necessary, + new focused regression/integrity/evidence; reproduce current method before minimal fix. Existing app/feed modules read-only unless additional evidence/routing. Preserve access/filter/deletion/cache/event semantics; no forcedrefresh baseline substitute. QA retains actual-app/browser qualification; client cheapfocused until explicit fullslot.
|
||||
|
||||
- Owner stopmilestone is Ready for me to review and test locally. Finish current storage2/history6/faithful actual-app baseline+preview/handoff,then no new optional tasks before review. Reconcile upstream docs-only8d4622bb via area merge+normal queue;new playback promises stay explicit/open,do not churn current scope or declare them done from partial/static evidence. Genuine cached offline dependency mirror is testprep only,not CDN/deployed/offline production proof.
|
||||
|
||||
|
||||
@@ -619,6 +619,7 @@ export class NostrService {
|
||||
}
|
||||
});
|
||||
this.videosMap = null;
|
||||
this.videoFeedNotification = null;
|
||||
this.videosByAuthorIndex = null;
|
||||
this.authorIndexDirty = false;
|
||||
this.moderationService = moderationService || null;
|
||||
@@ -1429,6 +1430,7 @@ export class NostrService {
|
||||
}
|
||||
|
||||
clearVideoSubscription() {
|
||||
this.videoFeedNotification = null;
|
||||
const current = getStoredVideoSubscription();
|
||||
if (current && typeof current.unsub === "function") {
|
||||
try {
|
||||
@@ -1713,6 +1715,12 @@ export class NostrService {
|
||||
this.clearVideoSubscription();
|
||||
}
|
||||
|
||||
// Reused relay subscriptions follow the currently displayed feed consumer.
|
||||
const notification =
|
||||
(getStoredVideoSubscription() && this.videoFeedNotification) || {};
|
||||
notification.onVideos = onVideos;
|
||||
this.videoFeedNotification = notification;
|
||||
|
||||
const applyAndNotify = (videos, reason) => {
|
||||
const filtered = this.filterVideos(videos, {
|
||||
blacklistedEventIds,
|
||||
@@ -1793,6 +1801,9 @@ export class NostrService {
|
||||
};
|
||||
const subscription = this.nostrClient.subscribeVideos(
|
||||
(updatedVideos) => {
|
||||
if (this.videoFeedNotification !== notification) {
|
||||
return;
|
||||
}
|
||||
if (!Array.isArray(updatedVideos) || !updatedVideos.length) {
|
||||
return;
|
||||
}
|
||||
@@ -1837,6 +1848,14 @@ export class NostrService {
|
||||
return bCreated - aCreated;
|
||||
});
|
||||
|
||||
if (typeof notification.onVideos === "function") {
|
||||
try {
|
||||
notification.onVideos(fullList, { reason: "subscription" });
|
||||
} catch (error) {
|
||||
userLogger.warn("[nostrService] onVideos handler threw", error);
|
||||
}
|
||||
}
|
||||
|
||||
this.emit("videos:updated", {
|
||||
videos: fullList,
|
||||
deleted: deletedUpdates,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# Item7 live feed consumer notification
|
||||
|
||||
```yaml
|
||||
test_integrity_note:
|
||||
change_type: ["new_tests"]
|
||||
scenarios:
|
||||
- id: SCN-cold-live-feed
|
||||
given: "actual service cold cache and controlled eligible/ineligible subscription arrivals"
|
||||
when: "two live batches arrive"
|
||||
then: "consumer,events and persisted cache expose same full ordered eligible snapshots"
|
||||
- id: SCN-latest-feed-consumer
|
||||
given: "one live relay subscription and successive loadVideos consumers"
|
||||
when: "consumer switches then becomes event-only"
|
||||
then: "only latest consumer receives live updates;event-only updates still emit and persist"
|
||||
- id: SCN-replaced-feed-subscription
|
||||
given: "live subscription is force-replaced then cleared"
|
||||
when: "old controlled callbacks arrive after unsubscribe"
|
||||
then: "no stale callback effects;current subscription notifies and persists normally"
|
||||
- id: SCN-live-deletion
|
||||
given: "two cached videos"
|
||||
when: "deletion-only,mixed deletion+addition,and final deletion arrive"
|
||||
then: "consumer gets reduced,updated and empty full snapshots;deleted metadata and persistence preserved"
|
||||
- id: SCN-live-consumer-throw
|
||||
given: "consumer throws on first live arrival"
|
||||
when: "two live batches arrive"
|
||||
then: "service events,persistence and later callback delivery remain intact"
|
||||
- id: SCN-warm-live-feed
|
||||
given: "persistent warm cache and empty in-memory input"
|
||||
when: "load then live arrival"
|
||||
then: "optimistic data survives without empty flash;live snapshot includes old and new entries"
|
||||
- id: SCN-event-only-filtered-feed
|
||||
given: "no onVideos consumer"
|
||||
when: "empty,ineligible,then eligible updates arrive"
|
||||
then: "empty/ineligible updates do not notify/cache;eligible update emits and persists without consumer"
|
||||
observable_outcomes:
|
||||
- "real loadVideos callback values,service events,shared map and localStorage snapshots"
|
||||
- "latest callback ownership,unsubscribe lifecycle and no stale effects"
|
||||
determinism_controls:
|
||||
- "controlled nostrClient.subscribeVideos boundary;actual service filter/cache/persistence/emitter"
|
||||
- "fixed videos/order,isolated shared state/storage;global network guards;no sleeps/retries/public traffic"
|
||||
- "teardown restores globals/map/subscription/storage and verifies owned subscriptions close"
|
||||
anti_cheat_rationale:
|
||||
prevents:
|
||||
- "event-only/source-string assertion mistaken for actual consumer delivery"
|
||||
- "notify delta or stale first consumer instead of current full-list consumer"
|
||||
- "lose deletion/event/persistence on callback exceptions or no consumer"
|
||||
- "filter bypass or optimistic cache flash"
|
||||
relaxation:
|
||||
did_relax_any_assertion: false
|
||||
if_true_explain_spec_basis: ""
|
||||
```
|
||||
|
||||
Nine isolated runtime loader mutants all naturally exit1/0cancel and restore at process termination: missing-live kills scenarios1-6;delta-only kills1/4/5/6;ignore-deletions kills4;uncaught-consumer kills5;first-consumer kills2;late-callback kills3;event-only-drop kills2/7;retained-on-clear kills3;filter-bypass kills1/7. No source file changed during mutation runs; no arbitrary sleeps/retries/relaxed assertions. Outputs `/tmp/item7-mutant-*.log` and `/tmp/item7-mutants.json`.
|
||||
|
||||
Scope is service notification contract with controlled client IO;not real wire parser/signing/relay/bootstrap/DOM/mobile proof. QA measured actual app failure independently and owns postlanding qualification. Existing suites are unchanged.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Item7 ready evidence
|
||||
|
||||
- Baseline4e90434992471c3c6b40b017c6eae133aec92b62;filed386cc60e source unchanged. `env -u NODE_TEST_CONTEXT NODE_ENV=test node tests/nostr-service-live-feed-notification.test.mjs` beforeproductedit naturalexit1:4fail1pass/0cancel/0skip (`/tmp/item7-before.log`);cached0→eligiblefull3/persisted3 but consumer receives only cache0. Deletion/warm/throw scenarios also fail;ineligible/event-only controls pass.
|
||||
- First8line closed-over callback addition5/5 passed;then latest-consumer/force/clear controls failnaturally,5pass2fail/0cancel (`/tmp/item7-first-callback-controls.log`). Revised service19added lines (loadVideos+constructor/clear state)7/7 naturalexit0,0fail/cancel/skip (`/tmp/item7-owned-after.log`). No existing tests changed.
|
||||
- `python3 /tmp/item7-mutants.py`:9/9 isolated source-loader mutants each naturalexit1,0cancelled,named assertions;no disk source mutation. Summary `/tmp/item7-mutants.json`,outputs `/tmp/item7-mutant-*.log`;mapping unique integrity note.
|
||||
- Five focused files executed sequentially with `env` NODE_TEST_CONTEXT removed,NODE_ENV=test,each `node --import ./tests/test-helpers/setup-localstorage.mjs <file>` and existing60s/file diagnostic bound. New7+access-ready4+access5+service6+channelcache5=27pass,all5processesexit0 (`/tmp/item7-focused.log`). Access5 prints five TAP ok lines but has pre-existing forced-exit afterhook/no final summary;not edited. Otherfiles0fail/cancel/skip.
|
||||
- `env -u NODE_TEST_CONTEXT npm run lint` naturalexit0,11stages (`/tmp/item7-lint.log`). `git diff --check` exit0.
|
||||
- No live network,signer/protocol changes,full/build/browser run. Fake client only controls subscription/cache input and existing access boundary;actual service filter/cache/emitter/persistence. Cleanup restores shared app map/subscription/storage/globals and closes owned subscriptions. Service moderation disabled in fixture to avoid unrelated singleton refresh side effects.
|
||||
- QA owns actual app retest and dispatch preview rebuild. Waiting explicit full normal slot;single unchanged normal gate authoritative full candidate run,no duplicate full solely for logs. Live relay/signing/real peers/core/mobile remain unverified by these service tests.
|
||||
@@ -0,0 +1,11 @@
|
||||
# Item7 landed service fix
|
||||
|
||||
- Fix commit `fecf42948fb9c44dcd1147612d2f88b2c5dd3b51`;merged verified unstable `d7d632f88ba27496d1370e68825cae4abab689ca` to submission `f39622da3ba3a172c643641dab816a998a380489`. Actual tested candidate/durable landed SHA `b1174163556e2705ba85868e21adcaf51b44bd5f` at16:20:56.834UTC. `land_status` landed;`git log -1 unstable` same;`git merge-base --is-ancestor f39622da3ba3a172c643641dab816a998a380489 unstable` exit0. Product changes19added lines in service constructor/clear/loadVideos only;current consumer replaces stale first callback and cleared/replaced subscription callbacks are ignored.
|
||||
- Slot request16:14:32UTC;dispatch grant16:14:59.388UTC,wait27seconds. Merge/focused verification began before grant using cheap checks only; normal submit16:15:12.477UTC. Full slot RELEASE event16:21:30UTC. No browser/build run by worker.
|
||||
- Merged focused5files27passing (new7,access-ready4,access5,service6,channelcache5),each processnaturalexit0;access5 has pre-existing forced-exit afterhook/no final summary but five TAP ok records. `/tmp/item7-merged-focused.log`. Otherfourfiles0fail/cancel/skip. Merged lint `env -u NODE_TEST_CONTEXT npm run lint` exit0,11stages `/tmp/item7-merged-lint.log`.
|
||||
- Original baseline and firstcallback ownership failures plus nine runtime mutants mapped in ready log/integrity. Final new7/7 also passes within full gate,0fail/cancel/skip.
|
||||
- SINGLE authoritative normal full gate launched `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD UNIT_TEST_TIMEOUT_MS=60000 node tools/fleet/bin/landq.mjs submit`. Existing unchanged shell: `if [ -f package.json ]; then if [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then npm ci --ignore-scripts || npm install; else npm install; fi; npm test; else echo 'set gates.land' >&2; exit 1; fi`. Durable result explicitly exit0. No duplicate full diagnostic run;runner/package/config blobs identical to d7d632f8.
|
||||
- Independent configured discovery at tested b1174163:432files using unchanged recursive runner selection,empty quarantine,no filters/shard;retained log432unique file starts. Full gate completed exit0;not every file uses same framework/summary format.
|
||||
- Durable original result `/home/user/.local/state/agent-fleet/bitvid/landq/results/f39622da3ba3a172c643641dab816a998a380489.json`;preserved `/tmp/client-reliability-7-normal-result.json`,3201bytes,SHA256 `328fc0b0f78e55b79509c0087141aa6308eb37378b590f5bdcdb8d2fd4ba2776`. Detail is gate exit0+stderr tail;All unit tests passed stdout marker is absent,not invented.
|
||||
- Existing runner artifact `/home/user/Documents/GitHub/bitvid-lander/artifacts/test_unit.log` preserved before overwrite at `/tmp/client-reliability-7-normal-unit.log`,707001bytes,SHA256 `fef7966f8e138ce2d04887cf8cac0d353f1c98b6982dbb4eb45025ef60b9c1e3`. Retained unit stdout/stderr/file starts,not entire npm-install/full-process transcript or guarantee of final marker flush. Raw logs uncommitted.
|
||||
- Implementation task closed after durable service landing;dispatch+QA informed. Dispatch owns preview rebuild/artifact verification;QA owns original actual-app cold3card/core/mobile retest. Those results are pending and are NOT implied by unit gate. No forced refresh substitute. Real relay/signing/live-peer/browser/mobile proof remains unverified here. No optional task;IDLE after metadata closure.
|
||||
@@ -0,0 +1,227 @@
|
||||
import "./test-helpers/setup-localstorage.mjs";
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { NostrService } from "../js/services/nostrService.js";
|
||||
import {
|
||||
getVideosMap,
|
||||
setVideosMap,
|
||||
getVideoSubscription,
|
||||
setVideoSubscription,
|
||||
} from "../js/state/appState.js";
|
||||
|
||||
const storageKey = "bitvid:filtered-videos:v1";
|
||||
const video = (id, created_at, extra = {}) => ({
|
||||
id,
|
||||
created_at,
|
||||
pubkey: "a".repeat(64),
|
||||
title: id,
|
||||
url: "https://example.invalid/video.mp4",
|
||||
...extra,
|
||||
});
|
||||
const ids = (videos) => videos.map((entry) => entry.id);
|
||||
|
||||
function fixture(t, { cached = [], persisted = [] } = {}) {
|
||||
const previousMap = getVideosMap();
|
||||
const previousSubscription = getVideoSubscription();
|
||||
const previousStorage = window.localStorage.getItem(storageKey);
|
||||
setVideosMap(new Map());
|
||||
setVideoSubscription(null);
|
||||
window.localStorage.setItem(storageKey, JSON.stringify(persisted));
|
||||
const service = new NostrService();
|
||||
service.moderationService = null;
|
||||
const events = [];
|
||||
const subscriptions = [];
|
||||
const unsubscribe = service.on("videos:updated", (detail) => {
|
||||
events.push({ ...detail, videos: detail.videos.slice() });
|
||||
});
|
||||
let receive;
|
||||
let networkAttempts = 0;
|
||||
const globals = ["fetch", "WebSocket"].map((key) => [
|
||||
key, Object.getOwnPropertyDescriptor(globalThis, key),
|
||||
]);
|
||||
globalThis.fetch = () => {
|
||||
networkAttempts++;
|
||||
throw new Error("Unexpected live-feed network IO");
|
||||
};
|
||||
globalThis.WebSocket = class {
|
||||
constructor() {
|
||||
networkAttempts++;
|
||||
throw new Error("Unexpected live-feed socket");
|
||||
}
|
||||
};
|
||||
t.after(() => {
|
||||
service.clearVideoSubscription();
|
||||
unsubscribe();
|
||||
setVideosMap(previousMap);
|
||||
setVideoSubscription(previousSubscription);
|
||||
if (previousStorage === null) window.localStorage.removeItem(storageKey);
|
||||
else window.localStorage.setItem(storageKey, previousStorage);
|
||||
for (const [key, descriptor] of globals) {
|
||||
if (descriptor) Object.defineProperty(globalThis, key, descriptor);
|
||||
else delete globalThis[key];
|
||||
}
|
||||
assert.equal(networkAttempts, 0);
|
||||
assert.ok(subscriptions.every((entry) => entry.closed));
|
||||
});
|
||||
service.nostrClient = {
|
||||
pubkey: null,
|
||||
getActiveVideos: () => cached,
|
||||
getLatestCachedCreatedAt: () => cached[0]?.created_at || 0,
|
||||
clampVideoRequestLimit: (limit) => limit,
|
||||
subscribeVideos(callback, options) {
|
||||
receive = callback;
|
||||
const subscription = { closed: false, unsub() { this.closed = true; } };
|
||||
subscriptions.push(subscription);
|
||||
return subscription;
|
||||
},
|
||||
};
|
||||
service.accessControl = {
|
||||
isHydrated: () => true,
|
||||
ensureReady: async () => {},
|
||||
canAccess: (entry) => entry.denied !== true,
|
||||
};
|
||||
return {
|
||||
service,
|
||||
events,
|
||||
subscriptions,
|
||||
currentReceiver: () => receive,
|
||||
receive: (updates) => receive(updates),
|
||||
persisted: () => JSON.parse(window.localStorage.getItem(storageKey)),
|
||||
};
|
||||
}
|
||||
|
||||
test("cold feed consumer receives full ordered eligible snapshots after live arrivals", async (t) => {
|
||||
const f = fixture(t);
|
||||
const notifications = [];
|
||||
const initial = await f.service.loadVideos({
|
||||
blacklistedEventIds: new Set(["blocked-event"]),
|
||||
isAuthorBlocked: (pubkey) => pubkey === "b".repeat(64),
|
||||
onVideos: (videos, detail) => notifications.push({ ids: ids(videos), ...detail }),
|
||||
});
|
||||
assert.deepEqual(initial, []);
|
||||
assert.deepEqual(notifications, [{ ids: [], reason: "cache" }]);
|
||||
f.receive([
|
||||
video("older", 10), video("newer", 30),
|
||||
video("private", 40, { isPrivate: true }),
|
||||
video("denied", 50, { denied: true }),
|
||||
video("blocked-event", 60),
|
||||
video("blocked-author", 70, { pubkey: "b".repeat(64) }),
|
||||
]);
|
||||
f.receive([video("middle", 20)]);
|
||||
assert.deepEqual(ids(Array.from(f.service.getVideosMap().values())), ["older", "newer", "middle"]);
|
||||
assert.deepEqual(ids(f.persisted()), ["newer", "middle", "older"]);
|
||||
assert.deepEqual(notifications, [
|
||||
{ ids: [], reason: "cache" },
|
||||
{ ids: ["newer", "older"], reason: "subscription" },
|
||||
{ ids: ["newer", "middle", "older"], reason: "subscription" },
|
||||
]);
|
||||
assert.deepEqual(f.events.map((entry) => ids(entry.videos)), notifications.map((entry) => entry.ids));
|
||||
});
|
||||
|
||||
test("a reused live subscription notifies only the latest feed consumer", async (t) => {
|
||||
const f = fixture(t);
|
||||
const first = [];
|
||||
const latest = [];
|
||||
await f.service.loadVideos({ onVideos: (videos, detail) => first.push({ ids: ids(videos), ...detail }) });
|
||||
await f.service.loadVideos({ onVideos: (videos, detail) => latest.push({ ids: ids(videos), ...detail }) });
|
||||
f.receive([video("live", 10)]);
|
||||
assert.equal(f.subscriptions.length, 1, "feed switch reuses the live relay subscription");
|
||||
assert.deepEqual(first, [{ ids: [], reason: "cache" }]);
|
||||
assert.deepEqual(latest, [{ ids: [], reason: "cache" }, { ids: ["live"], reason: "subscription" }]);
|
||||
await f.service.loadVideos();
|
||||
const beforeDetach = latest.length;
|
||||
f.receive([video("event-only", 20)]);
|
||||
assert.equal(latest.length, beforeDetach, "an event-only load detaches the prior feed consumer");
|
||||
assert.deepEqual(ids(f.events.at(-1).videos), ["event-only", "live"]);
|
||||
});
|
||||
|
||||
test("force-fetch and clear invalidate late callbacks from replaced subscriptions", async (t) => {
|
||||
const f = fixture(t);
|
||||
const first = [];
|
||||
const current = [];
|
||||
await f.service.loadVideos({ onVideos: (videos) => first.push(ids(videos)) });
|
||||
const obsolete = f.currentReceiver();
|
||||
await f.service.loadVideos({ forceFetch: true, onVideos: (videos) => current.push(ids(videos)) });
|
||||
assert.equal(f.subscriptions.length, 2);
|
||||
assert.equal(f.subscriptions[0].closed, true);
|
||||
obsolete([video("obsolete", 10)]);
|
||||
assert.equal(f.service.getVideosMap().has("obsolete"), false);
|
||||
assert.deepEqual(first, [[]]);
|
||||
f.receive([video("current", 20)]);
|
||||
assert.deepEqual(current, [[], ["current"]]);
|
||||
const afterClear = f.currentReceiver();
|
||||
f.service.clearVideoSubscription();
|
||||
const eventsBefore = f.events.length;
|
||||
afterClear([video("late-after-clear", 30)]);
|
||||
assert.equal(f.events.length, eventsBefore);
|
||||
assert.deepEqual(current, [[], ["current"]]);
|
||||
assert.deepEqual(ids(f.persisted()), ["current"]);
|
||||
});
|
||||
|
||||
test("deletion-only and mixed live updates notify reduced and empty feed snapshots", async (t) => {
|
||||
const first = video("first", 10);
|
||||
const second = video("second", 20);
|
||||
const f = fixture(t, { cached: [second, first] });
|
||||
const notifications = [];
|
||||
await f.service.loadVideos({ onVideos: (videos, detail) => notifications.push({ ids: ids(videos), ...detail }) });
|
||||
const deletedFirst = { ...first, deleted: true };
|
||||
f.receive([deletedFirst]);
|
||||
assert.equal(f.service.getVideosMap().has("first"), false);
|
||||
assert.deepEqual(f.events.at(-1).deleted, [deletedFirst]);
|
||||
assert.deepEqual(ids(f.persisted()), ["second"]);
|
||||
const third = video("third", 30);
|
||||
const deletedSecond = { ...second, deleted: true };
|
||||
f.receive([deletedSecond, third]);
|
||||
assert.deepEqual(f.events.at(-1).deleted, [deletedSecond]);
|
||||
assert.deepEqual(ids(f.persisted()), ["third"]);
|
||||
f.receive([{ ...third, deleted: true }]);
|
||||
assert.equal(f.service.getVideosMap().size, 0);
|
||||
assert.deepEqual(f.persisted(), []);
|
||||
assert.deepEqual(notifications, [
|
||||
{ ids: ["second", "first"], reason: "cache" },
|
||||
{ ids: ["second"], reason: "subscription" },
|
||||
{ ids: ["third"], reason: "subscription" },
|
||||
{ ids: [], reason: "subscription" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("throwing live consumer does not lose service events, persistence or later notifications", async (t) => {
|
||||
const f = fixture(t);
|
||||
const notifications = [];
|
||||
await f.service.loadVideos({ onVideos(videos, detail) {
|
||||
notifications.push({ ids: ids(videos), ...detail });
|
||||
if (detail.reason === "subscription" && videos.length === 1) throw new Error("consumer failed");
|
||||
} });
|
||||
assert.doesNotThrow(() => f.receive([video("first", 10)]));
|
||||
assert.deepEqual(ids(f.events.at(-1).videos), ["first"]);
|
||||
assert.deepEqual(ids(f.persisted()), ["first"]);
|
||||
f.receive([video("second", 20)]);
|
||||
assert.deepEqual(notifications.at(-1), { ids: ["second", "first"], reason: "subscription" });
|
||||
assert.deepEqual(ids(f.events.at(-1).videos), ["second", "first"]);
|
||||
assert.deepEqual(ids(f.persisted()), ["second", "first"]);
|
||||
});
|
||||
|
||||
test("warm persistent feed keeps optimistic notification without an empty cache flash", async (t) => {
|
||||
const f = fixture(t, { persisted: [video("warm", 10)] });
|
||||
const notifications = [];
|
||||
const initial = await f.service.loadVideos({ onVideos: (videos, detail) => notifications.push({ ids: ids(videos), ...detail }) });
|
||||
assert.deepEqual(ids(initial), ["warm"]);
|
||||
assert.deepEqual(notifications, [{ ids: ["warm"], reason: "persistent-cache" }]);
|
||||
f.receive([video("new", 20)]);
|
||||
assert.deepEqual(notifications.at(-1), { ids: ["new", "warm"], reason: "subscription" });
|
||||
assert.deepEqual(ids(f.persisted()), ["new", "warm"]);
|
||||
});
|
||||
|
||||
test("absent consumer and empty or ineligible updates preserve notification and cache contracts", async (t) => {
|
||||
const f = fixture(t);
|
||||
await f.service.loadVideos();
|
||||
f.receive(null);
|
||||
f.receive([]);
|
||||
f.receive([video("private", 10, { isPrivate: true }), video("denied", 20, { denied: true })]);
|
||||
assert.equal(f.events.length, 1);
|
||||
assert.equal(f.service.getVideosMap().size, 0);
|
||||
assert.deepEqual(f.persisted(), []);
|
||||
assert.doesNotThrow(() => f.receive([video("public", 30)]));
|
||||
assert.deepEqual(ids(f.events.at(-1).videos), ["public"]);
|
||||
assert.deepEqual(ids(f.persisted()), ["public"]);
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
# Item7
|
||||
|
||||
- [x] Sync/claim;currentHEAD original failure+eligibility/persistence controls.
|
||||
- [x] Latest-consumer/feed-switch and force/clear controls;minimal service-owned notification token.
|
||||
- [x] Seven new scenarios,nine isolated failing runtime mutants,focused5files27passing scenarios,lint11/11.
|
||||
- [ ] Explicit fullslot,commit/merge+mergedfocused/lint,one unchanged normal gate/durable landed+ancestry.
|
||||
- [ ] QA actual-app retest/dispatch preview handoff;close backlog and IDLE.
|
||||
@@ -0,0 +1,10 @@
|
||||
# Item7 implementation completed
|
||||
|
||||
- [x] Reproduce actual service cold feed notification failure before edits.
|
||||
- [x] Latest-consumer/deletion/event-only/throw/warm/force-clear controls;19line service-only fix.
|
||||
- [x] New7,focused27/5files,nine killed mutants,lint11;merged checks green.
|
||||
- [x] Single unchanged normal gate432files exit0;durable landed b1174163 and ancestry0;log hashes preserved.
|
||||
- [x] Release full slot;close implementation backlog;handoff dispatch build/QA actual-app retest explicitly pending.
|
||||
- [ ] Documentation-only closure landing thenIDLE;no optional task.
|
||||
|
||||
Do not claim owner local-review baseline complete until QA reports actual-app/core/mobile outcome.
|
||||
Reference in new issue
Block a user