refactor(contracts): extract the broker stores' shared persistence, not their model (#497) - #518
Merged
Merged
Conversation
…ot their model (#497) #497 set the trigger for generalising the brokered-contract state-store *model* at a third store. There are two, and they are genuinely different models: `BackupBrokerStateStore` holds one record per host because a capture has one open operation at a time, while `RestoreBrokerStateStore` is keyed by request id with a serialising lock and retention pruning. Two data points is not enough to know where that boundary belongs, so this does not touch it. What IS identical between them, character for character apart from a log string, is `read()` and `write()`. That is what this extracts, into a composed `JsonRecordStore<T>` — not a base class. Each store keeps its own `update()`, locking, expiry and query methods entirely to itself, so nothing about the model is prejudged and a third store gets the mechanism for free without inheriting a shape it may not want. The two policies are the reason this is worth having in one place, and both are choices about what must NOT happen: - a read fails open to `{}`, because a missing file is the first-run case and an unreadable one must not wedge the broker — the record is bookkeeping, and refusing to proceed without it would turn a corrupt file into an outage. Each caller's own expiry rule then decides what empty means; `isPrepareExpired` reads an unparseable timestamp as expired precisely so that failing open here cannot leave a dump on disk forever; - a write warns rather than throws, because losing the record costs bookkeeping and never the correctness of the operation a provider is mid-way through announcing. Those were previously asserted only indirectly, through the two stores that happened to implement them identically. They now have direct tests, including the corrupt-file and failing-storage paths that the indirect coverage never reached. Honest accounting: this removes about 40 lines of duplicated logic and adds a 75-line module, most of it the explanation above, so raw line count goes UP. The gain is that the two policies have one home and one test, not that there is less code. Also removes an orphaned `private logger` field the extraction left behind in the restore store — unused private fields do not trip the linter. Refs #497 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vck5KSX2CLxhohx14nb5Sh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #497 — deliberately not closing it. See "What this leaves open" below.
Why this isn't what #497 asked for
#497 sets the trigger for generalising the broker-state model at a third store. There are two, and since I filed it they have diverged further (#502 added retention pruning, #509/#510 made a second service depend on the restore store's locking). They are genuinely different models:
BackupBrokerStateStoreRestoreBrokerStateStoreupdate()transition(compare-and-set),get,pendingFor,retainWhileWaitingForcing a shared base on two data points would be guessing at the boundary, so this doesn't.
What it does instead
read()andwrite()are character-for-character identical between the two, apart from a log string and the type. That is extracted into a composedJsonRecordStore<T>— composition, not inheritance, so each store keeps its ownupdate(), locking, expiry and queries, and a third store can take the mechanism without inheriting a shape it may not want.The two policies are why this is worth centralising, and both are choices about what must not happen:
{}. A missing file is the first-run case; an unreadable one must not wedge the broker. The record is bookkeeping, and refusing to proceed without it would turn a corrupt file into an outage. Each caller's expiry rule then decides what empty means —isPrepareExpiredtreats an unparseable timestamp as expired precisely so failing open here cannot leave a dump on disk forever.Those were previously asserted only indirectly, via two stores that happened to implement them the same way. They now have direct tests — including the corrupt-file and failing-storage paths, which the indirect coverage never reached.
Honest accounting
This removes ~40 lines of duplicated logic and adds a 75-line module, most of it the explanation above. Raw line count goes up. The gain is that the two policies have one home and one test, not that there is less code. If that trade isn't worth it to you, this is a clean revert — the model generalisation in #497 is unaffected either way.
What this leaves open
#497 stays open for the model question, with its trigger intact: a third brokered store is still the signal to look at whether the single-record and keyed-by-id shapes want a common abstraction. This change makes that cheaper to do (the mechanism is already shared) without pre-judging it.
Verification
Server 1392 → 1397 (+5 direct tests for the extracted policies). Both stores' own suites unchanged and green (
restore-broker.test.ts22/22). Full gate green: typecheck ×2, lint, test (1397 server / 377 web / 286 CLI), build.Also removes an orphaned
private loggerfield the extraction left in the restore store — unused private fields don't trip the linter, so it would have sat there.🤖 Generated with Claude Code
https://claude.ai/code/session_01Vck5KSX2CLxhohx14nb5Sh