OverviewFilesCommitsIssuesDocsReleasesPeers

keet-daemon commits / 570e814

design: R4 (async storage: C++20 coroutines over a JS-faithful Promise, exact measured hops, scripted virtual completions)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
570e8144ac6df5df28a92c197c3f05268ab4fd3d
by plankatron <fleeky@gmail.com> · 2026-10-06T15:05:58.000Z

Changed files (1) +149 -1

docs/CPP-PORT-DESIGN.md +149 -1
……@@ -3365,7 +3365,155 @@ injected per layer:
33653365 and a cached one counts as a miss, so a poisoned cache heals itself.
33663366 - Not in R3 (noted): `Core::now` stays an injected `Date.now` (gates pin it); l2 storage has
33673367 its own `now_ms`.
3368- **R4:** storage, per the decision above.
3368- **R4 DESIGN (2026-10-06; approved by the user 2026-10-06).** Storage completes asynchronously,
3369 as in JS, and every JS `await` is a continuation at the same place.
3370 - **User decisions (2026-10-06):**
3371 - continuations are **C++20 coroutines** over a JS-faithful Promise (keetd moves from
3372 C++17 to C++20; clang 18);
3373 - **exact microtask hops**: every await costs what it costs in JS, MEASURED on Bare,
3374 never derived on paper;
3375 - the **virtual** runtime delivers storage completions in a **scripted order** (FIFO by
3376 default; a permutation when the gate or the generator asks for one).
3377 - **What JS does (full reads of rocksdb-native 3.17.4 + librocksdb 1a00e82,
3378 hypercore-storage 3.3.0, hypercore 11.37.0, corestore 7.12.6, hyperbee2 2.18.0,
3379 hyperblobs 2.12.1; notes with file:line in the R4 research record):**
3380 - **rocksdb-native.** Every op is ONE libuv thread-pool work item: a read batch is one
3381 MultiGet, a write batch one atomic Write, each iterator open / refill (8 entries) /
3382 close its own item. A batch submits ONE microtask after `flush()` / `tryFlush()`
3383 (`ReadBatch._flush` awaits `super._flush()`); an iterator's `_read` submits
3384 synchronously, its `_open` / `_destroy` after one hop. A completion is one native→JS
3385 call with a FULL microtask checkpoint after it, each completion its own checkpoint even
3386 when one `uv__work_done` pass delivers several. Inside one, the per-op promises resolve
3387 in index order, then (auto-destroy) the batch's flush promise. Completions arrive in
3388 thread-pool FINISH order (4 threads, shared with fs), not submission order; batches
3389 overlap and are not serialised. Snapshots, properties and stats are synchronous. Read
3390 errors are per op (AggregateError on the flush); write errors batch-wide; a
3391 submission throw is an unhandled rejection whose promises never settle. Suspend drains
3392 in-flight io, later ops park FIFO; close waits for io, handles, sessions.
3393 - **hypercore-storage.** The View is consulted SYNCHRONOUSLY when a get is issued (a hit
3394 is a resolved promise and no DB op). Raw getters resume the caller one hop after the
3395 completion, decoding getters two, `CoreRX.getGroup` three, a tree-node cache miss
3396 four (cached: one, no I/O). `CoreTX.flush` submits with no await of its own and resumes
3397 the caller four hops after the write; on an atom it applies synchronously and returns
3398 `Promise.resolve(false)`. `Atom.flush` throws when already flushing, runs preflushes
3399 synchronously, awaits the write, then `Promise.all` of the onflush handlers, then resets
3400 the view. Store mutations go through a FIFO ScopeLock with a shared View and GROUP
3401 COMMIT (`_exit` writes only when `enters === 0` or the view holds > 128 changes, lock
3402 held across the write). Streams capture the overlay when created; dependency block
3403 streams run one iterator after another. (All hop counts here are DERIVED; R4a measures
3404 them.)
3405 - **hypercore.** protomux does not serialise handlers: storage reads of many messages
3406 are in flight at once. The locks are `SessionState.mutex` (lock() is one hop even when
3407 free; FIFO), the replicator's `_rxLock` (only `ondata` with `request === 0` or an
3408 upgrade) and `_txLock` (requests with an upgrade and `remoteAllowPush`), `exclusive`,
3409 the mark mutex, `receiverQueue` (only while the mux is not drained).
3410 `_verifyShared` batching: data arriving while it waits for the mutex joins the same op
3411 list (`_verifies` cleared only after the lock is granted). Sequential per-level read
3412 chains: `missingNodes` (≤ 1024 batches), ByteSeeker, `seekTrustedTree` /
3413 `seekFromHead`, truncate / reorg, `ReorgBatch._update`, `_validateCommit`, audit.
3414 Proof generation queues its node reads on the caller's batch synchronously (a seek
3415 reads first, in its own batches). `'manifest'` and `onupgrade` fire BEFORE the flush is
3416 awaited. `_send` registers the request inflight, THEN awaits `missingNodes`, then sends.
3417 `yieldToLoop` (setImmediate) is the one phase use; `seeker.update` is awaited in
3418 `seek` and `_updateNonPrimary`.
3419 - **Above.** corestore's dedup is a synchronous map (the Core exists before any I/O; a
3420 named get reads its alias first). hyperbee2 reads in parallel inside a node and in
3421 sequence across levels; `ptr.inflating` is a shared in-flight promise; `update()` may
3422 WRITE (`setUserData` under `updateLock`). hyperblobs' write stream holds its mutexify
3423 lock across several appends. ScopeLock and mutexify grant on a microtask. Wakeup,
3424 core-coupler and the blobs monitor never touch storage.
3425 - **What keetd does today:** the Db runs librocksdb synchronously on a PRIVATE uv loop
3426 (`cb = nullptr`); `Db::get` reads one key per call (no MultiGet); iterators collect into
3427 vectors; every `CoreRX` getter reads at once (nothing waits for `tryFlush`); `Mutex`
3428 grants synchronously on unlock and 8 callers assert it is idle; `merkle::Storage` is a
3429 synchronous virtual; bee and blobs REQUIRE synchronous Session completion (blobs' `now<>`
3430 throws otherwise); the Core constructor does I/O. About 120 consumer await points, the
3431 l3 interface, about 45 sequences inside L2, and the tools.
3432 - **Design.**
3433 - **`hc::js` (in rt/): Promise, async functions, combinators.**
3434 - `js::Promise<T>`: pending / fulfilled / rejected; `then` reactions run as
3435 microtasks on `rt`'s queue (one PromiseReactionJob each, in registration order).
3436 Resolving with another `js::Promise` schedules the thenable job, as V8 does
3437 (adoption = 2 extra hops). `Promise.resolve(p)` returns `p` itself.
3438 - `js::Async<T>`, the coroutine type of a JS `async` function: the body starts
3439 SYNCHRONOUSLY at the call (`initial_suspend = never`), `co_return v` resolves the
3440 returned promise (a promise value adopts: +2), an exception rejects it.
3441 `co_await p` is JS `await`: one reaction job after `p` settles; awaiting a
3442 non-promise costs one hop.
3443 - Combinators ported from V8's spec steps: `all`, `allSettled`, `race`; and `for await`
3444 over the streamx port (measured hops).
3445 - Unhandled rejections follow Bare: MEASURED first (what Bare does at the checkpoint;
3446 whether it ends the process), then routed through `hc::uncaughtException`'s path.
3447 - Lifetimes: a frame owns its locals and lives until it finishes (fire-and-forget calls
3448 keep it alive). Objects that can die under a pending frame keep the existing
3449 `alive_` tokens; a resumption against a dead owner is dropped, as R3b does for
3450 backoff. No symmetric transfer is needed: every resumption is a microtask, so the
3451 stack never deepens.
3452 - **`rn/`: rocksdb-native's JS layer, ported literally** over librocksdb's ASYNC mode
3453 on `rt::uvLoop()` (not a private loop): ReadBatch (ops queued, submit one hop after
3454 flush, one `rocksdb_read` for all ops = MultiGet, resolve in index order, then the
3455 flush promise), WriteBatch (same, one write), Iterator (a streamx Readable, capacity
3456 8, `_read` submits synchronously), State (open / close / suspend / resume, the io
3457 counter, handle refs, parking), sessions, snapshots (synchronous), column families.
3458 Each librocksdb completion enters "JS" through `rt::callback` (one checkpoint per
3459 completion). DeviceFile moves to `uv_fs_*` on the pool, as `fs.promises` does
3460 (device-file read in full before porting).
3461 - **The thread-pool model in rt.**
3462 - Real: libuv's own pool (`uv_queue_work`, inside librocksdb); completions in finish
3463 order, as JS.
3464 - Virtual: a submit runs the work AT ONCE on the loop thread (real RocksDB, real bytes)
3465 and parks the completion. The poll phase delivers parked completions one per
3466 `runCallback`, in the order the script gives (FIFO by default). `alive_()` counts
3467 parked completions.
3468 - JS reference for FIFO: Bare with `UV_THREADPOOL_SIZE=1` (one worker: FIFO by
3469 construction, no patch). MEASURE that Bare honours it and that completion order is
3470 then submission order before any gate relies on it.
3471 - JS reference for permutations: OPEN — a patched binding that holds completions can
3472 reorder them but may cost each released completion its own checkpoint. R4j measures
3473 whether a JS-side release keeps one checkpoint per completion. If not, permutation
3474 runs compare content and end state only; FIFO runs compare order.
3475 - **hypercore-storage, ported literally on `js::Async`**: View, Atom (throwing on a
3476 concurrent flush), CoreRX / CoreTX / CorestoreRX / CorestoreTX, the tree cache's shared
3477 pending entries, the streams (OverlayStream, BlockDependencyStream, CloseErrorStream,
3478 the ignored `reverse` flags), the store ScopeLock with group commit, migrations,
3479 `_maybeRecover`.
3480 - **The libraries above, call site for call site:** `merkle::Storage` becomes async
3481 (17 entry points); `hcore::Mutex` grants in a microtask, and the idle asserts go; the
3482 Core opens through an `opening` promise (no I/O in the constructor); replicator
3483 handlers become `js::Async` with `_rxLock` / `_txLock` / `receiverQueue` /
3484 `syncsProcessing` as in JS, `yieldToLoop` = `rt::setImmediate`; corestore's
3485 synchronous dedup with `preopen`; hyperbee2's `inflating` futures and ScopeLocks;
3486 hyperblobs' mutexify ports. The replicator.h "wire order may differ" and "synchronous
3487 until R4" notes are removed.
3488 - **Migration (each step keeps the tree green: ctest, every gate, every affected drill):**
3489 - **R4a** C++20 switch (ctest unchanged) + `hc::js`. Gate: Bare probe scripts vs `rt`
3490 (real and virtual), as rt-gate: await on settled / pending, adoption, async return of a
3491 promise, `all` / `allSettled` / `race`, rejection, unhandled rejection, `for await`.
3492 Hop-probe method: a reference chain of N `queueMicrotask` steps runs beside the code
3493 under test; the step at which each resumption lands is its hop count. The same method
3494 measures every library primitive the later steps port (rn get / flush, each
3495 hypercore-storage getter, Atom.flush, ScopeLock, Mutex, mutexify).
3496 - **R4b** `rn/` with completions delivered INLINE (the work runs at submit, its
3497 completion is queued at once), so every layer above keeps working while it converts.
3498 The L2 API returns `js::Promise`s from here on; not-yet-converted callers settle them
3499 by draining microtasks (`rt::drainMicrotasks`, a migration scaffold listed and
3500 removed at R4i; lint fails if it survives). Gate: rn trace vs JS (hops + order).
3501 - **R4c** hypercore-storage onto `rn` + `js::Async`. Gates: the storage gate, the store
3502 gate, the P1 generators, hop traces vs JS.
3503 - **R4d** l3 merkle Storage async. **R4e** core (SessionState, Core, Session, marks,
3504 fully-remote-proof, ByteSeeker, the Mutex). **R4f** replicator. **R4g** corestore.
3505 **R4h** hyperbee2 and hyperblobs (bee's and blobs' synchronous assumptions go).
3506 Each: its gates, its generators, its drills; the m3c-era sets for R4e and R4f.
3507 - **R4i** completions through the pool: Real = librocksdb's `uv_queue_work`; Virtual =
3508 parked + scripted. The scaffold is deleted. Gates: FIFO traces vs Bare at
3509 `UV_THREADPOOL_SIZE=1`; the composed pipe gates on the Real runtime.
3510 - **R4j** permutations: the generators gain a completion-order dimension; the JS
3511 reference per the measurement above.
3512 - **Risks:** coroutine frames outliving their owners (mitigated by `alive_` tokens and
3513 ASan runs per step); hop counts that differ from the derived ones (hence measure-first);
3514 the size (about 120 sites plus l3, L2 and the tools); compile-time growth.
3515 - **Not R4:** network-side awaits that are already continuations (wire, timers);
3516 4e-4 `replicate()` comes after R4i.
33693517- **Then 4e-4** (`createProtocolStream` / `replicate`): the first gate that composes the stack
33703518 is a COMPOSED trace against JS (secret-stream + protomux + replicator), now meaningful.
33713519