@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 +23 -10
- package/package.json +25 -3
- package/src/async-reactive-family.d.ts +78 -0
- package/src/async-reactive-family.js +249 -0
- package/src/ffi-node.d.ts +35 -0
- package/src/ffi-node.js +197 -0
- package/src/ffi.d.ts +66 -0
- package/src/ffi.js +205 -0
- package/src/instrumentation.d.ts +28 -0
- package/src/instrumentation.js +194 -0
- package/src/reactive.d.ts +14 -0
- package/src/reactive.js +62 -0
- package/src/thread-safe-reactive-family.d.ts +65 -0
- package/src/thread-safe-reactive-family.js +185 -0
- package/src/thread-safe.d.ts +67 -0
- package/src/thread-safe.js +246 -0
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`
|
|
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
|
-
|
|
525
|
-
single-threaded
|
|
526
|
-
|
|
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.
|
|
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,
|
|
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;
|