@lazily-hub/lazily-js 0.4.0 → 0.5.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 +22 -1
- package/README.md +123 -30
- package/package.json +14 -2
- package/src/distributed.d.ts +136 -0
- package/src/distributed.js +647 -0
- package/src/index.js +9 -5
- package/src/reactive-async.d.ts +80 -0
- package/src/reactive-async.js +625 -0
- package/src/signaling.d.ts +320 -0
- package/src/signaling.js +725 -0
|
@@ -24,6 +24,15 @@ jobs:
|
|
|
24
24
|
|
|
25
25
|
steps:
|
|
26
26
|
- uses: actions/checkout@v4
|
|
27
|
+
with:
|
|
28
|
+
path: lazily-js
|
|
29
|
+
|
|
30
|
+
# The conformance / schema tests replay fixtures from the lazily-spec sibling
|
|
31
|
+
# (../lazily-spec). Check it out alongside so `npm test` passes in CI.
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
with:
|
|
34
|
+
repository: lazily-hub/lazily-spec
|
|
35
|
+
path: lazily-spec
|
|
27
36
|
|
|
28
37
|
- uses: actions/setup-node@v4
|
|
29
38
|
with:
|
|
@@ -35,9 +44,11 @@ jobs:
|
|
|
35
44
|
run: npm install -g npm@latest
|
|
36
45
|
|
|
37
46
|
- name: Install dependencies
|
|
47
|
+
working-directory: lazily-js
|
|
38
48
|
run: npm ci || npm install
|
|
39
49
|
|
|
40
50
|
- name: Test
|
|
51
|
+
working-directory: lazily-js
|
|
41
52
|
run: npm test --if-present
|
|
42
53
|
|
|
43
54
|
# Published under the `prev` dist-tag so this v0.x line does not steal
|
|
@@ -45,4 +56,14 @@ jobs:
|
|
|
45
56
|
# (`npm dist-tag add @lazily-hub/lazily-js@<v> latest`) once the accidental
|
|
46
57
|
# v1.x releases are removed.
|
|
47
58
|
- name: Publish
|
|
48
|
-
|
|
59
|
+
working-directory: lazily-js
|
|
60
|
+
run: |
|
|
61
|
+
version="$(node -p "require('./package.json').version")"
|
|
62
|
+
if npm publish --access public --tag prev; then
|
|
63
|
+
echo "published @lazily-hub/lazily-js@${version}"
|
|
64
|
+
elif npm view "@lazily-hub/lazily-js@${version}" version >/dev/null 2>&1; then
|
|
65
|
+
echo "@lazily-hub/lazily-js@${version} already published — idempotent no-op"
|
|
66
|
+
else
|
|
67
|
+
echo "publish failed for a reason other than already-published" >&2
|
|
68
|
+
exit 1
|
|
69
|
+
fi
|
package/README.md
CHANGED
|
@@ -6,6 +6,9 @@ is the JavaScript/TypeScript binding alongside [`lazily-rs`][rs],
|
|
|
6
6
|
[`lazily-dart`][dart]. It ships:
|
|
7
7
|
|
|
8
8
|
- a full reactive graph (`Context`, `Cell`, `Slot`, `Signal`, `Effect`);
|
|
9
|
+
- an async reactive graph (`AsyncContext`) for Promise-driven derivations, with
|
|
10
|
+
revision-guarded stale-completion discard, in-flight deduplication, and
|
|
11
|
+
cancellation;
|
|
9
12
|
- the [`lazily-spec`][spec] IPC wire types (`Snapshot`, `Delta`, `CrdtSync`,
|
|
10
13
|
capability negotiation, and default-deny peer permissions);
|
|
11
14
|
- keyed cell collections, move-minimized reconciliation, and a memoized
|
|
@@ -13,6 +16,9 @@ is the JavaScript/TypeScript binding alongside [`lazily-rs`][rs],
|
|
|
13
16
|
- move-aware sequence CRDT, Fugue/RGA text CRDT, and manufactured text identity;
|
|
14
17
|
- a Cell-backed flat state machine plus a full Harel/SCXML state-chart
|
|
15
18
|
interpreter with the typed `ChartBuilder` API;
|
|
19
|
+
- the distributed plane — the WebSocket signaling protocol + client, a
|
|
20
|
+
`DataChannel` transport seam with permission-filtering sink/source, a browser
|
|
21
|
+
`RTCPeerConnection` adapter, and the `CrdtPlaneRuntime` anti-entropy engine;
|
|
16
22
|
- a koffi-backed state-projection consumer for agent-doc host projections.
|
|
17
23
|
|
|
18
24
|
> **Package note.** Earlier `@lazily-hub/js` releases were only a state-projection
|
|
@@ -24,26 +30,34 @@ the FFI projection transport is used.
|
|
|
24
30
|
|
|
25
31
|
## Feature Set
|
|
26
32
|
|
|
27
|
-
The full `lazily` capability set
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
|
33
|
-
|
|
34
|
-
|
|
|
35
|
-
|
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
|
|
|
40
|
-
|
|
|
41
|
-
|
|
|
42
|
-
|
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
|
33
|
+
The full `lazily` capability set across every binding. Legend: ✅ shipped ·
|
|
34
|
+
`~` partial · `—` absent or not applicable. The canonical matrix with per-cell
|
|
35
|
+
notes and platform carve-outs lives in
|
|
36
|
+
[`lazily-spec` § Cross-Language Coverage](../lazily-spec/docs/coverage.md).
|
|
37
|
+
|
|
38
|
+
| Feature | Rust | Python | Kotlin | JS | Dart | Zig |
|
|
39
|
+
|---------|:----:|:------:|:------:|:--:|:----:|:---:|
|
|
40
|
+
| Reactive graph — `Cell` / `Slot` / `Signal` / `Effect` / memo / batch | ✅ | ~ | ✅ | ✅ | ~ | ~ |
|
|
41
|
+
| Thread-safe context (lock-backed) | ✅ | ✅ | ✅ | — | — | ✅ |
|
|
42
|
+
| Async reactive context | ✅ | ✅ | ✅ | ✅ | ✅ | — |
|
|
43
|
+
| Flat state machine | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
44
|
+
| Harel state charts | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
45
|
+
| Keyed cell collections (`CellMap` / `CellTree`) + reconcile | ✅ | ✅ | ✅ | ✅ | ✅ | ~ |
|
|
46
|
+
| Memoized semantic tree (`SemTree`) | ✅ | — | ✅ | ✅ | — | — |
|
|
47
|
+
| Stable-id alignment (manufactured identity) | ✅ | — | ✅ | ✅ | — | — |
|
|
48
|
+
| Free-text character CRDT (`TextCrdt`) | ✅ | — | ✅ | ✅ | — | — |
|
|
49
|
+
| `TextCrdt` delta sync (`version_vector` / `delta_since` / `apply_delta`) | ✅ | — | ✅ | ✅ | — | — |
|
|
50
|
+
| Move-aware sequence CRDT (`SeqCrdt`) | ✅ | — | ✅ | ✅ | — | — |
|
|
51
|
+
| Registers (LWW / MV) + `PnCounter` + `CellCrdt` | ✅ | — | ✅ | ✅ | — | — |
|
|
52
|
+
| IPC wire — `Snapshot` + `Delta` + `CrdtSync` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
53
|
+
| Shared-memory blob path (`ShmBlobArena`) | ✅ | ✅ | ✅ | ~ | ~ | ✅ |
|
|
54
|
+
| Distributed CRDT plane (`CrdtPlaneRuntime` / anti-entropy) | ✅ | — | ✅ | ✅ | ~ | — |
|
|
55
|
+
| Distributed plane — WebRTC transport + signaling | ✅ | — | ✅ | ✅ | — | — |
|
|
56
|
+
| State projection / mirror | ✅ | — | ✅ | ✅ | — | — |
|
|
57
|
+
| C-ABI FFI boundary | ✅ | ✅ | ✅ | — | ✅ | ✅ |
|
|
58
|
+
| Permission boundary (`PeerPermissions` / `RemoteOp`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
59
|
+
| Capability negotiation (`SessionHandshake`) | ✅ | — | ✅ | ✅ | ✅ | ✅ |
|
|
60
|
+
| Instrumentation / benchmarks | ✅ | — | — | — | — | — |
|
|
47
61
|
|
|
48
62
|
CRDT convergence and the wire protocol are pinned by the shared conformance fixtures
|
|
49
63
|
and JSON Schemas in `lazily-spec` and the Lean models in `lazily-formal`.
|
|
@@ -53,6 +67,7 @@ and JSON Schemas in `lazily-spec` and the Lean models in `lazily-formal`.
|
|
|
53
67
|
|--------|------------|
|
|
54
68
|
| `@lazily-hub/lazily-js` | `lazily-spec` IPC wire types: `Snapshot`, `Delta`, `DeltaOp`, `IpcMessage` (`Snapshot` / `Delta` / `CrdtSync`), `NodeState`, `IpcValue`, `PeerPermissions`, `SessionHandshake`, `BINDING_CAPABILITIES` |
|
|
55
69
|
| `@lazily-hub/lazily-js/reactive` | Reactive dependency graph: `Context`, `Cell`, `Slot`, `Signal`, `Effect` |
|
|
70
|
+
| `@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 |
|
|
56
71
|
| `@lazily-hub/lazily-js/state-machine` | Flat finite-state-machine kernel backed by a reactive `Cell` |
|
|
57
72
|
| `@lazily-hub/lazily-js/statechart` | Harel/SCXML chart interpreter plus `ChartBuilder`, `StateBuilder`, `TransitionBuilder` |
|
|
58
73
|
| `@lazily-hub/lazily-js/collections` | `CellMap`, `CellTree`, keyed reconciliation, and LIS move minimization |
|
|
@@ -60,6 +75,8 @@ and JSON Schemas in `lazily-spec` and the Lean models in `lazily-formal`.
|
|
|
60
75
|
| `@lazily-hub/lazily-js/seq-crdt` | Move-aware sequence CRDT using independent LWW value / position / deletion registers |
|
|
61
76
|
| `@lazily-hub/lazily-js/text-crdt` | Fugue/RGA character CRDT |
|
|
62
77
|
| `@lazily-hub/lazily-js/stable-id` | Manufactured text identity: anchors, content hashes, similarity alignment |
|
|
78
|
+
| `@lazily-hub/lazily-js/signaling` | WebSocket signaling protocol: `ClientMessage` / `ServerMessage`, `SignalingClient`, `SignalingRoom` routing (anti-spoof, roster), `SignalingPermissions` |
|
|
79
|
+
| `@lazily-hub/lazily-js/distributed` | Distributed plane: `DataChannel` seam + `InMemoryDataChannel`, `WebRtcSink` / `WebRtcSource`, `CrdtPlaneRuntime` anti-entropy, and the browser `RTCPeerConnection` adapter |
|
|
63
80
|
| `@lazily-hub/lazily-js/state-projection` | koffi FFI consumer for agent-doc `DocumentStateProjection` |
|
|
64
81
|
|
|
65
82
|
## Reactive graph
|
|
@@ -96,6 +113,40 @@ ctx.setCell(a, 11);
|
|
|
96
113
|
ctx.getSignal(parity); // "odd", already materialized
|
|
97
114
|
```
|
|
98
115
|
|
|
116
|
+
## Async reactive context
|
|
117
|
+
|
|
118
|
+
`AsyncContext` (from `@lazily-hub/lazily-js/reactive-async`) is a **separate**
|
|
119
|
+
reactive surface for derivations whose values are produced by `async` functions.
|
|
120
|
+
It is not an overload of the synchronous `Context`: futures introduce in-flight
|
|
121
|
+
state, stale completion, cancellation, and dependency tracking across `await`
|
|
122
|
+
that the synchronous graph does not have. Cells remain the synchronous input
|
|
123
|
+
layer; computed slots, memos, and effects are async.
|
|
124
|
+
|
|
125
|
+
Each async slot runs an `Empty → Computing → Resolved/Error` state machine with
|
|
126
|
+
**revision-guarded publish** (a completion is published only if the slot's
|
|
127
|
+
revision is still current, so a stale result is discarded), **in-flight
|
|
128
|
+
deduplication** (concurrent `getAsync` callers share one compute), and
|
|
129
|
+
cooperative cancellation. Async effects serialize reruns and always run the
|
|
130
|
+
previous cleanup before the next body.
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
import { AsyncContext } from "@lazily-hub/lazily-js/reactive-async";
|
|
134
|
+
|
|
135
|
+
const ctx = new AsyncContext();
|
|
136
|
+
const userId = ctx.cell(1);
|
|
137
|
+
|
|
138
|
+
const profile = ctx.computedAsync(async (cctx) => {
|
|
139
|
+
const id = cctx.getCell(userId); // dependency registered before the await
|
|
140
|
+
return await fetchProfile(id);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await ctx.getAsync(profile); // spawns the compute, awaits the value
|
|
144
|
+
ctx.get(profile); // synchronous cached read once resolved (undefined while pending)
|
|
145
|
+
|
|
146
|
+
ctx.setCell(userId, 2); // supersedes any in-flight compute; slot re-resolves
|
|
147
|
+
await ctx.getAsync(profile); // the profile for user 2
|
|
148
|
+
```
|
|
149
|
+
|
|
99
150
|
## State machine and state charts
|
|
100
151
|
|
|
101
152
|
`StateMachine` is the flat finite-state-machine kernel: a pure
|
|
@@ -280,18 +331,54 @@ snapshot.filterReadable(permissions, 11).nodes.length; // 0
|
|
|
280
331
|
|
|
281
332
|
`BINDING_CAPABILITIES` advertises the JS binding truthfully: reactive core,
|
|
282
333
|
IPC, CRDT, keyed collections, semantic tree, sequence/text CRDT, stable-id,
|
|
283
|
-
state machine, state charts, permissions,
|
|
284
|
-
shipped; C-ABI FFI is `none` because
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
334
|
+
state machine, state charts, permissions, capability negotiation, async context,
|
|
335
|
+
signaling, and the WebRTC transport are shipped; C-ABI FFI is `none` because
|
|
336
|
+
browser/Worker JS cannot host a native in-process ABI. The same payload types
|
|
337
|
+
can still be carried by any transport a host application owns.
|
|
338
|
+
|
|
339
|
+
## Distributed plane
|
|
340
|
+
|
|
341
|
+
The `@lazily-hub/lazily-js/signaling` and `@lazily-hub/lazily-js/distributed`
|
|
342
|
+
entry points ship the distributed plane. Signaling is the kebab-tagged discovery
|
|
343
|
+
wire protocol (`ClientMessage` / `ServerMessage`), a transport-agnostic
|
|
344
|
+
`SignalingRoom` that enforces the anti-spoof forwarded-`from` invariant, and a
|
|
345
|
+
`SignalingClient` over a pluggable socket seam. The distributed module is the
|
|
346
|
+
WebRTC DataChannel IPC transport (`WebRtcSink` / `WebRtcSource` with outbound
|
|
347
|
+
permission filtering, over any `DataChannel`) plus `CrdtPlaneRuntime`, the CRDT
|
|
348
|
+
anti-entropy runtime. Both are koffi-free and testable over an in-memory loopback
|
|
349
|
+
with zero network; "real" WebRTC is reached through a browser platform adapter
|
|
350
|
+
(`RtcPeerChannel` / `RtcPeerConnector`) that wraps the `RTCDataChannel` /
|
|
351
|
+
`RTCPeerConnection` globals with no npm dependency.
|
|
352
|
+
|
|
353
|
+
```js
|
|
354
|
+
import { CrdtPlaneRuntime, InMemoryDataChannel, WebRtcSink, WebRtcSource } from "@lazily-hub/lazily-js/distributed";
|
|
355
|
+
import { SignalingRoom, ClientMessage } from "@lazily-hub/lazily-js/signaling";
|
|
356
|
+
import { IpcMessage, IpcValue, PeerPermissions, OpKind, Snapshot, NodeSnapshot } from "@lazily-hub/lazily-js";
|
|
357
|
+
|
|
358
|
+
// Route a signaling handshake with server-stamped `from` (anti-spoof).
|
|
359
|
+
const room = new SignalingRoom();
|
|
360
|
+
room.receive("a", ClientMessage.join(1));
|
|
361
|
+
room.receive("b", ClientMessage.join(2));
|
|
362
|
+
room.receive("a", ClientMessage.offer(2, "SDP-A")); // -> { to: "b", message: offer{ from: 1, sdp: "SDP-A" } }
|
|
363
|
+
|
|
364
|
+
// Two replicas converge over an anti-entropy exchange.
|
|
365
|
+
const alice = new CrdtPlaneRuntime(1);
|
|
366
|
+
const bob = new CrdtPlaneRuntime(2);
|
|
367
|
+
alice.register(1, "doc/title");
|
|
368
|
+
bob.register(1, "doc/title");
|
|
369
|
+
const op = alice.localUpdate(1, Date.now() * 1000, IpcValue.inline([66]));
|
|
370
|
+
bob.ingest(alice.syncFrame(), Date.now() * 1000); // 1 op applied; re-ingest applies 0
|
|
371
|
+
```
|
|
288
372
|
|
|
289
373
|
## Conformance
|
|
290
374
|
|
|
291
375
|
lazily-js replays the shared `lazily-spec` fixtures for IPC, agent-doc state,
|
|
292
376
|
keyed collections (`CellMap`, `CellTree`, LIS reconciliation), semantic tree,
|
|
293
|
-
sequence and text CRDTs, manufactured text identity,
|
|
294
|
-
|
|
377
|
+
sequence and text CRDTs, manufactured text identity, Harel state charts, the
|
|
378
|
+
signaling protocol (`signaling/frames.json`, `signaling/anti_spoof_session.json`),
|
|
379
|
+
and the distributed CRDT plane (`distributed/crdt_sync_frames.json`,
|
|
380
|
+
`distributed/anti_entropy_converge.json`). It also validates generated wire
|
|
381
|
+
values against the canonical JSON Schemas.
|
|
295
382
|
|
|
296
383
|
`npm test` builds the [`lazily-formal`][formal] Lean 4 model when that sibling
|
|
297
384
|
checkout and the `lake` toolchain are present. The script exits successfully
|
|
@@ -309,9 +396,15 @@ names the Lean theorems it mirrors:
|
|
|
309
396
|
| `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` |
|
|
310
397
|
| `Tree` | `tree-properties.test.js` | `setNodeValue_preserves_{other_nodes,node_signals}`, `moveChild_preserves_{non_parent,parent_value}`, `moveChild_advances_order_signal_only` |
|
|
311
398
|
| `Reconciliation` | `reconciliation-properties.test.js` | `lisBy_longest`, `reconcile_move_minimized`, `reconcile_stable_not_invalidated` |
|
|
399
|
+
| `AsyncSlotState` | `reactive-async.test.js` | `stale_completeOk_discarded`, `current_completeOk_publishes`, `current_completeErr_to_error` |
|
|
400
|
+
| `AsyncEffect` | `reactive-async.test.js` | `fire_blocked_during_cleanup`, `invalidate_from_idle_schedules`, `cleanupDone_resumes_deferred`, `dispose_absorbing`, `disposed_terminal` |
|
|
312
401
|
|
|
313
|
-
|
|
314
|
-
|
|
402
|
+
Thread-safe context theorems are not mirrored because JavaScript is
|
|
403
|
+
single-threaded (one event loop): a lock-backed context has no meaning on this
|
|
404
|
+
runtime — the concurrency-layer platform carve-out. The `Signaling` /
|
|
405
|
+
`SignalingRoster` formal models are exercised through the `SignalingRoom`
|
|
406
|
+
fixture replay (`signaling/anti_spoof_session.json`) rather than a named-theorem
|
|
407
|
+
property test.
|
|
315
408
|
|
|
316
409
|
## The lazily family
|
|
317
410
|
|
|
@@ -319,7 +412,7 @@ because lazily-js does not ship async or thread-safe context surfaces.
|
|
|
319
412
|
|---------|----------|----------------|
|
|
320
413
|
| [`lazily-rs`][rs] | Rust | `lazily` on crates.io; single-threaded, thread-safe, and async context layers |
|
|
321
414
|
| [`lazily-py`][py] | Python | `lazily` on PyPI; dict-backed context plus IPC/shared-blob host types |
|
|
322
|
-
| **`lazily-js`** | JavaScript / TypeScript | `@lazily-hub/lazily-js`; reactive core, spec wire types, state charts, CRDTs |
|
|
415
|
+
| **`lazily-js`** | JavaScript / TypeScript | `@lazily-hub/lazily-js`; reactive core + async context, spec wire types, state charts, CRDTs, distributed plane (signaling + WebRTC) |
|
|
323
416
|
| [`lazily-zig`][zig] | Zig | Zig library / FFI-oriented embedding surface |
|
|
324
417
|
| [`lazily-kt`][kt] | Kotlin/JVM | Kotlin reactive core plus typed state charts |
|
|
325
418
|
| [`lazily-dart`][dart] | Dart | Dart binding with statechart conformance |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazily-hub/lazily-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
"types": "./src/reactive.d.ts",
|
|
15
15
|
"default": "./src/reactive.js"
|
|
16
16
|
},
|
|
17
|
+
"./reactive-async": {
|
|
18
|
+
"types": "./src/reactive-async.d.ts",
|
|
19
|
+
"default": "./src/reactive-async.js"
|
|
20
|
+
},
|
|
17
21
|
"./state-machine": {
|
|
18
22
|
"types": "./src/state-machine.d.ts",
|
|
19
23
|
"default": "./src/state-machine.js"
|
|
@@ -45,10 +49,18 @@
|
|
|
45
49
|
"./state-projection": {
|
|
46
50
|
"types": "./src/state-projection.d.ts",
|
|
47
51
|
"default": "./src/state-projection.js"
|
|
52
|
+
},
|
|
53
|
+
"./signaling": {
|
|
54
|
+
"types": "./src/signaling.d.ts",
|
|
55
|
+
"default": "./src/signaling.js"
|
|
56
|
+
},
|
|
57
|
+
"./distributed": {
|
|
58
|
+
"types": "./src/distributed.d.ts",
|
|
59
|
+
"default": "./src/distributed.js"
|
|
48
60
|
}
|
|
49
61
|
},
|
|
50
62
|
"scripts": {
|
|
51
|
-
"build": "node --check src/index.js && node --check src/reactive.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/state-projection.js",
|
|
63
|
+
"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/state-projection.js && node --check src/signaling.js && node --check src/distributed.js",
|
|
52
64
|
"test:formal": "node scripts/formal-check.mjs",
|
|
53
65
|
"test": "npm run test:formal && node --test test/*.test.js"
|
|
54
66
|
},
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CrdtOp,
|
|
3
|
+
CrdtSync,
|
|
4
|
+
IpcMessage,
|
|
5
|
+
IpcValueInline,
|
|
6
|
+
IpcValueSharedBlob,
|
|
7
|
+
PeerPermissions,
|
|
8
|
+
WireStamp,
|
|
9
|
+
} from "./index.js";
|
|
10
|
+
import type { SignalingClient } from "./signaling.js";
|
|
11
|
+
|
|
12
|
+
export type PeerId = number;
|
|
13
|
+
export type NodeId = number;
|
|
14
|
+
export type IpcValue = IpcValueInline | IpcValueSharedBlob;
|
|
15
|
+
|
|
16
|
+
// --- DataChannel transport ---
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Minimal ordered, reliable, bidirectional byte-frame channel. One frame is one
|
|
20
|
+
* serialized IpcMessage; the methods are non-blocking.
|
|
21
|
+
*/
|
|
22
|
+
export interface DataChannel {
|
|
23
|
+
sendFrame(frame: Uint8Array): void;
|
|
24
|
+
tryRecvFrame(): Uint8Array | null;
|
|
25
|
+
isOpen(): boolean;
|
|
26
|
+
close(): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const WebRtcTransportErrorKind: {
|
|
30
|
+
readonly Closed: "Closed";
|
|
31
|
+
readonly Encode: "Encode";
|
|
32
|
+
readonly Decode: "Decode";
|
|
33
|
+
readonly Channel: "Channel";
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export class WebRtcTransportError extends Error {
|
|
37
|
+
readonly name: "WebRtcTransportError";
|
|
38
|
+
readonly kind: "Closed" | "Encode" | "Decode" | "Channel";
|
|
39
|
+
readonly cause?: unknown;
|
|
40
|
+
constructor(kind: "Closed" | "Encode" | "Decode" | "Channel", cause?: unknown);
|
|
41
|
+
static closed(): WebRtcTransportError;
|
|
42
|
+
static encode(cause: unknown): WebRtcTransportError;
|
|
43
|
+
static decode(cause: unknown): WebRtcTransportError;
|
|
44
|
+
static channel(cause: unknown): WebRtcTransportError;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class InMemoryDataChannel implements DataChannel {
|
|
48
|
+
static pair(): [InMemoryDataChannel, InMemoryDataChannel];
|
|
49
|
+
sendFrame(frame: Uint8Array): void;
|
|
50
|
+
tryRecvFrame(): Uint8Array | null;
|
|
51
|
+
isOpen(): boolean;
|
|
52
|
+
close(): void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class WebRtcSink {
|
|
56
|
+
constructor(channel: DataChannel, permissions: PeerPermissions, peer: PeerId);
|
|
57
|
+
readonly channel: DataChannel;
|
|
58
|
+
send(message: IpcMessage): void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class WebRtcSource {
|
|
62
|
+
constructor(channel: DataChannel);
|
|
63
|
+
readonly channel: DataChannel;
|
|
64
|
+
recv(): IpcMessage | null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// --- CRDT anti-entropy runtime ---
|
|
68
|
+
|
|
69
|
+
export interface PlaneCell {
|
|
70
|
+
merge(op: CrdtOp): boolean;
|
|
71
|
+
readonly value: IpcValue | undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface FrontierEntry {
|
|
75
|
+
peer: PeerId;
|
|
76
|
+
stamp: WireStamp;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ConvergedEntry {
|
|
80
|
+
node: NodeId;
|
|
81
|
+
key?: string;
|
|
82
|
+
state: unknown;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export class CrdtPlaneRuntime {
|
|
86
|
+
constructor(peer: PeerId);
|
|
87
|
+
readonly peer: PeerId;
|
|
88
|
+
readonly size: number;
|
|
89
|
+
isEmpty(): boolean;
|
|
90
|
+
register(node: NodeId, key?: string | null, cell?: PlaneCell | null): this;
|
|
91
|
+
value(node: NodeId): IpcValue | undefined;
|
|
92
|
+
winningOp(node: NodeId): CrdtOp | undefined;
|
|
93
|
+
nodes(): NodeId[];
|
|
94
|
+
converged(): ConvergedEntry[];
|
|
95
|
+
localUpdate(node: NodeId, nowMicros: number, state: IpcValue | Uint8Array): CrdtOp | null;
|
|
96
|
+
ingest(sync: CrdtSync, nowMicros: number): number;
|
|
97
|
+
frontierEntries(): FrontierEntry[];
|
|
98
|
+
wireFrontier(): Array<[PeerId, unknown]>;
|
|
99
|
+
membership(): PeerId[];
|
|
100
|
+
membershipCount(): number;
|
|
101
|
+
syncFrame(): CrdtSync;
|
|
102
|
+
syncFrameSince(
|
|
103
|
+
since: Iterable<FrontierEntry | [PeerId, WireStamp | unknown]>,
|
|
104
|
+
): CrdtSync;
|
|
105
|
+
syncReply(request: CrdtSync): CrdtSync;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- Browser WebRTC platform adapter ---
|
|
109
|
+
|
|
110
|
+
export function isWebRtcAvailable(): boolean;
|
|
111
|
+
|
|
112
|
+
export class RtcPeerChannel implements DataChannel {
|
|
113
|
+
constructor(dataChannel: RTCDataChannel);
|
|
114
|
+
sendFrame(frame: Uint8Array): void;
|
|
115
|
+
tryRecvFrame(): Uint8Array | null;
|
|
116
|
+
isOpen(): boolean;
|
|
117
|
+
close(): void;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface RtcPeerConnectorOptions {
|
|
121
|
+
rtcConfig?: RTCConfiguration;
|
|
122
|
+
remote?: PeerId | null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export class RtcPeerConnector {
|
|
126
|
+
constructor(signalingClient: SignalingClient, options?: RtcPeerConnectorOptions);
|
|
127
|
+
readonly connection: RTCPeerConnection;
|
|
128
|
+
set remote(peer: PeerId);
|
|
129
|
+
createDataChannel(label?: string, options?: RTCDataChannelInit): RtcPeerChannel;
|
|
130
|
+
onDataChannel(): Promise<RtcPeerChannel>;
|
|
131
|
+
createOffer(to: PeerId): Promise<RTCSessionDescriptionInit>;
|
|
132
|
+
acceptOffer(from: PeerId, sdp: string): Promise<RTCSessionDescriptionInit>;
|
|
133
|
+
acceptAnswer(sdp: string): Promise<void>;
|
|
134
|
+
addIceCandidate(candidate: string | RTCIceCandidateInit): Promise<void>;
|
|
135
|
+
close(): void;
|
|
136
|
+
}
|