@lazily-hub/lazily-js 0.6.1 → 0.9.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/.github/workflows/release.yml +16 -8
- package/README.md +117 -7
- package/package.json +19 -2
- package/src/index.d.ts +12 -0
- package/src/index.js +41 -2
- package/src/queue.d.ts +91 -0
- package/src/queue.js +354 -0
- package/src/reactive-family.d.ts +100 -0
- package/src/reactive-family.js +269 -0
- package/src/shm-backend.js +435 -0
- package/src/transport.d.ts +113 -0
- package/src/transport.js +491 -0
|
@@ -39,9 +39,13 @@ jobs:
|
|
|
39
39
|
node-version: 24
|
|
40
40
|
registry-url: https://registry.npmjs.org
|
|
41
41
|
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
# OIDC trusted publishing (provenance) needs npm >= 9.5.0; Node 24's
|
|
43
|
+
# bundled npm 11.x already supports it. Do NOT `npm install -g npm@latest`
|
|
44
|
+
# — that global self-upgrade drops the `sigstore` dependency that
|
|
45
|
+
# libnpmpublish requires for provenance, producing
|
|
46
|
+
# `Cannot find module 'sigstore'` at publish time.
|
|
47
|
+
- name: Verify npm supports OIDC provenance
|
|
48
|
+
run: npm --version
|
|
45
49
|
|
|
46
50
|
- name: Install dependencies
|
|
47
51
|
working-directory: lazily-js
|
|
@@ -51,15 +55,19 @@ jobs:
|
|
|
51
55
|
working-directory: lazily-js
|
|
52
56
|
run: npm test --if-present
|
|
53
57
|
|
|
54
|
-
#
|
|
55
|
-
# `
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
+
# Publish as `latest`. The accidental v1.x versions that forced the old
|
|
59
|
+
# `prev` dist-tag workaround have been removed (npm now shows only the
|
|
60
|
+
# 0.4.0 / 0.5.0 / 0.6.x line, latest = 0.6.1), so the v0.x line is the
|
|
61
|
+
# canonical latest again.
|
|
58
62
|
- name: Publish
|
|
59
63
|
working-directory: lazily-js
|
|
60
64
|
run: |
|
|
61
65
|
version="$(node -p "require('./package.json').version")"
|
|
62
|
-
|
|
66
|
+
# --provenance triggers the OIDC trusted-publishing exchange (the
|
|
67
|
+
# `id-token: write` permission minted a short-lived publish token).
|
|
68
|
+
# The bundled npm 11.x does not auto-enable provenance the way a
|
|
69
|
+
# freshly-upgraded npm@latest does, so it must be explicit.
|
|
70
|
+
if npm publish --provenance --access public; then
|
|
63
71
|
echo "published @lazily-hub/lazily-js@${version}"
|
|
64
72
|
elif npm view "@lazily-hub/lazily-js@${version}" version >/dev/null 2>&1; then
|
|
65
73
|
echo "@lazily-hub/lazily-js@${version} already published — idempotent no-op"
|
package/README.md
CHANGED
|
@@ -39,6 +39,7 @@ notes and platform carve-outs lives in
|
|
|
39
39
|
| Feature | Rust | Python | Kotlin | JS | Dart | Zig | Go | C++ |
|
|
40
40
|
| --------- | :----: | :------: | :------: | :--: | :----: | :---: | :--: | :---: |
|
|
41
41
|
| Reactive graph — `Cell` / `Slot` / `Signal` / `Effect` / memo / batch | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
42
|
+
| Reactive family (`ReactiveFamily`) — keyed cell/slot family + materialization mode (`#lzmatmode`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
42
43
|
| Thread-safe context (lock-backed) | ✅ | ✅ | ✅ | — | — | ✅ | ✅ | ✅ |
|
|
43
44
|
| Async reactive context | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
44
45
|
| Flat state machine | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
@@ -46,21 +47,22 @@ notes and platform carve-outs lives in
|
|
|
46
47
|
| Keyed cell collections (`CellMap` / `CellTree`) + reconcile | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
47
48
|
| Memoized semantic tree (`SemTree`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
48
49
|
| Stable-id alignment (manufactured identity) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
49
|
-
| Reactive queue (`QueueCell` SPSC/MPSC + `QueueStorage` adapter) |
|
|
50
|
+
| Reactive queue (`QueueCell` SPSC/MPSC + `QueueStorage` adapter) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
50
51
|
| Free-text character CRDT (`TextCrdt`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
51
52
|
| `TextCrdt` delta sync (`version_vector` / `delta_since` / `apply_delta`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
52
53
|
| Move-aware sequence CRDT (`SeqCrdt`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
53
|
-
| Lossless tree CRDT core (`LosslessTreeCrdt`, M1) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
54
|
-
| Lossless tree — dotted-frontier anti-entropy | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
55
|
-
| Lossless tree — concurrent merge convergence | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
54
|
+
| Lossless tree CRDT core (`LosslessTreeCrdt`, M1) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
55
|
+
| Lossless tree — dotted-frontier anti-entropy | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
56
|
+
| Lossless tree — concurrent merge convergence | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
56
57
|
| Registers (LWW / MV) + `PnCounter` + `CellCrdt` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
57
58
|
| IPC wire — `Snapshot` + `Delta` + `CrdtSync` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
58
|
-
| Shared-memory blob path (`ShmBlobArena`) | ✅ | ✅ | ✅ |
|
|
59
|
+
| Shared-memory blob path (`ShmBlobArena`) | ✅ | ✅ | ✅ | ✅ | ~ | ✅ | ✅ | ✅ |
|
|
60
|
+
| Cross-process zero-copy transport (`BlobBackend` / shm / arrow) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
59
61
|
| Distributed CRDT plane (`CrdtPlaneRuntime` / anti-entropy) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
60
62
|
| Distributed plane — WebRTC transport + signaling | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
61
63
|
| State projection / mirror | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
62
64
|
| Causal receipts (`CausalReceipts` outcome projection) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
63
|
-
| Message-passing + RPC command plane (`command-plane-v1`) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
65
|
+
| Message-passing + RPC command plane (`command-plane-v1`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
64
66
|
| C-ABI FFI boundary | ✅ | ✅ | ✅ | — | ✅ | ✅ | ✅ | ✅ |
|
|
65
67
|
| Permission boundary (`PeerPermissions` / `RemoteOp`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
66
68
|
| Capability negotiation (`SessionHandshake`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
@@ -74,8 +76,10 @@ and JSON Schemas in `lazily-spec` and the Lean models in `lazily-formal`.
|
|
|
74
76
|
| Import | What it is |
|
|
75
77
|
|--------|------------|
|
|
76
78
|
| `@lazily-hub/lazily-js` | `lazily-spec` IPC wire types: `Snapshot`, `Delta`, `DeltaOp`, `IpcMessage` (`Snapshot` / `Delta` / `CrdtSync`), `NodeState`, `IpcValue`, `PeerPermissions`, `SessionHandshake`, `BINDING_CAPABILITIES` |
|
|
79
|
+
| `@lazily-hub/lazily-js/transport` | Cross-process zero-copy transport (`#lzzcpy`): `ShmBlobArena`, `InProcessBackend` / `ArrowBackend`, `BlobRouter`, `spillMessage` / `resolveValue`, and the FFI-gated `createShmBackend` (Node/Bun/Deno). Isomorphic — no FFI import; browser-safe |
|
|
77
80
|
| `@lazily-hub/lazily-js/reactive` | Reactive dependency graph: `Context`, `Cell`, `Slot`, `Signal`, `Effect` |
|
|
78
81
|
| `@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 |
|
|
82
|
+
| `@lazily-hub/lazily-js/reactive-family` | Unified keyed reactive family: `ReactiveFamily` (`EntryKind` cell/slot × `MaterializationMode` eager/lazy) + `cellFamily` input-cell specialization (`#lzmatmode`) |
|
|
79
83
|
| `@lazily-hub/lazily-js/state-machine` | Flat finite-state-machine kernel backed by a reactive `Cell` |
|
|
80
84
|
| `@lazily-hub/lazily-js/statechart` | Harel/SCXML chart interpreter plus `ChartBuilder`, `StateBuilder`, `TransitionBuilder` |
|
|
81
85
|
| `@lazily-hub/lazily-js/collections` | `CellMap`, `CellTree`, keyed reconciliation, and LIS move minimization |
|
|
@@ -155,6 +159,44 @@ ctx.setCell(userId, 2); // supersedes any in-flight compute; slot re-resolves
|
|
|
155
159
|
await ctx.getAsync(profile); // the profile for user 2
|
|
156
160
|
```
|
|
157
161
|
|
|
162
|
+
## Reactive family and materialization mode
|
|
163
|
+
|
|
164
|
+
`ReactiveFamily` (from `@lazily-hub/lazily-js/reactive-family`) is the unified
|
|
165
|
+
**keyed reactive family** (`#lzmatmode`): it maps keys to per-entry reactive
|
|
166
|
+
nodes and abstracts over the entry's handle kind. Two orthogonal axes:
|
|
167
|
+
|
|
168
|
+
- **Entry kind** — `EntryKind.Cell` entries are input cells (**always
|
|
169
|
+
materialized**, any mode); `EntryKind.Slot` entries are derived slots (what
|
|
170
|
+
materialization governs). `cellFamily(...)` is the input-cell specialization.
|
|
171
|
+
- **Materialization mode** — `MaterializationMode.Eager` (**default**) allocates
|
|
172
|
+
every derived node up front; `MaterializationMode.Lazy` (opt-in) allocates a
|
|
173
|
+
derived node on its **first read** ("materialize on pull") and caches it.
|
|
174
|
+
|
|
175
|
+
Mode is **observationally transparent**: a read returns the same value under
|
|
176
|
+
either mode — it changes allocation timing and memory, never results. Lazy pays
|
|
177
|
+
off only for sparsely-touched large keyed address spaces.
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
import { Context } from "@lazily-hub/lazily-js/reactive";
|
|
181
|
+
import { ReactiveFamily } from "@lazily-hub/lazily-js/reactive-family";
|
|
182
|
+
|
|
183
|
+
const ctx = new Context();
|
|
184
|
+
|
|
185
|
+
// A derived (slot) family of key*3 over a large address space, built lazily:
|
|
186
|
+
// nothing is allocated until a key is read.
|
|
187
|
+
const fam = ReactiveFamily.lazy(ctx, range(0, 1_000_000), (k) => k * 3);
|
|
188
|
+
fam.presentCount(); // 0
|
|
189
|
+
|
|
190
|
+
fam.observe(5); // 15 — first read materializes just this entry
|
|
191
|
+
fam.presentCount(); // 1
|
|
192
|
+
fam.isPresent(5); // true
|
|
193
|
+
fam.isPresent(6); // false
|
|
194
|
+
|
|
195
|
+
// Eager builds the same values up front — observationally identical.
|
|
196
|
+
const eager = ReactiveFamily.eager(ctx, [0, 1, 2, 3], (k) => k * 3);
|
|
197
|
+
eager.observe(2) === fam.observe(2); // true
|
|
198
|
+
```
|
|
199
|
+
|
|
158
200
|
## State machine and state charts
|
|
159
201
|
|
|
160
202
|
`StateMachine` is the flat finite-state-machine kernel: a pure
|
|
@@ -344,6 +386,69 @@ signaling, and the WebRTC transport are shipped; C-ABI FFI is `none` because
|
|
|
344
386
|
browser/Worker JS cannot host a native in-process ABI. The same payload types
|
|
345
387
|
can still be carried by any transport a host application owns.
|
|
346
388
|
|
|
389
|
+
## Cross-process zero-copy transport
|
|
390
|
+
|
|
391
|
+
`@lazily-hub/lazily-js/transport` implements the pluggable blob-backend
|
|
392
|
+
transport (`#lzzcpy`). A large payload is not copied through the wire codec: the
|
|
393
|
+
producer **spills** it to a backend (which mints a `ShmBlobRef` descriptor) and
|
|
394
|
+
ships only the descriptor; the receiver **routes** the descriptor by its
|
|
395
|
+
`backend` discriminator and **resolves** it zero-copy — reading the backend's own
|
|
396
|
+
bytes in place. `ShmBlobRef` gained an optional `backend` field (`shm` | `arrow`
|
|
397
|
+
| `in_process`) that defaults to `shm` and is omitted from the wire, so every
|
|
398
|
+
pre-transport descriptor round-trips byte-for-byte.
|
|
399
|
+
|
|
400
|
+
The module is **isomorphic**: it imports no FFI, so it bundles and runs in the
|
|
401
|
+
browser. `InProcessBackend`, `ArrowBackend`, the `BlobRouter`, and the
|
|
402
|
+
spill/resolve policy are pure JS and available everywhere (including a
|
|
403
|
+
main-thread ↔ Web Worker deployment).
|
|
404
|
+
|
|
405
|
+
```js
|
|
406
|
+
import {
|
|
407
|
+
BlobRouter,
|
|
408
|
+
InProcessBackend,
|
|
409
|
+
ArrowBackend,
|
|
410
|
+
spillMessage,
|
|
411
|
+
} from "@lazily-hub/lazily-js/transport";
|
|
412
|
+
import { Delta, DeltaOp, IpcMessage, IpcValue } from "@lazily-hub/lazily-js";
|
|
413
|
+
|
|
414
|
+
const backend = new InProcessBackend(); // or ArrowBackend for columnar payloads
|
|
415
|
+
const big = IpcValue.inline(new Uint8Array(4096));
|
|
416
|
+
const { message, spilledBytes } = spillMessage(
|
|
417
|
+
IpcMessage.delta(new Delta({ baseEpoch: 0, epoch: 1, ops: [DeltaOp.slotValue(7, big)] })),
|
|
418
|
+
backend,
|
|
419
|
+
); // message now carries a small SharedBlob descriptor; spilledBytes === 4096
|
|
420
|
+
|
|
421
|
+
const router = new BlobRouter().register(backend);
|
|
422
|
+
router.resolve(message.delta.ops[0].payload); // Uint8Array view, zero copy
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
The genuine **cross-process** `shm` backend (POSIX `shm_open` + `mmap`) is loaded
|
|
426
|
+
lazily and only where a runtime provides FFI — Node (via `koffi`), Bun (`bun:ffi`),
|
|
427
|
+
or Deno (`Deno.dlopen`, needs `--allow-ffi --unstable-ffi`). A peer process on the
|
|
428
|
+
same host that attaches the same name resolves the descriptor without copying
|
|
429
|
+
across the process boundary. In the browser (or any runtime without FFI)
|
|
430
|
+
`createShmBackend` rejects with `ShmUnavailableError`; guard with `shmSupported()`
|
|
431
|
+
and fall back to `InProcessBackend` / `ArrowBackend`.
|
|
432
|
+
|
|
433
|
+
```js
|
|
434
|
+
import { createShmBackend, shmSupported } from "@lazily-hub/lazily-js/transport";
|
|
435
|
+
|
|
436
|
+
if (shmSupported()) {
|
|
437
|
+
const shm = await createShmBackend("my-session", { capacity: 1 << 20 });
|
|
438
|
+
const ref = shm.write(new Uint8Array([1, 2, 3])); // ref.backend === "shm"
|
|
439
|
+
// ...ship `ref` to a peer; the peer attaches `createShmBackend("my-session",
|
|
440
|
+
// { capacity: 1 << 20, create: false })` and calls `shm.readView(ref)`.
|
|
441
|
+
shm.close();
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
All three runtimes are verified end-to-end, including cross-runtime interop: a
|
|
446
|
+
Node process writes a region that a separate Deno process attaches and resolves,
|
|
447
|
+
proving the layout is byte-identical across FFI implementations. The `shm` region
|
|
448
|
+
is a bump-allocated arena with a fixed header (magic / version / capacity / epoch
|
|
449
|
+
/ generation / cursor) and per-entry `{ generation, epoch, len, checksum }`
|
|
450
|
+
validation.
|
|
451
|
+
|
|
347
452
|
## Distributed plane
|
|
348
453
|
|
|
349
454
|
The `@lazily-hub/lazily-js/signaling` and `@lazily-hub/lazily-js/distributed`
|
|
@@ -384,7 +489,11 @@ lazily-js replays the shared `lazily-spec` fixtures for IPC, agent-doc state,
|
|
|
384
489
|
keyed collections (`CellMap`, `CellTree`, LIS reconciliation), semantic tree,
|
|
385
490
|
sequence and text CRDTs (incl. `TextCrdt` delta sync, `#lztextsync`:
|
|
386
491
|
`textcrdt_convergence.json` + `textcrdt_delta_sync.json`), manufactured text
|
|
387
|
-
identity,
|
|
492
|
+
identity, the reactive family / materialization mode (`#lzmatmode`:
|
|
493
|
+
`materialization/observational_transparency.json`,
|
|
494
|
+
`materialization/deferral_not_deallocation.json`,
|
|
495
|
+
`materialization/entry_kind_orthogonal_to_mode.json`), Harel state charts, the
|
|
496
|
+
signaling protocol (`signaling/frames.json`,
|
|
388
497
|
`signaling/anti_spoof_session.json`), and the distributed CRDT plane
|
|
389
498
|
(`distributed/crdt_sync_frames.json`, `distributed/anti_entropy_converge.json`).
|
|
390
499
|
It also validates generated wire values against the canonical JSON Schemas.
|
|
@@ -404,6 +513,7 @@ names the Lean theorems it mirrors:
|
|
|
404
513
|
| `Reactive` | `reactive-properties.test.js` | `setCell_equal_preserves_graph`, `setCell_different_invalidates_dependents`, `recomputeSlot_equal_preserves_dependents`, `recomputeSlot_different_invalidates_dependents`, `signal_materialized_after_recompute` |
|
|
405
514
|
| `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` |
|
|
406
515
|
| `Tree` | `tree-properties.test.js` | `setNodeValue_preserves_{other_nodes,node_signals}`, `moveChild_preserves_{non_parent,parent_value}`, `moveChild_advances_order_signal_only` |
|
|
516
|
+
| `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` |
|
|
407
517
|
| `Reconciliation` | `reconciliation-properties.test.js` | `lisBy_longest`, `reconcile_move_minimized`, `reconcile_stable_not_invalidated` |
|
|
408
518
|
| `AsyncSlotState` | `reactive-async.test.js` | `stale_completeOk_discarded`, `current_completeOk_publishes`, `current_completeErr_to_error` |
|
|
409
519
|
| `AsyncEffect` | `reactive-async.test.js` | `fire_blocked_during_cleanup`, `invalidate_from_idle_schedules`, `cleanupDone_resumes_deferred`, `dispose_absorbing`, `disposed_terminal` |
|
package/package.json
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazily-hub/lazily-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
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.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/lazily-hub/lazily-js.git"
|
|
9
|
+
},
|
|
5
10
|
"type": "module",
|
|
6
11
|
"main": "src/index.js",
|
|
7
12
|
"types": "src/index.d.ts",
|
|
@@ -10,6 +15,10 @@
|
|
|
10
15
|
"types": "./src/index.d.ts",
|
|
11
16
|
"default": "./src/index.js"
|
|
12
17
|
},
|
|
18
|
+
"./transport": {
|
|
19
|
+
"types": "./src/transport.d.ts",
|
|
20
|
+
"default": "./src/transport.js"
|
|
21
|
+
},
|
|
13
22
|
"./reactive": {
|
|
14
23
|
"types": "./src/reactive.d.ts",
|
|
15
24
|
"default": "./src/reactive.js"
|
|
@@ -18,6 +27,10 @@
|
|
|
18
27
|
"types": "./src/reactive-async.d.ts",
|
|
19
28
|
"default": "./src/reactive-async.js"
|
|
20
29
|
},
|
|
30
|
+
"./reactive-family": {
|
|
31
|
+
"types": "./src/reactive-family.d.ts",
|
|
32
|
+
"default": "./src/reactive-family.js"
|
|
33
|
+
},
|
|
21
34
|
"./state-machine": {
|
|
22
35
|
"types": "./src/state-machine.d.ts",
|
|
23
36
|
"default": "./src/state-machine.js"
|
|
@@ -30,6 +43,10 @@
|
|
|
30
43
|
"types": "./src/collections.d.ts",
|
|
31
44
|
"default": "./src/collections.js"
|
|
32
45
|
},
|
|
46
|
+
"./queue": {
|
|
47
|
+
"types": "./src/queue.d.ts",
|
|
48
|
+
"default": "./src/queue.js"
|
|
49
|
+
},
|
|
33
50
|
"./sem-tree": {
|
|
34
51
|
"types": "./src/sem-tree.d.ts",
|
|
35
52
|
"default": "./src/sem-tree.js"
|
|
@@ -64,7 +81,7 @@
|
|
|
64
81
|
}
|
|
65
82
|
},
|
|
66
83
|
"scripts": {
|
|
67
|
-
"build": "node --check src/index.js && node --check src/reactive.js && node --check src/reactive-async.js && node --check src/state-machine.js && node --check src/statechart.js && node --check src/collections.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",
|
|
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",
|
|
68
85
|
"test:formal": "node scripts/formal-check.mjs",
|
|
69
86
|
"bench": "node bench/context.bench.mjs",
|
|
70
87
|
"benchmark-update": "node scripts/run-benchmarks.mjs",
|
package/src/index.d.ts
CHANGED
|
@@ -2,6 +2,14 @@ export type NodeId = number;
|
|
|
2
2
|
export type PeerId = number;
|
|
3
3
|
export type WireBytes = readonly number[] | Uint8Array;
|
|
4
4
|
|
|
5
|
+
export type BlobBackendKindValue = "shm" | "arrow" | "in_process";
|
|
6
|
+
|
|
7
|
+
export const BlobBackendKind: {
|
|
8
|
+
readonly Shm: "shm";
|
|
9
|
+
readonly Arrow: "arrow";
|
|
10
|
+
readonly InProcess: "in_process";
|
|
11
|
+
};
|
|
12
|
+
|
|
5
13
|
export class ShmBlobRef {
|
|
6
14
|
constructor(fields: {
|
|
7
15
|
offset: number;
|
|
@@ -9,12 +17,15 @@ export class ShmBlobRef {
|
|
|
9
17
|
generation: number;
|
|
10
18
|
epoch: number;
|
|
11
19
|
checksum: number;
|
|
20
|
+
backend?: BlobBackendKindValue;
|
|
12
21
|
});
|
|
13
22
|
readonly offset: number;
|
|
14
23
|
readonly len: number;
|
|
15
24
|
readonly generation: number;
|
|
16
25
|
readonly epoch: number;
|
|
17
26
|
readonly checksum: number;
|
|
27
|
+
readonly backend: BlobBackendKindValue;
|
|
28
|
+
withBackend(backend: BlobBackendKindValue): ShmBlobRef;
|
|
18
29
|
toWire(): ShmBlobRefWire;
|
|
19
30
|
static fromWire(value: ShmBlobRefWire): ShmBlobRef;
|
|
20
31
|
}
|
|
@@ -25,6 +36,7 @@ export type ShmBlobRefWire = {
|
|
|
25
36
|
generation: number;
|
|
26
37
|
epoch: number;
|
|
27
38
|
checksum: number;
|
|
39
|
+
backend?: BlobBackendKindValue;
|
|
28
40
|
};
|
|
29
41
|
|
|
30
42
|
export class NodeStatePayload {
|
package/src/index.js
CHANGED
|
@@ -73,24 +73,62 @@ function normalizeNodeKey(key) {
|
|
|
73
73
|
return key;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// The pluggable blob backends a descriptor may name (zero-copy transport,
|
|
77
|
+
// `#lzzcpy`). `shm` is the default and is omitted from the wire so legacy
|
|
78
|
+
// descriptors round-trip byte-for-byte. See src/transport.js and
|
|
79
|
+
// lazily-spec/docs/zero-copy-transport.md.
|
|
80
|
+
export const BlobBackendKind = Object.freeze({
|
|
81
|
+
Shm: "shm",
|
|
82
|
+
Arrow: "arrow",
|
|
83
|
+
InProcess: "in_process",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const BLOB_BACKEND_KINDS = new Set(Object.values(BlobBackendKind));
|
|
87
|
+
|
|
76
88
|
export class ShmBlobRef {
|
|
77
|
-
constructor({ offset, len, generation, epoch, checksum }) {
|
|
89
|
+
constructor({ offset, len, generation, epoch, checksum, backend }) {
|
|
78
90
|
this.offset = assertInteger(offset, "offset");
|
|
79
91
|
this.len = assertInteger(len, "len");
|
|
80
92
|
this.generation = assertInteger(generation, "generation");
|
|
81
93
|
this.epoch = assertInteger(epoch, "epoch");
|
|
82
94
|
this.checksum = assertInteger(checksum, "checksum");
|
|
95
|
+
// Optional pluggable-backend discriminator. Defaults to `shm` so every
|
|
96
|
+
// pre-transport descriptor keeps meaning "POSIX shared memory".
|
|
97
|
+
const kind = backend ?? BlobBackendKind.Shm;
|
|
98
|
+
if (!BLOB_BACKEND_KINDS.has(kind)) {
|
|
99
|
+
throw new TypeError(`unknown blob backend: ${kind}`);
|
|
100
|
+
}
|
|
101
|
+
this.backend = kind;
|
|
83
102
|
Object.freeze(this);
|
|
84
103
|
}
|
|
85
104
|
|
|
105
|
+
// Return a copy of this descriptor tagged for `backend`. Backends stamp their
|
|
106
|
+
// own kind onto an arena-minted descriptor via this helper.
|
|
107
|
+
withBackend(backend) {
|
|
108
|
+
return new ShmBlobRef({
|
|
109
|
+
offset: this.offset,
|
|
110
|
+
len: this.len,
|
|
111
|
+
generation: this.generation,
|
|
112
|
+
epoch: this.epoch,
|
|
113
|
+
checksum: this.checksum,
|
|
114
|
+
backend,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
86
118
|
toWire() {
|
|
87
|
-
|
|
119
|
+
const wire = {
|
|
88
120
|
offset: this.offset,
|
|
89
121
|
len: this.len,
|
|
90
122
|
generation: this.generation,
|
|
91
123
|
epoch: this.epoch,
|
|
92
124
|
checksum: this.checksum,
|
|
93
125
|
};
|
|
126
|
+
// Omit the default `shm` so the descriptor stays byte-compatible with the
|
|
127
|
+
// pre-`#lzzcpy` wire form (the `backend`-absent conformance fixture).
|
|
128
|
+
if (this.backend !== BlobBackendKind.Shm) {
|
|
129
|
+
wire.backend = this.backend;
|
|
130
|
+
}
|
|
131
|
+
return wire;
|
|
94
132
|
}
|
|
95
133
|
|
|
96
134
|
static fromWire(value) {
|
|
@@ -101,6 +139,7 @@ export class ShmBlobRef {
|
|
|
101
139
|
generation: object.generation,
|
|
102
140
|
epoch: object.epoch,
|
|
103
141
|
checksum: object.checksum,
|
|
142
|
+
backend: object.backend ?? BlobBackendKind.Shm,
|
|
104
143
|
});
|
|
105
144
|
}
|
|
106
145
|
}
|
package/src/queue.d.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Reactive queue: QueueCell + pluggable QueueStorage backend (#lzqueue).
|
|
2
|
+
// Pure logic — no reactive graph. See queue.js for the reader-kind invalidation
|
|
3
|
+
// contract and the shell / storage split.
|
|
4
|
+
|
|
5
|
+
export type QueuePushErrorLabel = "Full" | "Closed";
|
|
6
|
+
export type QueuePopErrorLabel = "Empty" | "Closed";
|
|
7
|
+
|
|
8
|
+
export const QueuePushError: Readonly<{ Full: "Full"; Closed: "Closed" }>;
|
|
9
|
+
export const QueuePopError: Readonly<{ Empty: "Empty"; Closed: "Closed" }>;
|
|
10
|
+
|
|
11
|
+
/** The reader-kind invalidation matrix returned by every mutating op. */
|
|
12
|
+
export type QueueInvalidates = {
|
|
13
|
+
head: boolean;
|
|
14
|
+
len: boolean;
|
|
15
|
+
is_empty: boolean;
|
|
16
|
+
is_full: boolean;
|
|
17
|
+
closed: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Result of a push op (`returns` is the error label, or `null` on success). */
|
|
21
|
+
export type QueuePushResult = {
|
|
22
|
+
returns: null | QueuePushErrorLabel;
|
|
23
|
+
invalidates: QueueInvalidates;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Result of a pop op (`returns` is the element, or the error label). */
|
|
27
|
+
export type QueuePopResult = {
|
|
28
|
+
returns: unknown | QueuePopErrorLabel;
|
|
29
|
+
invalidates: QueueInvalidates;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Result of a close op. */
|
|
33
|
+
export type QueueCloseResult = {
|
|
34
|
+
returns: null;
|
|
35
|
+
invalidates: QueueInvalidates;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** A duck-typed `QueueStorage` backend. */
|
|
39
|
+
export type QueueStorage = {
|
|
40
|
+
tryPush(value: unknown): null | QueuePushErrorLabel;
|
|
41
|
+
tryPop(): unknown | QueuePopErrorLabel;
|
|
42
|
+
peek(): unknown;
|
|
43
|
+
len(): number;
|
|
44
|
+
capacity(): number | null;
|
|
45
|
+
isClosed(): boolean;
|
|
46
|
+
close(): void;
|
|
47
|
+
snapshot(): QueueStorageSnapshot;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type QueueStorageSnapshot = {
|
|
51
|
+
elements: unknown[];
|
|
52
|
+
capacity: number | null;
|
|
53
|
+
closed: boolean;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type QueueInitial = {
|
|
57
|
+
elements?: unknown[];
|
|
58
|
+
capacity?: number | null;
|
|
59
|
+
closed?: boolean;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** The reference `QueueStorage` backend (unbounded or bounded array FIFO). */
|
|
63
|
+
export class VecDequeStorage {
|
|
64
|
+
constructor(initial?: QueueInitial);
|
|
65
|
+
elements: unknown[];
|
|
66
|
+
static from(initial?: QueueInitial): VecDequeStorage;
|
|
67
|
+
tryPush(value: unknown): null | QueuePushErrorLabel;
|
|
68
|
+
tryPop(): unknown | QueuePopErrorLabel;
|
|
69
|
+
peek(): unknown;
|
|
70
|
+
len(): number;
|
|
71
|
+
capacity(): number | null;
|
|
72
|
+
isClosed(): boolean;
|
|
73
|
+
close(): void;
|
|
74
|
+
snapshot(): QueueStorageSnapshot;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** A reactive FIFO queue — SPSC primitive with an MPSC usage rule. */
|
|
78
|
+
export class QueueCell {
|
|
79
|
+
constructor(initial?: QueueInitial, storage?: QueueStorage);
|
|
80
|
+
static from(initial?: QueueInitial, storage?: QueueStorage): QueueCell;
|
|
81
|
+
tryPush(value: unknown): QueuePushResult;
|
|
82
|
+
tryPop(): QueuePopResult;
|
|
83
|
+
close(): QueueCloseResult;
|
|
84
|
+
head(): unknown;
|
|
85
|
+
len(): number;
|
|
86
|
+
isEmpty(): boolean;
|
|
87
|
+
isFull(): boolean;
|
|
88
|
+
isClosed(): boolean;
|
|
89
|
+
capacity(): number | null;
|
|
90
|
+
elements(): unknown[];
|
|
91
|
+
}
|