| … | … | @@ -3365,7 +3365,155 @@ injected per layer: |
| 3365 | 3365 | and a cached one counts as a miss, so a poisoned cache heals itself. |
| 3366 | 3366 | - Not in R3 (noted): `Core::now` stays an injected `Date.now` (gates pin it); l2 storage has |
| 3367 | 3367 | 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. |
| 3369 | 3517 | - **Then 4e-4** (`createProtocolStream` / `replicate`): the first gate that composes the stack |
| 3370 | 3518 | is a COMPOSED trace against JS (secret-stream + protomux + replicator), now meaningful. |
| 3371 | 3519 | |