Skip to content

feat(types): type the __dbis__ resume-cursor store so sync get() misuse is a compile error (#485) - #487

Merged
kriszyp merged 2 commits into
mainfrom
kris/485-dbis-typed-store
Jun 25, 2026
Merged

kriszyp merged 2 commits into
mainfrom
kris/485-dbis-typed-store

Conversation

@kriszyp

@kriszyp kriszyp commented Jun 25, 2026

Copy link
Copy Markdown
Member

Closes #485. Stacked on #484 (base = kris/seq-cursor-getsync); rebase onto main once #484 merges.

Summary

Makes the __dbis__ resume-cursor MaybePromise misuse 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 the hdb_nodes store.

On RocksDB __dbis__.get() is a MaybePromise (the value on a block-cache hit, a Promise on a miss). A new DbisStore type declares the read path honestly:

type DbisStore = {
  get(key: any): MaybePromise<DbisCursor | undefined>;
  getSync(key: any): DbisCursor | undefined;
  [key: string]: any;   // put/remove/getRange/… 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 — you must use getSync() or await.

Changes

  • DbisCursor / DbisStore / MaybePromise types; readDbisCursorSync typed (dbisDB: DbisStore | undefined) => DbisCursor | undefined; tableSubscriptionToReplicator annotated { dbisDB?: DbisStore; [key: string]: any }.
  • Removed a now-redundant ?? 1 on the seq read (every consumer is already sequenceEntry?.seqId ?? 1/0) — behavior-identical.

Verification

  • The invariant holds, verified locally:
    • reverting readDbisCursorSync's getSync → get() ⇒ TS2322: Type 'MaybePromise<DbisCursor>' is not assignable to type 'DbisCursor'.
    • a direct tableSubscriptionToReplicator.dbisDB.get(k)?.seqId read ⇒ TS2339: Property 'seqId' does not exist on type 'MaybePromise<DbisCursor>'.
  • tsc --noEmit error multiset is identical to baseline (zero new errors); unit suite green (194 passing).

Where to look

Follow-up (not here)

The most durable version types Table.dbisDB / rootStore.dbisDb in 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's dbisDb reads already disciplined (all getSync), so this is guard-rail, deferred.


🤖 Generated by Claude (Opus 4.8). Reviewed by Codex. (Gemini/agy leg skipped — Gemini daily quota exhausted, same as #484.)

…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>
@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@claude

claude Bot commented Jun 25, 2026 •

Copy link
Copy Markdown
Contributor

Reviewed (re-run after 26222f5); no blockers found. David's non-blocking finding — discardMalformedCopyCursor params untyped, leaving the copyCursor path without the compile-time guard — has been addressed.

@kriszyp
kriszyp marked this pull request as ready for review June 25, 2026 04:48
@kriszyp
kriszyp requested a review from a team as a code owner June 25, 2026 04:48
@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@kriszyp
kriszyp requested review from cb1kenobi and removed request for a team and kylebernhardy June 25, 2026 10:35
Base automatically changed from kris/seq-cursor-getsync to main June 25, 2026 11:49

@DavidCockerill DavidCockerill left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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/getSync members take precedence over the [key: string]: any index signature for member access, so store.get(k)?.seqId is a real type error while put/remove/getRange fall through untyped — exactly the "no blast radius" tradeoff claimed. The revert-experiment verification (TS2322 / TS2339) confirms it.
  • The ?? 1 removal is genuinely behavior-identical. All three consumers of sequenceEntry (lines ~3431, ~3440, ~3449) independently guard with ?.seqId ?? …. Whether sequenceEntry is the number 1 or undefined, (…)?.seqId yields undefined in both, so the fallbacks land on the same values. Safe, and it drops a confusing number | DbisCursor union.

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 disk

Because 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

  • DbisCursor merges the seq and copyCursor shapes into one all-optional type. Pragmatic and documented, but a SeqCursor | CopyCursor union would let tsc catch reading copyStartTime off a seq row. Not worth blocking.
  • MaybePromise is now redeclared in a 4th file (alongside knownNodes.ts, Table.ts, DatabaseTransaction.ts). Consistent with convention, but edging toward a shared types module — 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>
@kriszyp

kriszyp commented Jun 25, 2026

Copy link
Copy Markdown
Member Author

Thanks @DavidCockerill — good catch on the copyCursor path, and you're right that the headline claim shouldn't stop at seq. Pushed 26222f5 to close it.

Two parts to your finding, and they split:

Verification: tsc error multiset identical to baseline (zero new); revert-experiment confirms enforcement (a raw get() at the call site → TS2345: 'MaybePromise<DbisCursor>' is not assignable to parameter of type 'DbisCursor'). Type-only — compiled JS is byte-identical, so #484's runtime test still covers the path.

Leaving your two minors (SeqCursor | CopyCursor union; the 4th MaybePromise redeclaration → shared types module) as the noted follow-ups — same scope call as in the PR description.

— KrAIs (Opus 4.8)

@kriszyp
kriszyp merged commit 8b25c6e into main Jun 25, 2026
31 checks passed
@kriszyp
kriszyp deleted the kris/485-dbis-typed-store branch June 25, 2026 16:35
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.

feat(types): type the __dbis__/seq store so sync get() misuse is a compile error (#484 follow-up)

2 participants