@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.
@@ -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
- run: npm publish --access public --tag prev
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 and its cross-language coverage (`lazily-rs`,
28
- `lazily-kt`, `lazily-js`). `✅` shipped, `~` partial, `—` not applicable/absent.
29
-
30
- | Feature | Rust | Kotlin | JS |
31
- |---------|:----:|:------:|:--:|
32
- | Reactive graph `Context`, `Slot`, `Cell`, `memo`, `Signal` (eager), `Effect`, `batch` | | | |
33
- | Thread-safe `Context` (`Send + Sync`, lock-backed) | ✅ | ✅ | — |
34
- | Async reactive `Context` | ✅ | ✅ | |
35
- | Statechart (Harel) + state machine | ✅ | ✅ | ✅ |
36
- | Keyed cell collections + `reconcile` + `SemTree` (keyed tree) | ✅ | ✅ | |
37
- | Stable-id alignment (manufactured identity) | ✅ | ✅ | ✅ |
38
- | Free-text character CRDT (`TextCrdt`) | ✅ | ✅ | ✅ |
39
- | **`TextCrdt` delta sync `version_vector` / `delta_since` / `apply_delta` (`#lztextsync`)** | ✅ | ✅ | ✅ |
40
- | Move-aware sequence CRDT (`SeqCrdt`) | ✅ | ✅ | ✅ |
41
- | Registers (LWW / MV), `PnCounter`, `CellCrdt` | | ✅ | ✅ |
42
- | IPC wire `Snapshot` + `Delta` + `CrdtSync` + shared-memory blobs | ✅ | | ~ |
43
- | State projection / mirror | ✅ | ✅ | ✅ |
44
- | FFI boundary | ✅ | ✅ | n/a |
45
- | Distributed plane (WebRTC transport + signaling) | ✅ | — | — |
46
- | Instrumentation / benchmarks | ✅ | | |
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, and capability negotiation are
284
- shipped; C-ABI FFI is `none` because browser/Worker JS cannot host a native
285
- in-process ABI; async context, signaling, and WebRTC transports are not shipped
286
- by this package. The same payload types can still be carried by any transport a
287
- host application owns.
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, and Harel state charts.
294
- It also validates generated wire values against the canonical JSON Schemas.
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
- `AsyncSlotState`, `AsyncEffect`, and thread-safe context theorems are not mirrored
314
- because lazily-js does not ship async or thread-safe context surfaces.
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.4.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
+ }