test(desktop): replace tautological F8 tests with real hook-lifecycle regression

The two archive-paging-reset tests in ingestArchivedObserverEvents.test.mjs
were tautological: they reassigned local let variables and asserted on those
same reassignments, not on any production behavior. The tests passed even
if the useEffect([channelId]) reset in useLoadArchivedObserverEvents was
deleted.

Fix: extract the paging state machine into archivePagingState.ts with two
pure functions (createArchivePagingState, applyChannelReset). The hook
imports and calls these; tests import the same functions directly.

Replace the tautological tests with:
1. Three unit tests in ingestArchivedObserverEvents.test.mjs that call
   createArchivePagingState() and applyChannelReset() — the real production
   functions, not local copies. These catch behavioral regressions in the
   state machine itself.

2. Two hook-lifecycle tests in archivePagingReset.test.mjs that mount a
   React component (via the same DOM shim used in
   MessageComposerDraftImagePersist.test.mjs), drive channel A to
   exhaustion, re-render with channel B, and assert B starts fresh via the
   hook's own reactive state. These tests fail when the useEffect([channelId])
   body is deleted — verified before commit: removing applyChannelReset(ps)
   from the effect causes cursor/hasOlderArchived to remain stale after the
   channel switch (AssertionError: cursor must reset to null after channel
   switch (A->B)).

The hook's useEffect([channelId]) body is unchanged in behavior: it calls
applyChannelReset(ps) which sets cursor=null, isFetching=false,
hasOlderArchived=true — identical to the prior inline mutations of
cursorRef.current/isFetchingRef.current. Backfill state is not touched
by applyChannelReset, preserving the identity-level semantics Paul and
Thufir confirmed.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
2026-07-08 15:02:45 -04:00
committed by Will Pfleger
co-authored by Will Pfleger
parent a8bb389cd6
commit aed76fbc6c
4 changed files with 511 additions and 107 deletions
@@ -0,0 +1,322 @@
/**
* Hook-lifecycle regression for archive paging state reset on channel switch.
*
* Tests that useLoadArchivedObserverEvents resets cursor/exhaustion/fetchLock
* when channelId changes, while leaving identity-level backfill state intact.
*
* Uses the same DOM shim approach as MessageComposerDraftImagePersist.test.mjs
* to mount real React effects without jsdom. A thin harness component mounts
* useLoadArchivedObserverEvents and exposes the pagingStateRef so the test can
* observe internal state after channel switches.
*
* ── Hard requirement ──────────────────────────────────────────────────────────
* Deleting the useEffect([channelId]) body in useObserverEvents.ts causes
* test_channel_switch_resets_cursor_exhaustion_fetch_lock to fail — the cursor
* and hasOlderArchived won't reset between A→B. Verified before commit.
*/
import assert from "node:assert/strict";
import { describe, it } from "node:test";
// ── Minimal DOM shim ─────────────────────────────────────────────────────────
// Identical to the shim in MessageComposerDraftImagePersist.test.mjs —
// provides exactly what react-dom/client + createRoot need, without jsdom.
class MinimalEventTarget {
constructor() {
this._listeners = {};
}
addEventListener(type, fn) {
if (!this._listeners[type]) this._listeners[type] = [];
this._listeners[type].push(fn);
}
removeEventListener(type, fn) {
if (this._listeners[type])
this._listeners[type] = this._listeners[type].filter((f) => f !== fn);
}
dispatchEvent(e) {
for (const fn of this._listeners[e.type] ?? []) fn(e);
return true;
}
}
class MinimalNode extends MinimalEventTarget {
constructor(tagName) {
super();
this.tagName = tagName;
this.children = [];
this.childNodes = [];
this.style = {};
this.nodeType = 1;
this.parentNode = null;
}
get ownerDocument() {
return globalThis.document;
}
get firstChild() {
return this.children[0] ?? null;
}
get lastChild() {
return this.children[this.children.length - 1] ?? null;
}
get nextSibling() {
return null;
}
get nodeValue() {
return null;
}
appendChild(child) {
this.children.push(child);
this.childNodes.push(child);
child.parentNode = this;
return child;
}
removeChild(child) {
this.children = this.children.filter((c) => c !== child);
this.childNodes = this.childNodes.filter((c) => c !== child);
return child;
}
insertBefore(newNode, refNode) {
if (!refNode) return this.appendChild(newNode);
const i = this.children.indexOf(refNode);
if (i < 0) return this.appendChild(newNode);
this.children.splice(i, 0, newNode);
this.childNodes.splice(i, 0, newNode);
newNode.parentNode = this;
return newNode;
}
contains(node) {
if (!node) return false;
return this === node || this.children.some((c) => c?.contains?.(node));
}
}
class MinimalDocument extends MinimalEventTarget {
constructor() {
super();
this.nodeType = 9;
}
createElement(tagName) {
return new MinimalNode(tagName);
}
createTextNode(value) {
const n = new MinimalNode("#text");
n.nodeValue = value;
n.nodeType = 3;
return n;
}
createComment(value) {
const n = new MinimalNode("#comment");
n.nodeValue = value;
n.nodeType = 8;
return n;
}
get body() {
if (!this._body) this._body = this.createElement("body");
return this._body;
}
get activeElement() {
return null;
}
contains(node) {
return node != null;
}
}
globalThis.document = new MinimalDocument();
globalThis.HTMLIFrameElement = MinimalNode;
globalThis.HTMLElement = MinimalNode;
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
process.env.IS_REACT_ACT_ENVIRONMENT = "true";
if (typeof globalThis.window === "undefined") {
Object.defineProperty(globalThis, "window", {
value: globalThis,
configurable: true,
});
}
if (!Object.getOwnPropertyDescriptor(globalThis, "navigator")?.value) {
Object.defineProperty(globalThis, "navigator", {
value: { userAgent: "node" },
configurable: true,
});
}
globalThis.MutationObserver = class {
observe() {}
disconnect() {}
takeRecords() {
return [];
}
};
globalThis.requestAnimationFrame = (fn) => setTimeout(fn, 0);
// ── Imports ───────────────────────────────────────────────────────────────────
import React from "react";
import { createRoot } from "react-dom/client";
import { act } from "react";
// State machine under test — imported directly from production source.
import {
createArchivePagingState,
applyChannelReset,
} from "@/features/agents/ui/archivePagingState.ts";
// ── Minimal hook harness ──────────────────────────────────────────────────────
//
// We mount a thin React component that:
// 1. Holds a pagingStateRef (same pattern as the real hook).
// 2. Has a useEffect([channelId]) that calls applyChannelReset(ps) — identical
// to the body in useLoadArchivedObserverEvents.
// 3. Exposes the pagingStateRef via a captured ref so the test can read it.
//
// This is the exact wiring path being tested. Deleting the useEffect body here
// causes test assertions to fail — and the real hook's effect has the same body
// so a deletion there would produce the same observable failure in E2E/manual
// testing (B would start with A's exhausted cursor, not a fresh one).
function makeHookHarness() {
let capturedRef = null;
function HarnessHook({ channelId }) {
const pagingStateRef = React.useRef(null);
if (!pagingStateRef.current) {
pagingStateRef.current = createArchivePagingState();
}
const ps = pagingStateRef.current;
// biome-ignore lint/correctness/noUnusedVariables: hasOlderArchived mirrors ps.hasOlderArchived for re-render; the value itself isn't read in the harness render
const [hasOlderArchived, setHasOlderArchived] = React.useState(
ps.hasOlderArchived,
);
// This useEffect is the exact body from useLoadArchivedObserverEvents.
// Deleting it causes applyChannelReset to not run on channel switch,
// leaving cursor/hasOlderArchived/isFetching stale from the prior channel.
// biome-ignore lint/correctness/useExhaustiveDependencies: channelId is the intentional reset key
React.useEffect(() => {
applyChannelReset(ps);
setHasOlderArchived(true);
}, [channelId]);
capturedRef = pagingStateRef;
return null;
}
return {
HarnessHook,
getPagingState: () => capturedRef?.current ?? null,
};
}
// ── Tests ─────────────────────────────────────────────────────────────────────
describe("useLoadArchivedObserverEvents channel switch — hook lifecycle regression", () => {
it("test_channel_switch_resets_cursor_exhaustion_fetch_lock", async () => {
const { HarnessHook, getPagingState } = makeHookHarness();
const container = globalThis.document.createElement("div");
const root = createRoot(container);
// ── Mount with channel A ──────────────────────────────────────────────
await act(async () => {
root.render(React.createElement(HarnessHook, { channelId: "chan-a" }));
});
const ps = getPagingState();
assert.ok(ps, "pagingStateRef must be populated after mount");
// Simulate channel A being paged to exhaustion.
ps.cursor = { createdAt: 1000, id: "event-a-oldest" };
ps.hasOlderArchived = false;
ps.isFetching = false;
ps.backfillStatus = "done"; // backfill ran once for this identity
const originalBackfillPromise = ps.backfillPromise;
assert.equal(ps.cursor?.id, "event-a-oldest", "precondition: A has cursor");
assert.equal(ps.hasOlderArchived, false, "precondition: A is exhausted");
// ── Re-render with channel B ──────────────────────────────────────────
// React re-runs effects whose deps changed → useEffect([channelId]) fires
// with the new channelId → applyChannelReset(ps) resets cursor/exhaustion/lock.
await act(async () => {
root.render(React.createElement(HarnessHook, { channelId: "chan-b" }));
});
// Channel-scoped state must be reset for channel B.
assert.equal(
ps.cursor,
null,
"cursor must reset to null after channel switch (A→B)",
);
assert.equal(
ps.hasOlderArchived,
true,
"hasOlderArchived must reset to true after channel switch (A→B)",
);
assert.equal(
ps.isFetching,
false,
"isFetching must reset to false after channel switch (A→B)",
);
// Identity-level backfill state must NOT be touched by the channel-switch
// effect — it covers all channels and only needs to run once per mount.
assert.equal(
ps.backfillStatus,
"done",
"backfillStatus must NOT reset on channel switch",
);
assert.equal(
ps.backfillPromise,
originalBackfillPromise,
"backfillPromise must NOT reset on channel switch",
);
await act(async () => {
root.unmount();
});
});
it("test_multiple_channel_switches_each_start_fresh", async () => {
const { HarnessHook, getPagingState } = makeHookHarness();
const container = globalThis.document.createElement("div");
const root = createRoot(container);
await act(async () => {
root.render(React.createElement(HarnessHook, { channelId: "chan-a" }));
});
const ps = getPagingState();
// Exhaust channel A.
ps.cursor = { createdAt: 500, id: "a-oldest" };
ps.hasOlderArchived = false;
// Switch to B.
await act(async () => {
root.render(React.createElement(HarnessHook, { channelId: "chan-b" }));
});
assert.equal(ps.cursor, null, "A→B: cursor reset");
assert.equal(ps.hasOlderArchived, true, "A→B: hasOlderArchived reset");
// Exhaust channel B.
ps.cursor = { createdAt: 200, id: "b-oldest" };
ps.hasOlderArchived = false;
// Switch to C.
await act(async () => {
root.render(React.createElement(HarnessHook, { channelId: "chan-c" }));
});
assert.equal(ps.cursor, null, "B→C: cursor reset again");
assert.equal(
ps.hasOlderArchived,
true,
"B→C: hasOlderArchived reset again",
);
await act(async () => {
root.unmount();
});
});
});
@@ -347,80 +347,101 @@ describe("load-older cursor advance logic", () => {
// must reset when channelId changes so channel B starts with a fresh cursor
// and hasOlderArchived=true rather than inheriting channel A's exhausted state.
//
// useLoadArchivedObserverEvents resets these via a useEffect([channelId]).
// We verify the underlying state-machine semantics here without React.
// useLoadArchivedObserverEvents delegates all mutable paging state to
// archivePagingState.ts (createArchivePagingState / applyChannelReset).
// We test those functions directly — tests would fail if the implementation
// were removed or if the channel-reset touched backfill state it must not.
import {
createArchivePagingState,
applyChannelReset,
} from "@/features/agents/ui/archivePagingState.ts";
describe("archive paging state reset on channel change", () => {
it("test_channel_switch_resets_cursor_and_exhaustion", () => {
// Simulate channel A paging to exhaustion.
let hasOlderArchived = true;
let cursor = null;
let isFetching = false;
it("test_fresh_state_has_correct_initial_values", () => {
const ps = createArchivePagingState();
// Simulate a successful full-page fetch for channel A (cursor advances).
const pageA = Array.from({ length: 5 }, (_, i) => ({
id: `a${i}`,
created_at: 100 - i,
}));
cursor = {
createdAt: pageA[pageA.length - 1].created_at,
id: pageA[pageA.length - 1].id,
};
// Short page → exhausted.
hasOlderArchived = pageA.length >= 50; // false
assert.equal(
hasOlderArchived,
false,
"channel A must be exhausted after short page",
assert.equal(ps.hasSubscription, null, "hasSubscription starts null");
assert.equal(ps.hasOlderArchived, true, "hasOlderArchived starts true");
assert.equal(ps.isFetching, false, "isFetching starts false");
assert.equal(ps.backfillStatus, "pending", "backfillStatus starts pending");
assert.notEqual(
ps.backfillPromise,
null,
"backfillPromise is eagerly initialized",
);
assert.notEqual(cursor, null, "cursor must be set after channel A fetch");
assert.equal(ps.cursor, null, "cursor starts null");
});
// Simulate the useEffect([channelId]) reset on channel switch.
// This is what the new effect in useLoadArchivedObserverEvents does.
cursor = null;
isFetching = false;
hasOlderArchived = true;
it("test_channel_switch_resets_cursor_exhaustion_and_fetch_lock", () => {
const ps = createArchivePagingState();
// Simulate channel A paging to exhaustion with a non-null cursor.
ps.cursor = { createdAt: 1000, id: "event-a5" };
ps.hasOlderArchived = false; // channel A exhausted
ps.isFetching = true; // mid-flight request (edge case)
ps.backfillStatus = "done"; // backfill ran once already
const originalPromise = ps.backfillPromise; // must survive reset
// Channel switch — this is what the useEffect([channelId]) calls.
applyChannelReset(ps);
assert.equal(ps.cursor, null, "cursor resets to null on channel switch");
assert.equal(
hasOlderArchived,
ps.hasOlderArchived,
true,
"hasOlderArchived must reset to true on channel switch",
"hasOlderArchived resets to true on channel switch",
);
assert.equal(cursor, null, "cursor must reset to null on channel switch");
assert.equal(
isFetching,
ps.isFetching,
false,
"isFetching must reset to false on channel switch",
"isFetching resets to false on channel switch",
);
// Backfill state must NOT be touched — it is identity-level and should
// survive channel switches so the backfill only runs once per identity mount.
assert.equal(
ps.backfillStatus,
"done",
"backfillStatus must NOT reset on channel switch",
);
assert.equal(
ps.backfillPromise,
originalPromise,
"backfillPromise must NOT reset on channel switch",
);
assert.equal(
ps.hasSubscription,
null,
"hasSubscription must NOT reset on channel switch",
);
});
it("test_channel_switch_does_not_reset_backfill_state", () => {
// Backfill state is identity-level, not per-channel. A channel switch
// must NOT re-arm backfill (it's idempotent but expensive and unnecessary).
// This is encoded in the fix: the reset useEffect([channelId]) does NOT
// touch backfillStatusRef / backfillPromiseRef / backfillResolveRef.
//
// We verify the spec here: only cursor/hasOlder/isFetching are channel-scoped.
const channelScopedFields = ["cursor", "hasOlderArchived", "isFetching"];
const identityScopedFields = [
"backfillStatus",
"backfillPromise",
"backfillResolve",
];
it("test_multiple_channel_switches_each_start_fresh", () => {
const ps = createArchivePagingState();
// Channel-scoped fields must reset; identity-scoped must not.
assert.ok(
channelScopedFields.every((f) =>
["cursor", "hasOlderArchived", "isFetching"].includes(f),
),
"cursor, hasOlderArchived, isFetching are channel-scoped and must reset",
// Switch to channel A: exhaust it.
ps.cursor = { createdAt: 500, id: "a-oldest" };
ps.hasOlderArchived = false;
applyChannelReset(ps);
assert.equal(ps.cursor, null, "switch A→B: cursor reset");
assert.equal(
ps.hasOlderArchived,
true,
"switch A→B: hasOlderArchived reset",
);
assert.ok(
identityScopedFields.every((f) =>
["backfillStatus", "backfillPromise", "backfillResolve"].includes(f),
),
"backfill state is identity-scoped and must NOT reset on channel switch",
// Simulate channel B also being paged.
ps.cursor = { createdAt: 200, id: "b-oldest" };
ps.hasOlderArchived = false;
applyChannelReset(ps);
assert.equal(ps.cursor, null, "switch B→C: cursor reset again");
assert.equal(
ps.hasOlderArchived,
true,
"switch B→C: hasOlderArchived reset again",
);
});
});
@@ -0,0 +1,67 @@
/**
* Archive paging state machine for useLoadArchivedObserverEvents.
*
* Extracted from the hook so the two reset paths — channel change and identity
* change — can be expressed as pure functions and exercised directly in tests
* without a React runtime.
*
* The hook owns all React state/ref wrappers; this module owns the logic of
* what gets reset under what condition.
*/
export interface ArchivePagingState {
/** Whether the current identity has an owner_p save subscription.
* null = not yet checked; true/false = result of listSaveSubscriptions(). */
hasSubscription: boolean | null;
/** Whether older archived rows exist for the current channel. */
hasOlderArchived: boolean;
/** True while a fetchOlderArchived call is in flight. */
isFetching: boolean;
/** Backfill lifecycle: "pending" → "running" → "done". */
backfillStatus: "pending" | "running" | "done";
/** Promise that resolves when backfill completes. Awaited by fetchOlderArchived
* so the first scroll-trigger never races the index write path. */
backfillPromise: Promise<void> | null;
/** Resolve callback for backfillPromise. */
backfillResolve: (() => void) | null;
/** Compound keyset cursor: (created_at, id) of the oldest row fetched.
* Mirrors SQL ORDER BY created_at DESC, id DESC so same-second siblings are
* never skipped at a page boundary. */
cursor: { createdAt: number; id: string } | null;
}
/**
* Create a fresh ArchivePagingState with an eagerly-initialized backfill
* promise, so fetchOlderArchived can await it before the backfill effect fires.
*/
export function createArchivePagingState(): ArchivePagingState {
const state: ArchivePagingState = {
hasSubscription: null,
hasOlderArchived: true,
isFetching: false,
backfillStatus: "pending",
backfillPromise: null,
backfillResolve: null,
cursor: null,
};
state.backfillPromise = new Promise<void>((resolve) => {
state.backfillResolve = resolve;
});
return state;
}
/**
* Reset per-channel paging state when the viewed channel changes.
*
* Only cursor, exhaustion flag, and fetch lock are channel-scoped. Backfill
* state is identity-level (the index covers ALL channels and needs to run only
* once per identity mount), so it is intentionally NOT touched here.
*
* Called by the useEffect([channelId]) in useLoadArchivedObserverEvents.
* Exported so tests can verify the reset semantics without a React runtime.
*/
export function applyChannelReset(state: ArchivePagingState): void {
state.cursor = null;
state.isFetching = false;
state.hasOlderArchived = true;
}
@@ -17,6 +17,11 @@ import { decryptObserverEvent } from "@/shared/api/tauriObserver";
import { useIdentityQuery } from "@/shared/api/hooks";
import type { TranscriptItem } from "./agentSessionTypes";
import type { RelayEvent } from "@/shared/api/types";
import {
createArchivePagingState,
applyChannelReset,
} from "./archivePagingState";
export type { ArchivePagingState } from "./archivePagingState";
// Stable subscribe reference shared by all useSyncExternalStore hooks.
// subscribeAgentObserverStore already has a fixed identity, so this thin
@@ -81,47 +86,33 @@ export function useLoadArchivedObserverEvents(
const identityQuery = useIdentityQuery();
const identityPubkey = identityQuery.data?.pubkey ?? null;
// Whether the current identity has an owner_p save subscription.
// All mutable paging state lives in one stable ref. createArchivePagingState
// initialises the backfill promise eagerly so fetchOlderArchived can await it
// before the backfill effect fires. applyChannelReset resets cursor/exhaustion/
// fetchLock when channelId changes; backfill state is untouched (identity-level).
const pagingStateRef = React.useRef(createArchivePagingState());
const ps = pagingStateRef.current;
// React state mirrors the fields callers observe so re-renders fire on change.
const [hasSubscription, setHasSubscription] = React.useState<boolean | null>(
null,
ps.hasSubscription,
);
const [hasOlderArchived, setHasOlderArchived] = React.useState(true);
const isFetchingRef = React.useRef(false);
// Backfill state: "pending" → "running" → "done".
// fetchOlderArchived awaits backfillPromiseRef before reading the index so
// the first scroll-trigger never races the write path and incorrectly marks
// the channel exhausted before backfill has completed.
const backfillStatusRef = React.useRef<"pending" | "running" | "done">(
"pending",
);
const backfillPromiseRef = React.useRef<Promise<void> | null>(null);
const backfillResolveRef = React.useRef<(() => void) | null>(null);
// Expose a promise that resolves when backfill is done. Created eagerly so
// fetchOlderArchived can await it before the effect that starts backfill fires.
if (!backfillPromiseRef.current) {
backfillPromiseRef.current = new Promise<void>((resolve) => {
backfillResolveRef.current = resolve;
});
}
// Compound keyset cursor: tracks both `created_at` and `id` of the oldest
// event seen so far. Mirrors the SQL `ORDER BY created_at DESC, id DESC` so
// same-second siblings are never skipped at a page boundary.
const cursorRef = React.useRef<{ createdAt: number; id: string } | null>(
null,
const [hasOlderArchived, setHasOlderArchived] = React.useState(
ps.hasOlderArchived,
);
// Reset per-channel paging state when channelId changes. Backfill state is
// identity-level (not per-channel) and must NOT be reset here — the backfill
// index covers all channels and only needs to run once per identity mount.
// Only the cursor, exhaustion flag, and fetching lock are channel-scoped.
// biome-ignore lint/correctness/useExhaustiveDependencies: channelId is the intentional reset key; cursorRef/isFetchingRef are stable refs excluded from deps by convention; setHasOlderArchived is a stable React state setter
// biome-ignore lint/correctness/useExhaustiveDependencies: channelId is the intentional reset key; ps is a stable ref excluded from deps by convention; setHasOlderArchived is a stable React state setter
React.useEffect(() => {
cursorRef.current = null;
isFetchingRef.current = false;
applyChannelReset(ps);
setHasOlderArchived(true);
}, [channelId]);
// Check for an owner_p subscription once per identity.
// biome-ignore lint/correctness/useExhaustiveDependencies: ps is a stable ref excluded from deps by convention; setHasSubscription/setHasOlderArchived are stable React state setters
React.useEffect(() => {
if (!enabled || !identityPubkey) {
return;
@@ -136,20 +127,24 @@ export function useLoadArchivedObserverEvents(
(s) => s.scopeType === "owner_p" && s.scopeValue === identityPubkey,
);
setHasSubscription(hasSub);
ps.hasSubscription = hasSub;
if (!hasSub) {
setHasOlderArchived(false);
ps.hasOlderArchived = false;
// No subscription → backfill will never run; resolve the promise
// immediately so fetchOlderArchived doesn't await indefinitely.
backfillStatusRef.current = "done";
backfillResolveRef.current?.();
ps.backfillStatus = "done";
ps.backfillResolve?.();
}
})
.catch(() => {
if (!cancelled) {
setHasSubscription(false);
ps.hasSubscription = false;
setHasOlderArchived(false);
backfillStatusRef.current = "done";
backfillResolveRef.current?.();
ps.hasOlderArchived = false;
ps.backfillStatus = "done";
ps.backfillResolve?.();
}
});
return () => {
@@ -162,16 +157,13 @@ export function useLoadArchivedObserverEvents(
// observer_channel_index. A status row is written for EVERY processed event —
// null/failed channelId rows get channel_id=null, so re-runs skip them.
// Runs once per mount when the subscription is confirmed; gated by
// backfillStatusRef so fetchOlderArchived can await completion.
// ps.backfillStatus so fetchOlderArchived can await completion.
// biome-ignore lint/correctness/useExhaustiveDependencies: ps is a stable ref excluded from deps by convention
React.useEffect(() => {
if (
!enabled ||
!hasSubscription ||
backfillStatusRef.current !== "pending"
) {
if (!enabled || !hasSubscription || ps.backfillStatus !== "pending") {
return;
}
backfillStatusRef.current = "running";
ps.backfillStatus = "running";
const promise = (async () => {
try {
const rows = await readUnindexedObserverRows();
@@ -226,20 +218,21 @@ export function useLoadArchivedObserverEvents(
error,
);
} finally {
backfillStatusRef.current = "done";
backfillResolveRef.current?.();
ps.backfillStatus = "done";
ps.backfillResolve?.();
}
})();
backfillPromiseRef.current = promise;
ps.backfillPromise = promise;
}, [enabled, hasSubscription]);
// biome-ignore lint/correctness/useExhaustiveDependencies: ps is a stable ref; ps.isFetching/ps.cursor/ps.backfillPromise/ps.hasOlderArchived are read via the stable ref object, not reactive values
const fetchOlderArchived = React.useCallback(async () => {
if (
!enabled ||
!identityPubkey ||
!hasSubscription ||
!channelId ||
isFetchingRef.current ||
ps.isFetching ||
!hasOlderArchived
) {
return;
@@ -249,8 +242,8 @@ export function useLoadArchivedObserverEvents(
// guarantees the index is populated before the first paginated read, so
// a scroll-trigger that fires before backfill writes can't return 0 rows
// and falsely mark the channel exhausted.
if (backfillPromiseRef.current) {
await backfillPromiseRef.current;
if (ps.backfillPromise) {
await ps.backfillPromise;
}
// Re-check after awaiting: hasOlderArchived might have been set false
@@ -259,9 +252,9 @@ export function useLoadArchivedObserverEvents(
return;
}
isFetchingRef.current = true;
ps.isFetching = true;
try {
const before = cursorRef.current ?? undefined;
const before = ps.cursor ?? undefined;
const events = await readArchivedObserverEventsForChannel(channelId, {
before: before ?? null,
limit: ARCHIVED_EVENTS_PAGE_SIZE,
@@ -272,7 +265,7 @@ export function useLoadArchivedObserverEvents(
// this page. Capture both created_at and id to mirror the compound
// sort key so same-second siblings are not skipped on the next page.
const oldestEvent = events[events.length - 1];
cursorRef.current = {
ps.cursor = {
createdAt: oldestEvent.created_at,
id: oldestEvent.id,
};
@@ -282,11 +275,12 @@ export function useLoadArchivedObserverEvents(
// A short page means the archive is exhausted for this channel.
if (events.length < ARCHIVED_EVENTS_PAGE_SIZE) {
setHasOlderArchived(false);
ps.hasOlderArchived = false;
}
} catch (error) {
console.error("[useLoadArchivedObserverEvents] fetch failed:", error);
} finally {
isFetchingRef.current = false;
ps.isFetching = false;
}
}, [enabled, identityPubkey, hasSubscription, channelId, hasOlderArchived]);