Repository navigation
feat(types): type the __dbis__ resume-cursor store so sync get() misuse is a compile error (#485) - #487
Conversation
…se is a compile error (#485) Follow-up to #484 (the seq/copyCursor getSync fix): makes the whole MaybePromise misuse class a compile-time error rather than a latent full-copy-on-upgrade bug, mirroring what #477 did for the hdb_nodes store. On RocksDB `__dbis__.get()` is a MaybePromise — the value synchronously on a block-cache hit, a Promise on a miss. A new `DbisStore` type declares `get(): MaybePromise<DbisCursor | undefined>` and `getSync(): DbisCursor | undefined`, with an index signature so put/remove/getRange/etc. fall through (no broad retype / blast radius). `tableSubscriptionToReplicator.dbisDB` and the `readDbisCursorSync` helper are typed against it, so any synchronous consumption of `get()` — `store.get(k)?.seqId`, truthiness, spread — is now a `tsc` error: use `getSync()` or `await`. - This makes the #484 fix an invariant: reverting `readDbisCursorSync`'s `getSync` back to `get()` no longer satisfies the `DbisCursor | undefined` return type (TS2322), and a direct `tableSubscriptionToReplicator.dbisDB.get(k)?.seqId` read errors (seqId does not exist on MaybePromise). Both verified locally. - Removed a now-redundant `?? 1` on the seq read (every consumer is already `sequenceEntry?.seqId ?? 1/0`); behavior-identical. Pure types + one redundancy removal; no runtime behavior change. tsc adds zero errors vs baseline; unit suite green (194). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
|
Reviewed (re-run after |
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
DavidCockerill
left a comment
There was a problem hiding this comment.
Approving — clean, well-documented, low-risk types change that does what it says for the seq path. One non-blocking finding worth a follow-up.
What's solid
- The invariant works. Explicit
get/getSyncmembers take precedence over the[key: string]: anyindex signature for member access, sostore.get(k)?.seqIdis a real type error whileput/remove/getRangefall through untyped — exactly the "no blast radius" tradeoff claimed. The revert-experiment verification (TS2322 / TS2339) confirms it. - The
?? 1removal is genuinely behavior-identical. All three consumers ofsequenceEntry(lines ~3431, ~3440, ~3449) independently guard with?.seqId ?? …. WhethersequenceEntryis the number1orundefined,(…)?.seqIdyieldsundefinedin both, so the fallbacks land on the same values. Safe, and it drops a confusingnumber | DbisCursorunion.
Main finding (non-blocking) — the sibling copyCursor read is left unguarded
The PR hardens the seq read but not the structurally-identical copyCursor read a few lines down:
// replicationConnection.ts:~3424 — still raw .get(), not getSync
tableSubscriptionToReplicator?.dbisDB?.get([Symbol.for('copyCursor'), nodeId])…consumed synchronously inside discardMalformedCopyCursor(copyCursor: any, …):
if (!copyCursor || copyCursor.currentTable) return copyCursor; // synchronous field read
warn?.(); … remove(...) // else: discard + delete from diskBecause that param is any, the new DbisStore type provides zero compile-time protection here — and it's the same MaybePromise hazard #484/#485 is about. On a block-cache miss get() returns a Promise; !Promise is false and Promise.currentTable is undefined, so a valid copy cursor gets classified as malformed, removed from disk, and downgraded to a full recopy on reconnect (the exact #321-class failure the function exists to prevent).
So the headline claim holds for seq but has a gap on copyCursor. Suggest either converting line ~3424 to getSync and tightening discardMalformedCopyCursor's first param to DbisCursor | undefined (which would then enforce the call site), or explicitly scoping it out with a comment + follow-up issue so the next reader doesn't assume the whole __dbis__ read path is compile-safe. (If #484 already converted that read, disregard — it's not in this diff or in main.)
Minor
DbisCursormerges theseqandcopyCursorshapes into one all-optional type. Pragmatic and documented, but aSeqCursor | CopyCursorunion would lettsccatch readingcopyStartTimeoff a seq row. Not worth blocking.MaybePromiseis now redeclared in a 4th file (alongsideknownNodes.ts,Table.ts,DatabaseTransaction.ts). Consistent with convention, but edging toward a sharedtypesmodule — separate cleanup, not this PR.
Merge gating
The ?? 1 removal is behavior-identical given #484's readDbisCursorSync/getSync, so this is correct only if #484 lands first (merging ahead of it onto main wouldn't compile). The "rebase onto main once #484 merges" note covers it — just keep the order.
Reviewed with Claude Code.
…edCopyCursor (#485) Tightens discardMalformedCopyCursor's first param from `any` to `DbisCursor | undefined` (and `dbisDB` to `DbisStore | undefined`), so the #485 compile-time invariant reaches the copyCursor path too: passing a raw dbisDB.get() result (a MaybePromise on a RocksDB block-cache miss) is now a tsc error at the call site instead of a Promise that reads as malformed and gets remove()d from disk — a spurious full-recopy on reconnect (#321). Type-only: the production read already uses getSync via readDbisCursorSync (#484), so compiled JS is byte-identical; this closes the type gap David flagged so the whole __dbis__ read path is compile-safe, not just `seq`. tsc error multiset unchanged from baseline; enforcement verified (a raw get() at the call site now yields TS2345 MaybePromise-not-assignable). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks @DavidCockerill — good catch on the Two parts to your finding, and they split:
Verification: tsc error multiset identical to baseline (zero new); revert-experiment confirms enforcement (a raw Leaving your two minors ( — KrAIs (Opus 4.8) |
Closes #485. Stacked on #484 (base =
kris/seq-cursor-getsync); rebase ontomainonce #484 merges.Summary
Makes the
__dbis__resume-cursorMaybePromisemisuse a compile-time error instead of a latent runtime full-copy-on-upgrade bug — the durable follow-up to #484, mirroring what #477 did for thehdb_nodesstore.On RocksDB
__dbis__.get()is aMaybePromise(the value on a block-cache hit, a Promise on a miss). A newDbisStoretype declares the read path honestly:tableSubscriptionToReplicator.dbisDBand thereadDbisCursorSynchelper are typed against it, so any synchronous consumption ofget()(store.get(k)?.seqId, truthiness, spread) is now atscerror — you must usegetSync()orawait.Changes
DbisCursor/DbisStore/MaybePromisetypes;readDbisCursorSynctyped(dbisDB: DbisStore | undefined) => DbisCursor | undefined;tableSubscriptionToReplicatorannotated{ dbisDB?: DbisStore; [key: string]: any }.?? 1on the seq read (every consumer is alreadysequenceEntry?.seqId ?? 1/0) — behavior-identical.Verification
readDbisCursorSync'sgetSync→get()⇒TS2322: Type 'MaybePromise<DbisCursor>' is not assignable to type 'DbisCursor'.tableSubscriptionToReplicator.dbisDB.get(k)?.seqIdread ⇒TS2339: Property 'seqId' does not exist on type 'MaybePromise<DbisCursor>'.tsc --noEmiterror multiset is identical to baseline (zero new errors); unit suite green (194 passing).Where to look
tscsignal (same premise as feat(types): make sync get() misuse on hdb_nodes a compile error (#470 follow-up) #477) — it surfaces misuse in-editor and to anyone runningtsc. It is pure types plus one redundancy removal; no runtime behavior change, so the fix(replication): use getSync for seq/copyCursor resume-cursor reads (RocksDB MaybePromise) #484 runtime test still coversreadDbisCursorSync.DbisCursoruses all-optional fields becauseseqandcopyCursorrows share one keyspace;nodesis typedany[]deliberately (the proxied-skip loop readsseqNode.id/.seqIdoff untyped rows — tightening that is out of scope).Follow-up (not here)
The most durable version types
Table.dbisDB/rootStore.dbisDbin core so every consumer (not just this replication path) gets the guard — a coordinated core + harper-pro change. The core audit for #484 found core'sdbisDbreads already disciplined (allgetSync), so this is guard-rail, deferred.🤖 Generated by Claude (Opus 4.8). Reviewed by Codex. (Gemini/
agyleg skipped — Gemini daily quota exhausted, same as #484.)