| … | … | @@ -2554,6 +2554,91 @@ SessionState's truncate / reorg / named-session machinery. |
| 2554 | 2554 | uses an identity `stylize`. |
| 2555 | 2555 | - Gates: pipe scenarios (register before and after the peer opens, a repeated name, an |
| 2556 | 2556 | unknown name, broadcast to 2 peers, close), and a core-gate `inspect` op. |
| 2557 | - **4e-2 design (2026-10-04).** Read in full: |
| 2558 | - hypercore 11.37: `registerExtension`, the extension gc in `_close`, `_open`'s |
| 2559 | session array, `_removeSession`, lib/inspect.js; |
| 2560 | - the replicator's `extension` / `onextension` / `_onpeerupdate`; |
| 2561 | - corestore 7.12.6's SessionTracker and `_ongc`; |
| 2562 | - compact-encoding 3.5.0's `buffer` / `from`; |
| 2563 | - protomux 3.12's `_ondata`. |
| 2564 | - **Extension encodings are byte-level** (the 4a decision). The default `c.buffer` is |
| 2565 | NOT a raw codec: |
| 2566 | - encode: varint length + bytes; |
| 2567 | - decode: varint ("Out of bounds" on empty input), a bounds check ("Out of bounds"), |
| 2568 | then the slice (trailing bytes ignored). |
| 2569 | |
| 2570 | So it is a built-in `ValueEncoding` of its own. `c.from(name)` names reuse 4a's |
| 2571 | `ValueEncoding::from` (`'buffer'` there is binary, as fromNamed's default). |
| 2572 | - **A throw in `_onmessage`** (a decode, or the handler itself) reaches protomux's |
| 2573 | `_ondata` catch and destroys the STREAM. keetd: the message handler returns false with |
| 2574 | the error, and the mux's `_safe_destroy` follows, as for every other message. |
| 2575 | - **The peer-add order is interleaved:** `_onpeerupdate` walks the monitors newest |
| 2576 | first, and for EACH one emits 'peer-add' and then copies that session's extensions |
| 2577 | into `peer.extensions`. A name registered on several sessions ends up with the |
| 2578 | OLDEST session's extension. keetd's `emitMonitors` (all emits, then nothing) gets a |
| 2579 | per-session hook for this. |
| 2580 | - **Identity matters:** `destroy` deletes a peer's entry only if it is THIS extension |
| 2581 | (`peer.extensions.get(name) === ext`). Re-registering keeps the same object and |
| 2582 | replaces its handlers, encoding and session. Extensions live in an insertion-ordered |
| 2583 | map on the session (JS Map) and in `peer.extensions`. An extension holds its session |
| 2584 | weakly (`alive_`): JS's `session.peers` is `[]` once closed. |
| 2585 | - **`inspect` needs JS's session ARRAY** (`this.sessions`), which keetd lacks. It is |
| 2586 | shared by a parent and every session made from it, or comes from `opts.sessions`. |
| 2587 | Each opening session pushes itself (`_sessionIndex`), and `_removeSession` swap-pops |
| 2588 | it, then `ongc`. Corestore passes a PER-CORE array (SessionTracker: id → array; `_ongc` |
| 2589 | drops an empty one). keetd's corestore keeps one store-wide `std::set`, which also |
| 2590 | iterates in POINTER order on close (JS: map insertion order, then array order); it is |
| 2591 | replaced by the tracker. |
| 2592 | - `inspect(depth, { indentationLvl, stylize })`: lib/inspect.js's text, stylize called |
| 2593 | with JS's `String(value)` and the style name. `toHex(null)` prints `null`; at most 5 |
| 2594 | peers, then `... and N more`. |
| 2595 | - **OWED (not 4e-2):** |
| 2596 | - the replicator's per-message wire stats (`stats.wire*` tx / rx, `wireExtension` |
| 2597 | among them): keetd's stats count only invalid data / requests / backoffs; |
| 2598 | - more than 5 peers in `inspect`, and broadcast to 2 peers: these need the multi-peer |
| 2599 | pipe harness (already owed by M5g-1e). |
| 2600 | - **Gates:** |
| 2601 | - **Pipe s20.** JS uploader `upload.extensions` echoes each message back on its |
| 2602 | extension, and `logExt` records the raw wire frames (name, `''` when deduped, and |
| 2603 | bytes). Script ops `registerExtension` / `extSend` / `extBroadcast` / `extDestroy` / |
| 2604 | `inspect`. The script registers before replicating (deferred), sends on c.buffer / |
| 2605 | utf8 / json, re-registers with a new encoding, sends on a name the remote doesn't |
| 2606 | have, destroys one and gets its echo dropped, closes a session (its extensions go), |
| 2607 | and inspects with a peer. |
| 2608 | - **Pipe s20b:** an undecodable message destroys the stream (both sides log |
| 2609 | `stream-error`). |
| 2610 | - **Core gate `inspect`** (no peers, `indentationLvl` 0 / 4, before and after open, a |
| 2611 | snapshot), plus the sessions count through child sessions and closes. |
| 2612 | - **DONE 2026-10-04** (keetd b6714cd + 6041179). |
| 2613 | - **Upstream fact found by s20b:** a throw in a message handler does NOT just destroy |
| 2614 | the stream. protomux's `_safeDestroy` runs **safety-catch** first, and that RETHROWS |
| 2615 | "programmer" errors as UNCAUGHT exceptions: TypeError, SyntaxError, ReferenceError, |
| 2616 | EvalError, RangeError, URIError, code ERR_ASSERTION, name AssertionError. It throws |
| 2617 | synchronously AND again in a microtask, and the stream stays up. So a peer sending |
| 2618 | invalid JSON on a json extension raises the uncaught exception twice (measured: |
| 2619 | `["SyntaxError", "SyntaxError"]`). A plain Error (c.buffer's "Out of bounds") still |
| 2620 | destroys the stream. |
| 2621 | - **User decision: faithful + hook.** |
| 2622 | - `hc::JsError` carries a JS class / code; `hcore::Error` is one, of class Error. |
| 2623 | - `isActuallyUncaught` is safety-catch's rule. |
| 2624 | - A process-wide `setUncaughtExceptionHandler` mirrors `Bare.on('uncaughtException')`; |
| 2625 | the default prints and aborts, as Bare does with no handler. |
| 2626 | - A handler throws `mux::Uncaught`; the mux calls the hook now and again deferred, |
| 2627 | and abandons the frame without destroying the stream. |
| 2628 | - **`Session::id()` is the core's id whether or not the session opened.** The inspect |
| 2629 | gate caught the old `opened ? … : null`. |
| 2630 | - Gates green: pipe s20 / s20b, core gate `inspect`, store gate `sessions`; ctest 64/64. |
| 2631 | - **Drill `m5g4e2`: 17/17 red.** The first run killed 15; its 2 survivors were gate |
| 2632 | gaps and became cases: |
| 2633 | - **e6 on two sessions:** a later registration overwrites the peer's entry, and |
| 2634 | destroying the earlier one must leave it (identity); |
| 2635 | - **store gate `sessions`:** a moved session leaves from its NEW slot, and |
| 2636 | store.close() walks the arrays. |
| 2637 | - **TRAP:** keetd's pipe runner made its script sessions bare on the core, so each had |
| 2638 | its OWN array, while peer.js's `store.get()` sessions share corestore's. Script |
| 2639 | sessions now share one array. |
| 2640 | - **Process note:** 4e-2's gates were written alongside the code, not run red first. |
| 2641 | The drill is the evidence that they are sensitive. |
| 2557 | 2642 | - **4e-3, the secret-stream layer** (a new `ss/` library, transport-agnostic: bytes in, |
| 2558 | 2643 | bytes out). |
| 2559 | 2644 | - The whole noise-handshake surface: NN / NNpsk0 / XX / XXpsk0 / IK / XK, psk, |