@lazily-hub/lazily-js 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -40,10 +40,10 @@ notes and platform carve-outs lives in
40
40
  | --------- | :----: | :------: | :------: | :--: | :----: | :---: | :--: | :---: |
41
41
  | Reactive graph — `Cell` / `Slot` / `Signal` / `Effect` / memo / batch | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
42
42
  | Reactive family (`ReactiveFamily`) — keyed cell/slot family + materialization mode (`#lzmatmode`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
43
- | Thread-safe reactive family (`ThreadSafeReactiveFamily`) — `Send + Sync` keyed family + materialization confluence (`#lzmatmode`) | ✅ | — | — | | — | | — | |
44
- | Async reactive family (`AsyncReactiveFamily`) — keyed family + eventual transparency (`#lzmatmode`) | ✅ | — | — | | — | | — | |
45
- | Reactive family sync — membership propagation + materialize-on-ingest + derived-aggregate transparency (`#lzfamilysync`) | ✅ | — | ✅ | ✅ | — | | — | |
46
- | Thread-safe context (lock-backed) | ✅ | ✅ | ✅ | | — | ✅ | ✅ | ✅ |
43
+ | Thread-safe reactive family (`ThreadSafeReactiveFamily`) — `Send + Sync` keyed family + materialization confluence (`#lzmatmode`) | ✅ | — | — | | — | | — | |
44
+ | Async reactive family (`AsyncReactiveFamily`) — keyed family + eventual transparency (`#lzmatmode`) | ✅ | — | — | | — | | — | |
45
+ | Reactive family sync — membership propagation + materialize-on-ingest + derived-aggregate transparency (`#lzfamilysync`) | ✅ | — | ✅ | ✅ | — | | — | |
46
+ | Thread-safe context (lock-backed) | ✅ | ✅ | ✅ | | — | ✅ | ✅ | ✅ |
47
47
  | Async reactive context | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
48
48
  | Flat state machine | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
49
49
  | Harel state charts | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
@@ -66,10 +66,10 @@ notes and platform carve-outs lives in
66
66
  | State projection / mirror | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
67
67
  | Causal receipts (`CausalReceipts` outcome projection) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
68
68
  | Message-passing + RPC command plane (`command-plane-v1`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
69
- | C-ABI FFI boundary | ✅ | ✅ | ✅ | | ✅ | ✅ | ✅ | ✅ |
69
+ | C-ABI FFI boundary | ✅ | ✅ | ✅ | | ✅ | ✅ | ✅ | ✅ |
70
70
  | Permission boundary (`PeerPermissions` / `RemoteOp`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
71
71
  | Capability negotiation (`SessionHandshake`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
72
- | Instrumentation / benchmarks | ✅ | ✅ | ✅ | | ✅ | ✅ | ✅ | ✅ |
72
+ | Instrumentation / benchmarks | ✅ | ✅ | ✅ | | ✅ | ✅ | ✅ | ✅ |
73
73
  <!-- coverage-table:end -->
74
74
 
75
75
  CRDT convergence and the wire protocol are pinned by the shared conformance fixtures
@@ -83,6 +83,11 @@ and JSON Schemas in `lazily-spec` and the Lean models in `lazily-formal`.
83
83
  | `@lazily-hub/lazily-js/reactive` | Reactive dependency graph: `Context`, `Cell`, `Slot`, `Signal`, `Effect` |
84
84
  | `@lazily-hub/lazily-js/reactive-async` | Async reactive graph: `AsyncContext` — Promise-driven slots/effects with revision-guarded stale-completion discard, in-flight dedup, and cancellation |
85
85
  | `@lazily-hub/lazily-js/reactive-family` | Unified keyed reactive family: `ReactiveFamily` (`EntryKind` cell/slot × `MaterializationMode` eager/lazy) + `cellFamily` input-cell specialization (`#lzmatmode`) |
86
+ | `@lazily-hub/lazily-js/async-reactive-family` | Async keyed reactive family: `AsyncReactiveFamily` + `asyncCellFamily` over `AsyncContext` — eventual transparency (a pending slot observes `undefined` and resolves to the canonical value; eager ≡ lazy once resolved) (`#lzmatmode`) |
87
+ | `@lazily-hub/lazily-js/thread-safe` | Lock-backed reactive context: `ThreadSafeContext` (`Send + Sync` flavor of `Context`) + `AtomicMutex` — a real `SharedArrayBuffer` + `Atomics` reentrant mutex giving cross-worker mutual exclusion; degrades to a single-realm guard where shared memory is unavailable |
88
+ | `@lazily-hub/lazily-js/thread-safe-reactive-family` | Thread-safe keyed reactive family: `ThreadSafeReactiveFamily` + `threadSafeCellFamily` — mutex-guarded present set with first-writer-wins materialization confluence (`#lzmatmode`) |
89
+ | `@lazily-hub/lazily-js/ffi` | C-ABI FFI boundary (`schemas/ffi.json`): message codec (`encodeMessage` / `decodeMessage` / `validateMessage` / `kindOf`, `LazilyFfiStatus` / `LazilyFfiMessageKind`) + `FfiChannel` FIFO. Isomorphic core (browser shim); the Node build additionally exposes `NativeFfiChannel` / `loadNativeChannel` over the real `lazily_ffi_channel_*` C ABI via koffi |
90
+ | `@lazily-hub/lazily-js/instrumentation` | In-library instrumentation/benchmark API: `benchmark`, `runBenchmarkSuite`, `BenchmarkResult`, `withInstrumentation` — plus opt-in reactive-core counters via `new Context({ instrument: true })` / `instrumentationSnapshot()` |
86
91
  | `@lazily-hub/lazily-js/state-machine` | Flat finite-state-machine kernel backed by a reactive `Cell` |
87
92
  | `@lazily-hub/lazily-js/statechart` | Harel/SCXML chart interpreter plus `ChartBuilder`, `StateBuilder`, `TransitionBuilder` |
88
93
  | `@lazily-hub/lazily-js/collections` | `CellMap`, `CellTree`, keyed reconciliation, and LIS move minimization |
@@ -495,7 +500,10 @@ sequence and text CRDTs (incl. `TextCrdt` delta sync, `#lztextsync`:
495
500
  identity, the reactive family / materialization mode (`#lzmatmode`:
496
501
  `materialization/observational_transparency.json`,
497
502
  `materialization/deferral_not_deallocation.json`,
498
- `materialization/entry_kind_orthogonal_to_mode.json`), Harel state charts, the
503
+ `materialization/entry_kind_orthogonal_to_mode.json` replayed through the
504
+ single-threaded, async, and thread-safe families), the C-ABI FFI boundary
505
+ (`schemas/ffi.json`: message codec + channel round-trip over `snapshot_*` /
506
+ `delta_*` wire), Harel state charts, the
499
507
  signaling protocol (`signaling/frames.json`,
500
508
  `signaling/anti_spoof_session.json`), and the distributed CRDT plane
501
509
  (`distributed/crdt_sync_frames.json`, `distributed/anti_entropy_converge.json`).
@@ -517,13 +525,18 @@ names the Lean theorems it mirrors:
517
525
  | `Collection` | `collection-properties.test.js` | `setEntryValue_preserves_{membership,order,siblings}`, `moveKey_preserves_{membership,values}`, `moveKey_advances_order`, `addKey_advances_membership_and_order`, `Family.get_idempotent_after_first` |
518
526
  | `Tree` | `tree-properties.test.js` | `setNodeValue_preserves_{other_nodes,node_signals}`, `moveChild_preserves_{non_parent,parent_value}`, `moveChild_advances_order_signal_only` |
519
527
  | `Materialization` | `reactive-family.test.js` | `observe_canonical`, `eager_lazy_observationally_equivalent`, `eager_materializes_all`, `lazy_defers_slots`, `materialize_present_monotone`, `lazy_present_subset_eager`, `materialize_preserves_observe`, `cell_entries_materialized_in_every_mode`, `slot_entries_deferred_under_lazy` |
528
+ | `Materialization` (thread-safe) | `thread-safe-reactive-family.test.js` | `materialize_present_comm`, `materialize_observe_comm` (materialization confluence) + the base materialization laws replayed through `ThreadSafeReactiveFamily` |
529
+ | `AsyncMaterialization` | `async-reactive-family.test.js` | eventual transparency (a driven async slot resolves to the canonical value; eager ≡ lazy) + present-set monotonicity through `AsyncReactiveFamily` |
530
+ | `ThreadSafe` | `thread-safe.test.js` | `flushBatch_empty`, `flushBatch_singleton_eq_setCell` (thread-safe batch refines `setCell`), `flushBatch_dependent_dirty`, `flushBatch_preserves_nondependent_dirty` |
520
531
  | `Reconciliation` | `reconciliation-properties.test.js` | `lisBy_longest`, `reconcile_move_minimized`, `reconcile_stable_not_invalidated` |
521
532
  | `AsyncSlotState` | `reactive-async.test.js` | `stale_completeOk_discarded`, `current_completeOk_publishes`, `current_completeErr_to_error` |
522
533
  | `AsyncEffect` | `reactive-async.test.js` | `fire_blocked_during_cleanup`, `invalidate_from_idle_schedules`, `cleanupDone_resumes_deferred`, `dispose_absorbing`, `disposed_terminal` |
523
534
 
524
- Thread-safe context theorems are not mirrored because JavaScript is
525
- single-threaded (one event loop): a lock-backed context has no meaning on this
526
- runtime the concurrency-layer platform carve-out. The `Signaling` /
535
+ The thread-safe context IS mirrored on this runtime: JavaScript is
536
+ single-threaded per realm but shares memory across Web Workers via
537
+ `SharedArrayBuffer` + `Atomics`, so `ThreadSafeContext` guards every operation
538
+ with a real reentrant Atomics mutex (cross-worker mutual exclusion, degrading to
539
+ a single-realm guard where shared memory is unavailable). The `Signaling` /
527
540
  `SignalingRoster` formal models are exercised through the `SignalingRoom`
528
541
  fixture replay (`signaling/anti_spoof_session.json`) rather than a named-theorem
529
542
  property test.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lazily-hub/lazily-js",
3
- "version": "0.10.0",
4
- "description": "Native JavaScript port of the lazily reactive core: a full reactive graph (Cell/Slot/Signal/Effect), the lazily-spec IPC wire types, keyed cell collections + LIS reconciliation, the memoized semantic tree, the move-aware sequence CRDT, the Fugue/RGA text CRDT, manufactured text identity, full-Harel state charts, and an FFI state-projection consumer.",
3
+ "version": "0.12.0",
4
+ "description": "Native JavaScript port of the lazily reactive core: a full reactive graph (Cell/Slot/Signal/Effect), sync/async/thread-safe keyed reactive families with materialization mode, the lazily-spec IPC wire types + C-ABI FFI boundary (isomorphic channel + Node native binding), keyed cell collections + LIS reconciliation, the memoized semantic tree, the move-aware sequence CRDT, the Fugue/RGA text CRDT, manufactured text identity, full-Harel state charts, an FFI state-projection consumer, and an in-library instrumentation/benchmark API.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -31,6 +31,28 @@
31
31
  "types": "./src/reactive-family.d.ts",
32
32
  "default": "./src/reactive-family.js"
33
33
  },
34
+ "./async-reactive-family": {
35
+ "types": "./src/async-reactive-family.d.ts",
36
+ "default": "./src/async-reactive-family.js"
37
+ },
38
+ "./thread-safe": {
39
+ "types": "./src/thread-safe.d.ts",
40
+ "default": "./src/thread-safe.js"
41
+ },
42
+ "./thread-safe-reactive-family": {
43
+ "types": "./src/thread-safe-reactive-family.d.ts",
44
+ "default": "./src/thread-safe-reactive-family.js"
45
+ },
46
+ "./ffi": {
47
+ "types": "./src/ffi.d.ts",
48
+ "node": "./src/ffi-node.js",
49
+ "browser": "./src/ffi.js",
50
+ "default": "./src/ffi.js"
51
+ },
52
+ "./instrumentation": {
53
+ "types": "./src/instrumentation.d.ts",
54
+ "default": "./src/instrumentation.js"
55
+ },
34
56
  "./state-machine": {
35
57
  "types": "./src/state-machine.d.ts",
36
58
  "default": "./src/state-machine.js"
@@ -81,7 +103,7 @@
81
103
  }
82
104
  },
83
105
  "scripts": {
84
- "build": "node --check src/index.js && node --check src/transport.js && node --check src/shm-backend.js && node --check src/reactive.js && node --check src/reactive-async.js && node --check src/reactive-family.js && node --check src/state-machine.js && node --check src/statechart.js && node --check src/collections.js && node --check src/queue.js && node --check src/sem-tree.js && node --check src/stable-id.js && node --check src/seq-crdt.js && node --check src/text-crdt.js && node --check src/utf8-offsets.js && node --check src/lossless-tree-crdt.js && node --check src/state-projection.js && node --check src/signaling.js && node --check src/distributed.js",
106
+ "build": "node --check src/index.js && node --check src/transport.js && node --check src/shm-backend.js && node --check src/reactive.js && node --check src/reactive-async.js && node --check src/reactive-family.js && node --check src/async-reactive-family.js && node --check src/thread-safe.js && node --check src/thread-safe-reactive-family.js && node --check src/ffi.js && node --check src/ffi-node.js && node --check src/instrumentation.js && node --check src/state-machine.js && node --check src/statechart.js && node --check src/collections.js && node --check src/queue.js && node --check src/sem-tree.js && node --check src/stable-id.js && node --check src/seq-crdt.js && node --check src/text-crdt.js && node --check src/utf8-offsets.js && node --check src/lossless-tree-crdt.js && node --check src/state-projection.js && node --check src/signaling.js && node --check src/distributed.js",
85
107
  "test:formal": "node scripts/formal-check.mjs",
86
108
  "bench": "node bench/context.bench.mjs",
87
109
  "bench:scale": "node --max-old-space-size=8192 bench/scale.bench.mjs",
@@ -0,0 +1,78 @@
1
+ import type { AsyncCellHandle, AsyncContext, AsyncSlotHandle } from "./reactive-async.js";
2
+ import type { EntryKind, MaterializationMode } from "./reactive-family.js";
3
+
4
+ export { EntryKind, MaterializationMode, DEFAULT_MATERIALIZATION_MODE } from "./reactive-family.js";
5
+
6
+ export type EntryKindResolver<K> = EntryKind | ((key: K) => EntryKind);
7
+ export type AsyncFamilyHandle = AsyncCellHandle | AsyncSlotHandle;
8
+
9
+ /**
10
+ * The async keyed reactive family (`#lzmatmode`, async flavor): keys map to
11
+ * per-entry async reactive nodes (input cells resolved synchronously, or derived
12
+ * slots resolved asynchronously), allocated per the family's
13
+ * {@link MaterializationMode}. The transparency law is EVENTUAL — a pending slot
14
+ * observes as `undefined` and resolves to the canonical value.
15
+ */
16
+ export class AsyncReactiveFamily<K = unknown, V = unknown> {
17
+ constructor(
18
+ ctx: AsyncContext,
19
+ mode: MaterializationMode,
20
+ keys: Iterable<K>,
21
+ factory: (key: K) => V,
22
+ entryKind?: EntryKindResolver<K>,
23
+ );
24
+
25
+ /** Build an eager async family: every declared key's node is allocated now. */
26
+ static eager<K, V>(
27
+ ctx: AsyncContext,
28
+ keys: Iterable<K>,
29
+ factory: (key: K) => V,
30
+ entryKind?: EntryKindResolver<K>,
31
+ ): AsyncReactiveFamily<K, V>;
32
+
33
+ /** Build a lazy async family: derived (slot) entries deferred to first read. */
34
+ static lazy<K, V>(
35
+ ctx: AsyncContext,
36
+ keys: Iterable<K>,
37
+ factory: (key: K) => V,
38
+ entryKind?: EntryKindResolver<K>,
39
+ ): AsyncReactiveFamily<K, V>;
40
+
41
+ /** Build an async family in the default (eager) mode. */
42
+ static create<K, V>(
43
+ ctx: AsyncContext,
44
+ keys: Iterable<K>,
45
+ factory: (key: K) => V,
46
+ entryKind?: EntryKindResolver<K>,
47
+ ): AsyncReactiveFamily<K, V>;
48
+
49
+ /** Materialize (lazy pull) and return the entry handle for `key`. */
50
+ get(key: K): AsyncFamilyHandle;
51
+ /** Non-blocking observe: value for a cell/resolved slot, `undefined` if pending. */
52
+ observe(key: K): V | undefined;
53
+ /** Drive `key` to resolution and return its canonical value. */
54
+ resolve(key: K): Promise<V>;
55
+ /** Set a cell entry's value (input entries only). */
56
+ setCell(key: K, value: V): void;
57
+ /** Whether `key` is currently materialized. Non-reactive. */
58
+ isPresent(key: K): boolean;
59
+ /** Currently-materialized keys, in first-materialization order. */
60
+ presentKeys(): K[];
61
+ /** Number of currently-materialized entries. */
62
+ presentCount(): number;
63
+ /** This family's entry kind for `key`. */
64
+ entryKind(key: K): EntryKind;
65
+ /** This family's materialization mode. */
66
+ readonly mode: MaterializationMode;
67
+ }
68
+
69
+ /**
70
+ * The input-cell specialization of {@link AsyncReactiveFamily}: a keyed async
71
+ * family whose entries are all input cells (always materialized, always resolved).
72
+ */
73
+ export function asyncCellFamily<K, V>(
74
+ ctx: AsyncContext,
75
+ keys: Iterable<K>,
76
+ factory: (key: K) => V,
77
+ mode?: MaterializationMode,
78
+ ): AsyncReactiveFamily<K, V>;
@@ -0,0 +1,249 @@
1
+ // The async keyed reactive family (`AsyncReactiveFamily`, `#lzmatmode` async
2
+ // flavor) — the {@link AsyncContext} analog of {@link ReactiveFamily}.
3
+ //
4
+ // Keys `K` map to per-entry async reactive nodes: {@link EntryKind.Cell} input
5
+ // cells ({@link AsyncCellHandle}, always resolved) or {@link EntryKind.Slot}
6
+ // derived slots ({@link AsyncSlotHandle}, resolved asynchronously), allocated per
7
+ // the family's {@link MaterializationMode}.
8
+ //
9
+ // The eager/lazy contract and present-set monotonicity are identical to the
10
+ // single-threaded family. The transparency law here is EVENTUAL: an async derived
11
+ // slot read is `undefined` while pending and resolves to the canonical value — so
12
+ // `observe` returns `V | undefined`. Input cells are always resolved. Drive a slot
13
+ // to resolution with {@link AsyncReactiveFamily#resolve} (or
14
+ // `ctx.getAsync(fam.get(key))`).
15
+ //
16
+ // To keep the sync/thread-safe/async families API-parallel the per-key factory is
17
+ // the same sync `(key) => V`; a derived slot wraps it in a ready async
18
+ // recomputation. Mirrors the async materialization case in lazily-spec and the
19
+ // `AsyncMaterialization` proofs (eventual transparency) in lazily-formal.
20
+ //
21
+ // Rust reference: `lazily-rs/src/async_reactive_family.rs`.
22
+
23
+ import { EntryKind, MaterializationMode, DEFAULT_MATERIALIZATION_MODE } from "./reactive-family.js";
24
+
25
+ export { EntryKind, MaterializationMode, DEFAULT_MATERIALIZATION_MODE };
26
+
27
+ function resolveKind(entryKind, key) {
28
+ const kind = typeof entryKind === "function" ? entryKind(key) : entryKind;
29
+ if (kind !== EntryKind.Cell && kind !== EntryKind.Slot) {
30
+ throw new TypeError(`entry kind for key ${String(key)} must be EntryKind.Cell or EntryKind.Slot`);
31
+ }
32
+ return kind;
33
+ }
34
+
35
+ /**
36
+ * The async unified keyed reactive family (`#lzmatmode`): keys map to per-entry
37
+ * async reactive nodes ({@link EntryKind.Cell} input cells resolved
38
+ * synchronously, or {@link EntryKind.Slot} derived slots resolved
39
+ * asynchronously), allocated per the family's {@link MaterializationMode}.
40
+ *
41
+ * Operations run against an owning `AsyncContext` (from `./reactive-async.js`).
42
+ *
43
+ * @template K, V
44
+ */
45
+ export class AsyncReactiveFamily {
46
+ /** @type {import("./reactive-async.js").AsyncContext} */
47
+ #ctx;
48
+ /** @type {string} */
49
+ #mode;
50
+ /** @type {(key: K) => V} */
51
+ #factory;
52
+ /** @type {EntryKind | ((key: K) => EntryKind)} */
53
+ #entryKind;
54
+ /** Present (materialized) entries: key -> { kind, handle }. */
55
+ #materialized = new Map();
56
+ /** First-materialization order of the present set. */
57
+ #order = [];
58
+
59
+ /**
60
+ * @param {import("./reactive-async.js").AsyncContext} ctx owning async context
61
+ * @param {string} mode {@link MaterializationMode}
62
+ * @param {Iterable<K>} keys declared keys
63
+ * @param {(key: K) => V} factory canonical per-key value producer
64
+ * @param {EntryKind | ((key: K) => EntryKind)} [entryKind] entry kind (or per-key resolver); defaults to Slot
65
+ */
66
+ constructor(ctx, mode, keys, factory, entryKind = EntryKind.Slot) {
67
+ if (mode !== MaterializationMode.Eager && mode !== MaterializationMode.Lazy) {
68
+ throw new TypeError("mode must be a MaterializationMode");
69
+ }
70
+ if (typeof factory !== "function") {
71
+ throw new TypeError("factory must be a function");
72
+ }
73
+ this.#ctx = ctx;
74
+ this.#mode = mode;
75
+ this.#factory = factory;
76
+ this.#entryKind = entryKind;
77
+
78
+ for (const key of keys) {
79
+ // A cell entry is always materialized regardless of mode; a slot entry
80
+ // only under eager.
81
+ if (resolveKind(entryKind, key) === EntryKind.Cell || mode === MaterializationMode.Eager) {
82
+ this.#materializeKey(key);
83
+ }
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Build an eager async family: every declared key's node is allocated now. The
89
+ * default mode.
90
+ * @template K, V
91
+ * @param {import("./reactive-async.js").AsyncContext} ctx
92
+ * @param {Iterable<K>} keys
93
+ * @param {(key: K) => V} factory
94
+ * @param {EntryKind | ((key: K) => EntryKind)} [entryKind]
95
+ * @returns {AsyncReactiveFamily<K, V>}
96
+ */
97
+ static eager(ctx, keys, factory, entryKind = EntryKind.Slot) {
98
+ return new AsyncReactiveFamily(ctx, MaterializationMode.Eager, keys, factory, entryKind);
99
+ }
100
+
101
+ /**
102
+ * Build a lazy async family: derived (slot) entries deferred to first read;
103
+ * input (cell) entries in `keys` are still materialized at build.
104
+ * @template K, V
105
+ * @param {import("./reactive-async.js").AsyncContext} ctx
106
+ * @param {Iterable<K>} keys
107
+ * @param {(key: K) => V} factory
108
+ * @param {EntryKind | ((key: K) => EntryKind)} [entryKind]
109
+ * @returns {AsyncReactiveFamily<K, V>}
110
+ */
111
+ static lazy(ctx, keys, factory, entryKind = EntryKind.Slot) {
112
+ return new AsyncReactiveFamily(ctx, MaterializationMode.Lazy, keys, factory, entryKind);
113
+ }
114
+
115
+ /**
116
+ * Build an async family in the default mode (eager). Alias for
117
+ * {@link AsyncReactiveFamily.eager}.
118
+ * @template K, V
119
+ * @param {import("./reactive-async.js").AsyncContext} ctx
120
+ * @param {Iterable<K>} keys
121
+ * @param {(key: K) => V} factory
122
+ * @param {EntryKind | ((key: K) => EntryKind)} [entryKind]
123
+ * @returns {AsyncReactiveFamily<K, V>}
124
+ */
125
+ static create(ctx, keys, factory, entryKind = EntryKind.Slot) {
126
+ return AsyncReactiveFamily.eager(ctx, keys, factory, entryKind);
127
+ }
128
+
129
+ #materializeKey(key) {
130
+ const existing = this.#materialized.get(key);
131
+ if (existing !== undefined) {
132
+ return existing; // warm: already allocated (first-writer-wins, stable handle).
133
+ }
134
+ const kind = resolveKind(this.#entryKind, key);
135
+ const factory = this.#factory;
136
+ // A cell entry sets its value directly (always resolved); a derived slot
137
+ // wraps the factory as a ready async recomputation — the same node an eager
138
+ // build would allocate.
139
+ const handle =
140
+ kind === EntryKind.Cell
141
+ ? this.#ctx.cell(factory(key))
142
+ : this.#ctx.computedAsync(async () => factory(key));
143
+ const entry = { kind, handle };
144
+ this.#materialized.set(key, entry);
145
+ this.#order.push(key);
146
+ return entry;
147
+ }
148
+
149
+ /**
150
+ * Get the entry handle for `key`, materializing it on first access (the lazy
151
+ * pull) and caching it. For a slot family this is the {@link AsyncSlotHandle}
152
+ * to drive with `ctx.getAsync` or {@link AsyncReactiveFamily#resolve}.
153
+ * @param {K} key
154
+ * @returns {import("./reactive-async.js").AsyncCellHandle | import("./reactive-async.js").AsyncSlotHandle}
155
+ */
156
+ get(key) {
157
+ return this.#materializeKey(key).handle;
158
+ }
159
+
160
+ /**
161
+ * Non-blocking observe: the resolved value for a cell or resolved slot, or
162
+ * `undefined` for a slot still pending. The EVENTUAL-transparency law: once
163
+ * resolved this equals the canonical value under either mode. Materializes the
164
+ * entry if absent.
165
+ * @param {K} key
166
+ * @returns {V | undefined}
167
+ */
168
+ observe(key) {
169
+ const { kind, handle } = this.#materializeKey(key);
170
+ return kind === EntryKind.Cell ? this.#ctx.getCell(handle) : this.#ctx.get(handle);
171
+ }
172
+
173
+ /**
174
+ * Drive `key` to resolution and return its canonical value. For a cell this is
175
+ * immediate; for a slot it awaits the async recomputation. Materializes the
176
+ * entry if absent.
177
+ * @param {K} key
178
+ * @returns {Promise<V>}
179
+ */
180
+ async resolve(key) {
181
+ const { kind, handle } = this.#materializeKey(key);
182
+ return kind === EntryKind.Cell ? this.#ctx.getCell(handle) : this.#ctx.getAsync(handle);
183
+ }
184
+
185
+ /**
186
+ * Set a cell entry's value (input entries only). Materializes it if absent.
187
+ * @param {K} key
188
+ * @param {V} value
189
+ */
190
+ setCell(key, value) {
191
+ const { kind, handle } = this.#materializeKey(key);
192
+ if (kind !== EntryKind.Cell) {
193
+ throw new TypeError(`key ${String(key)} is a derived slot, not a writable input cell`);
194
+ }
195
+ this.#ctx.setCell(handle, value);
196
+ }
197
+
198
+ /**
199
+ * Whether `key` is currently materialized (present). Non-reactive.
200
+ * @param {K} key
201
+ * @returns {boolean}
202
+ */
203
+ isPresent(key) {
204
+ return this.#materialized.has(key);
205
+ }
206
+
207
+ /**
208
+ * The currently-materialized keys, in first-materialization order. The present
209
+ * set only grows (deferral, not de-allocation).
210
+ * @returns {K[]}
211
+ */
212
+ presentKeys() {
213
+ return [...this.#order];
214
+ }
215
+
216
+ /** Number of currently-materialized entries. @returns {number} */
217
+ presentCount() {
218
+ return this.#order.length;
219
+ }
220
+
221
+ /**
222
+ * This family's entry kind for `key`.
223
+ * @param {K} key
224
+ * @returns {EntryKind}
225
+ */
226
+ entryKind(key) {
227
+ return resolveKind(this.#entryKind, key);
228
+ }
229
+
230
+ /** This family's materialization mode. @returns {MaterializationMode} */
231
+ get mode() {
232
+ return this.#mode;
233
+ }
234
+ }
235
+
236
+ /**
237
+ * The input-cell specialization of {@link AsyncReactiveFamily}: a keyed async
238
+ * family whose entries are all input cells ({@link EntryKind.Cell} — always
239
+ * materialized, always resolved).
240
+ * @template K, V
241
+ * @param {import("./reactive-async.js").AsyncContext} ctx
242
+ * @param {Iterable<K>} keys
243
+ * @param {(key: K) => V} factory
244
+ * @param {string} [mode] {@link MaterializationMode} (cell entries materialize under either)
245
+ * @returns {AsyncReactiveFamily<K, V>}
246
+ */
247
+ export function asyncCellFamily(ctx, keys, factory, mode = DEFAULT_MATERIALIZATION_MODE) {
248
+ return new AsyncReactiveFamily(ctx, mode, keys, factory, EntryKind.Cell);
249
+ }
@@ -0,0 +1,35 @@
1
+ import type { IpcMessage } from "./index.js";
2
+ import type { DecodeResult, LazilyFfiStatus, RecvBytesResult } from "./ffi.js";
3
+
4
+ export {
5
+ FfiChannel,
6
+ IpcMessage,
7
+ LazilyFfiMessageKind,
8
+ LazilyFfiStatus,
9
+ decodeMessage,
10
+ encodeMessage,
11
+ kindOf,
12
+ validateMessage,
13
+ } from "./ffi.js";
14
+ export type { EncodeResult, DecodeResult, RecvBytesResult } from "./ffi.js";
15
+
16
+ /** Whether koffi is installed so a native binding can be established. */
17
+ export function hasNativeBinding(): boolean;
18
+
19
+ /**
20
+ * A native C-ABI channel backed by a `lazily` shared library. Interface-parallel
21
+ * to the in-process `FfiChannel`.
22
+ */
23
+ export class NativeFfiChannel {
24
+ constructor(libPath?: string);
25
+ sendJson(payload: Uint8Array | string): LazilyFfiStatus;
26
+ send(message: IpcMessage): LazilyFfiStatus;
27
+ recvJson(): RecvBytesResult;
28
+ recv(): DecodeResult;
29
+ len(): number;
30
+ readonly isEmpty: boolean;
31
+ free(): void;
32
+ }
33
+
34
+ /** Open a native C-ABI channel to a `lazily` shared library. */
35
+ export function loadNativeChannel(libPath?: string): NativeFfiChannel;