Skip to content

fix: restart the persistence process when usePersistSignals entries change - #163

Open
mbret wants to merge 1 commit into
mainfrom
fix/bug-hunt-2026-08-03
Open

fix: restart the persistence process when usePersistSignals entries change#163
mbret wants to merge 1 commit into
mainfrom
fix/bug-hunt-2026-08-03

Conversation

@mbret

@mbret mbret commented Aug 3, 2026

Copy link
Copy Markdown
Owner

The bug

usePersistSignals documents that "Passing a new list of entries will start over the process" — but entries passed after the initial hydration are silently ignored: they are never hydrated from storage and their updates are never persisted.

How to observe: render usePersistSignals({ entries: [a], adapter }), wait for isHydrated, then re-render with entries: [a, b] where storage already holds a value for b's key. Signal b keeps its default value forever (stored value never hydrated), and updates to b are never written to the adapter. Only the original entry a keeps working.

Root cause

In usePersistSignals, entries emissions are piped through concatMap(persistSignals):

entriesSubject.pipe(
  concatMap((entries) => persistSignals({ adapter, entries, ... })),
)

concatMap only subscribes to the next inner observable once the previous one completes — but the observable returned by persistSignals never completes on its own: after hydrating, it keeps persisting signal updates indefinitely (persisted$ is an infinite merge of the signals' streams). So every new entries list is queued behind an inner observable that never finishes, and is starved forever.

The fix

Use switchMap instead, mirroring the adapter-change path a few lines above (which already uses switchMap): a new entries list tears down the previous hydrate-and-persist process and starts a fresh one, as documented. The stale "once the current one is finished" phrasing in the entries doc comment is updated to match.

Verification

New regression test src/lib/state/react/usePersistSignals.test.tsx:

  1. hydrates with [a], waits for isHydrated,
  2. re-renders with [a, b] where storage holds 7 for b → asserts b hydrates to 7,
  3. updates b to 9 → asserts the adapter receives the write.

The test times out on main (step 2 never happens) and passes with the fix. Full gates run locally: npm run check, npm run build, and npm run test:ci (25 files / 140 tests) all pass. (useQuery$.test.tsx > should return consecutive results timed out once on a clean main baseline under parallel load but passes in isolation and in the post-fix full run — pre-existing flake, unrelated.)

Other findings (not addressed in this PR)

  • None confirmed. Notable candidates investigated and discarded: queryClient.clear() while a live-stream useQuery$ is mounted looked like it would leave a stale bridge cache entry, but TanStack's cache.remove() calls query.destroy(), which aborts the fetch signal and the bridge's takeUntil(fromEvent(signal, "abort")) tears the entry down correctly (verified empirically against a promise-based baseline).

Generated by Claude Code

…hange

Entries changes were piped through concatMap, but the inner persistSignals
observable never completes (it keeps persisting signal updates), so a new
entries list was queued forever: signals added after the initial hydration
were never hydrated nor persisted. Switch to switchMap — mirroring the
adapter-change path — so a new list restarts the process as documented.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0152ZzwVACMdMfk6jt7Ezzkn

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b94bb31c57

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

* queue new entries forever. Restart the process instead
* whenever a new list is emitted.
*/
switchMap((entries) =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve pending updates when restarting persistence

When the entries reference changes within the 500 ms throttleTime window after an existing signal update, this switchMap unsubscribes the old persistence stream and discards its pending trailing write. The replacement persistSignals then rehydrates every existing entry from the adapter, so the stale stored value overwrites the newer in-memory value and is persisted again; for example, updating a and immediately changing [a] to [a, b] loses the update to a. Restarting should flush/preserve pending values or hydrate only newly added entries rather than rehydrating unchanged signals.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants