Await durable persistence for existing cards in StoreService.add() - #5679
Await durable persistence for existing cards in StoreService.add()#5679lukemelia wants to merge 3 commits into
Conversation
For an existing card, add() queued a fire-and-forget autosave and returned immediately, so awaiting add() (and callers like SaveCardCommand) could resolve before serialization and the realm PATCH completed. A caller could report "saved" while the durable resource still held pre-mutation state, and late persistence errors were stored in autosave state where they never reached the caller. Await persistAndUpdate() for both new and existing cards in the default path, mirroring StoreService.patch(). Optimistic behavior still requires an explicit doNotWaitForPersist. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Not ready to approve
add() now bypasses the autosave queue for existing cards, which can leave AutoSaveState (e.g., hasUnsavedChanges / lastSaved / lastSaveError) out of sync with the completed save, changing observable UI state compared to the prior save(id) path.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Pull request overview
This PR changes StoreService.add() so that, when persistence is enabled and optimistic mode is not requested, add() only resolves after the card mutation has been durably persisted via persistAndUpdate()—including for existing cards. This aligns add()’s resolution semantics with callers that treat await add() as “saved”.
Changes:
- Update
StoreService.add()to awaitpersistAndUpdate()for existing cards (non-optimistic path). - Add integration regression tests ensuring
add()(1) waits for persistence, (2) stays pending while persistence is gated, and (3) returns a card error on persistence failure.
File summaries
| File | Description |
|---|---|
| packages/host/app/services/store.ts | Makes add() await durable persistence in the default path (existing + new cards). |
| packages/host/tests/integration/store-test.gts | Adds regression tests that fail if add() resolves before persistence or swallows persistence errors. |
Review details
Suppressed comments (2)
packages/host/tests/integration/store-test.gts:1155
- [Claude Code 🤖] Similar to the
persistAndUpdategate above, storing a boundsaveCardDocumentand restoring the bound version infinallycan subtly change behavior for subsequent tests. Capturing and restoring the original method reference avoids altering the service under test.
let store = storeService as any;
let originalSaveCardDocument = store.saveCardDocument.bind(store);
store.saveCardDocument = async () => {
throw new Error('intentional persistence failure');
};
packages/host/tests/integration/store-test.gts:1082
- [Claude Code 🤖] This comment uses temporal phrasing (“reported bug”) and asserts intent (“proves”) rather than stating the contract being tested. It’s clearer and more evergreen to say the test reads the durable backing JSON immediately to ensure
add()only resolves after persistence is complete.
// No waitUntil here: if add() resolved before persistence completed (the
// reported bug), the durable resource would still hold the pre-mutation
// state. Reading the backing JSON immediately proves add() waited.
- Files reviewed: 2/2 changed files
- Comments generated: 3
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
The stubs captured `x.bind(store)` and reassigned that bound copy in `finally`. Both targets are prototype methods, so this left a permanent own property with a different identity and arity shadowing the original. Capture the unbound method, call it with an explicit receiver, and delete the shadowing property to restore. Also reframe two comments that described the reproduction they came from rather than the invariant they assert. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Awaiting persistAndUpdate directly skipped two things doAutoSave was doing. useEphemeralState is the store's only write-permission check, and it guards the autosave path this no longer goes through — persistAndUpdate has none of its own. So add() would PATCH a realm the user cannot write to, where the autosave path had quietly declined. Re-applied for cards that already exist, mirroring what this branch used to do: a card with no id yet was always persisted directly and generally has no realm to check against. AutoSaveState was only updated by the queue, so an add()-driven save left lastSaved and lastSaveError reporting whatever the queue last did. The state-transition wrapper is now shared by both paths via trackingSaveState. Error handling stays per-caller: the queue swallows, because nothing awaited it and a throw would surface as an unhandled rejection, while add() propagates. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Problem
For an existing card,
StoreService.add()queued a fire-and-forget autosave and returned immediately, soawait add()— and callers likeSaveCardCommand— could resolve before serialization and the realm PATCH completed. A caller could report "saved," commit its own transaction, and finish even though no durable revision existed yet. Late persistence errors were stored in autosave state where they could never reach the caller. Only the new-card branch ofadd()awaitedpersistAndUpdate().Fix
In the default (non-optimistic) path,
add()now awaitspersistAndUpdate()for both new and existing cards, collapsing the two sub-branches. This mirrors whatStoreService.patch()already does. As a result:SaveCardCommandthrows instead of silently reporting success.doNotWaitForPersist: true(that branch is unchanged).Only existing-card
add()calls that passed neitherdoNotPersistnordoNotWaitForPersistchange behavior; new-card, render-context, and optimistic callers are untouched.Tests
Three regression tests in
store-test.gts, each written to fail against the old fire-and-forget path:add()with nowaitUntil.persistAndUpdatewith aDeferredand assertsadd()does not resolve while the gate is closed.add()returns a card error rather than swallowing it.Verification
ember-tsc --noEmit: cleanThe full host QUnit suite was not run locally (needs the realm/matrix server stack).
Fixes CS-12409.
🤖 Generated with Claude Code