You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(types): type the __dbis__/seq store so sync get() misuse is a compile error (#484 follow-up) #485
Each was a non-awaited someStore.get(key) whose result is consumed synchronously (?.field, truthiness, ?? default-as-object, for…of). On a block-cache miss get() returns a Promise — truthy, no fields, not iterable — so the code silently takes the wrong branch once the store grows past block-cache size or right after a cold restart. A manual grep audit (the one that produced #484) confirmed the active paths are currently clean, but a grep is a snapshot: nothing stops the next reader from reintroducing it.
#477 already proved the durable fix for one store: type the store honestly so synchronous consumption of get() is a compile error.
Proposed work
Extend the #477 pattern to the __dbis__ / seq store (the one #484 just fixed at runtime).
Mirror NodeStore from #477: a narrow typed handle that constrains only the read path and lets everything else fall through an index signature (no broad retype / blast radius):
typeDbisStore={// get() is a MaybePromise on RocksDB — a cache miss returns a Promise.get(key: DbisKey): MaybePromise<DbisValue>;getSync(key: DbisKey): DbisValue|undefined;[k: string]: any;// put/remove/getRange/etc. fall through};
DbisValue is the seq row ({ seqId, nodes, lastTxnTime }) ∪ copyCursor ({ copyStartTime, currentTable, afterKey }) ∪ residency, etc. The point is that any synchronous use of get() (store.get(k)?.seqId, if (store.get(k)), spreading it) becomes a TS error, forcing getSync(k) or await.
Design decision to settle (input wanted)
harper-pro boundary only (minimal): type tableSubscriptionToReplicator.dbisDB (currently reached off an any) as DbisStore. Smallest change; covers exactly where the bites have happened (replication).
core-level (most durable): type the dbisDb store where it's created in core (resources/Table.ts / wherever RocksDatabase is exposed as dbisDb) so every consumer — core and harper-pro — gets the compile-time guard. The core audit (fix(replication): use getSync for seq/copyCursor resume-cursor reads (RocksDB MaybePromise) #484) found core's dbisDb reads already disciplined (all getSync), so this is purely a guard-rail, but it's the one that prevents the next core reader from regressing.
Recommendation: (2) if the core dbisDb surface can be typed without a large blast radius; otherwise (1) as a first step. The same reasoning extends to typing primaryStore / system-table stores generally (the broader "stores are typed as lmdb Database (sync get) or any, but are RocksDB at runtime" root cause noted in the #484 audit) — but that can be a later increment; this issue is scoped to the __dbis__/seq store.
Acceptance criteria
A DbisStore-typed handle exists; the seq/copyCursor/residency reads in replicationConnection.ts / replicator.ts typecheck only via getSync / await.
tsc clean; no runtime behavior change (pure types).
The lesson this closes
#477's write-up captured it well: the manual audit under-classified defensive readers as safe because a Promise doesn't throw and is truthy. Typing catches all of them deterministically, no per-site judgment. This issue applies that to the store that caused #484 so we stop finding these one at a time.
Refs: #484 (runtime fix this hardens), #477 (the hdb_nodes typing precedent), #476 / #474 (original hdb_nodes sweep).
Problem
We have now fixed the
store.get()RocksDBMaybePromisemisuse three times by hand:hdb_nodespoint reads (get()Promise read as "no/!replicates" → silent replication disable).__dbis__seq/copyCursor/residencyreads in the subscription handshake (get()Promise read as "no resume cursor" → unnecessary full copy on restart/upgrade, severe for large DBs).Each was a non-awaited
someStore.get(key)whose result is consumed synchronously (?.field, truthiness,?? default-as-object,for…of). On a block-cache missget()returns a Promise — truthy, no fields, not iterable — so the code silently takes the wrong branch once the store grows past block-cache size or right after a cold restart. A manual grep audit (the one that produced #484) confirmed the active paths are currently clean, but a grep is a snapshot: nothing stops the next reader from reintroducing it.#477 already proved the durable fix for one store: type the store honestly so synchronous consumption of
get()is a compile error.Proposed work
Extend the #477 pattern to the
__dbis__/seqstore (the one #484 just fixed at runtime).Mirror
NodeStorefrom #477: a narrow typed handle that constrains only the read path and lets everything else fall through an index signature (no broad retype / blast radius):DbisValueis theseqrow ({ seqId, nodes, lastTxnTime }) ∪copyCursor({ copyStartTime, currentTable, afterKey }) ∪ residency, etc. The point is that any synchronous use ofget()(store.get(k)?.seqId,if (store.get(k)), spreading it) becomes a TS error, forcinggetSync(k)orawait.Design decision to settle (input wanted)
tableSubscriptionToReplicator.dbisDB(currently reached off anany) asDbisStore. Smallest change; covers exactly where the bites have happened (replication).dbisDbstore where it's created in core (resources/Table.ts/ whereverRocksDatabaseis exposed asdbisDb) so every consumer — core and harper-pro — gets the compile-time guard. The core audit (fix(replication): use getSync for seq/copyCursor resume-cursor reads (RocksDB MaybePromise) #484) found core'sdbisDbreads already disciplined (allgetSync), so this is purely a guard-rail, but it's the one that prevents the next core reader from regressing.Recommendation: (2) if the core
dbisDbsurface can be typed without a large blast radius; otherwise (1) as a first step. The same reasoning extends to typingprimaryStore/ system-table stores generally (the broader "stores are typed as lmdbDatabase(sync get) orany, but are RocksDB at runtime" root cause noted in the #484 audit) — but that can be a later increment; this issue is scoped to the__dbis__/seq store.Acceptance criteria
DbisStore-typed handle exists; theseq/copyCursor/residencyreads inreplicationConnection.ts/replicator.tstypecheck only viagetSync/await.getSynccalls back toget()with synchronous consumption is atscerror (the regression guard — same property feat(types): make sync get() misuse on hdb_nodes a compile error (#470 follow-up) #477 giveshdb_nodes).tscclean; no runtime behavior change (pure types).The lesson this closes
#477's write-up captured it well: the manual audit under-classified defensive readers as safe because a Promise doesn't throw and is truthy. Typing catches all of them deterministically, no per-site judgment. This issue applies that to the store that caused #484 so we stop finding these one at a time.
Refs: #484 (runtime fix this hardens), #477 (the
hdb_nodestyping precedent), #476 / #474 (originalhdb_nodessweep).