@lazily-hub/lazily-js 0.4.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 +48 -0
- package/README.md +356 -0
- package/package.json +64 -0
- package/src/collections.d.ts +110 -0
- package/src/collections.js +413 -0
- package/src/index.d.ts +416 -0
- package/src/index.js +1109 -0
- package/src/reactive.d.ts +43 -0
- package/src/reactive.js +531 -0
- package/src/sem-tree.d.ts +21 -0
- package/src/sem-tree.js +130 -0
- package/src/seq-crdt.d.ts +44 -0
- package/src/seq-crdt.js +364 -0
- package/src/stable-id.d.ts +45 -0
- package/src/stable-id.js +214 -0
- package/src/state-machine.d.ts +14 -0
- package/src/state-machine.js +78 -0
- package/src/state-projection.d.ts +114 -0
- package/src/state-projection.js +256 -0
- package/src/statechart.d.ts +154 -0
- package/src/statechart.js +678 -0
- package/src/text-crdt.d.ts +23 -0
- package/src/text-crdt.js +317 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: release-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: false
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
publish-npm:
|
|
15
|
+
name: Publish to npm (OIDC trusted publishing)
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
# OIDC trusted publishing: no long-lived NPM_TOKEN. The package must already
|
|
18
|
+
# exist on npm (npm has no "pending publisher"), then configure the Trusted
|
|
19
|
+
# Publisher on the package: user/org = lazily-hub, repo = lazily-js,
|
|
20
|
+
# workflow = release.yml, action = "npm publish".
|
|
21
|
+
permissions:
|
|
22
|
+
id-token: write
|
|
23
|
+
contents: read
|
|
24
|
+
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v4
|
|
27
|
+
|
|
28
|
+
- uses: actions/setup-node@v4
|
|
29
|
+
with:
|
|
30
|
+
node-version: 24
|
|
31
|
+
registry-url: https://registry.npmjs.org
|
|
32
|
+
|
|
33
|
+
# Trusted publishing needs npm CLI >= 11.5.1; Node 24's bundled npm may lag.
|
|
34
|
+
- name: Upgrade npm for OIDC trusted publishing
|
|
35
|
+
run: npm install -g npm@latest
|
|
36
|
+
|
|
37
|
+
- name: Install dependencies
|
|
38
|
+
run: npm ci || npm install
|
|
39
|
+
|
|
40
|
+
- name: Test
|
|
41
|
+
run: npm test --if-present
|
|
42
|
+
|
|
43
|
+
# Published under the `prev` dist-tag so this v0.x line does not steal
|
|
44
|
+
# `latest` from a higher already-published version; move `latest` explicitly
|
|
45
|
+
# (`npm dist-tag add @lazily-hub/lazily-js@<v> latest`) once the accidental
|
|
46
|
+
# v1.x releases are removed.
|
|
47
|
+
- name: Publish
|
|
48
|
+
run: npm publish --access public --tag prev
|
package/README.md
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# lazily-js
|
|
2
|
+
|
|
3
|
+
Native JavaScript port of the **lazily** reactive family. `@lazily-hub/lazily-js`
|
|
4
|
+
is the JavaScript/TypeScript binding alongside [`lazily-rs`][rs],
|
|
5
|
+
[`lazily-py`][py], [`lazily-zig`][zig], [`lazily-kt`][kt], and
|
|
6
|
+
[`lazily-dart`][dart]. It ships:
|
|
7
|
+
|
|
8
|
+
- a full reactive graph (`Context`, `Cell`, `Slot`, `Signal`, `Effect`);
|
|
9
|
+
- the [`lazily-spec`][spec] IPC wire types (`Snapshot`, `Delta`, `CrdtSync`,
|
|
10
|
+
capability negotiation, and default-deny peer permissions);
|
|
11
|
+
- keyed cell collections, move-minimized reconciliation, and a memoized
|
|
12
|
+
semantic tree;
|
|
13
|
+
- move-aware sequence CRDT, Fugue/RGA text CRDT, and manufactured text identity;
|
|
14
|
+
- a Cell-backed flat state machine plus a full Harel/SCXML state-chart
|
|
15
|
+
interpreter with the typed `ChartBuilder` API;
|
|
16
|
+
- a koffi-backed state-projection consumer for agent-doc host projections.
|
|
17
|
+
|
|
18
|
+
> **Package note.** Earlier `@lazily-hub/js` releases were only a state-projection
|
|
19
|
+
> consumer. `@lazily-hub/lazily-js` is the current full reactive binding.
|
|
20
|
+
|
|
21
|
+
Pure ES modules. The reactive, IPC, collections, CRDT, state-machine, and
|
|
22
|
+
state-chart modules have no runtime dependencies; `koffi` is loaded only when
|
|
23
|
+
the FFI projection transport is used.
|
|
24
|
+
|
|
25
|
+
## Feature Set
|
|
26
|
+
|
|
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 | ✅ | — | — |
|
|
47
|
+
|
|
48
|
+
CRDT convergence and the wire protocol are pinned by the shared conformance fixtures
|
|
49
|
+
and JSON Schemas in `lazily-spec` and the Lean models in `lazily-formal`.
|
|
50
|
+
## Packages
|
|
51
|
+
|
|
52
|
+
| Import | What it is |
|
|
53
|
+
|--------|------------|
|
|
54
|
+
| `@lazily-hub/lazily-js` | `lazily-spec` IPC wire types: `Snapshot`, `Delta`, `DeltaOp`, `IpcMessage` (`Snapshot` / `Delta` / `CrdtSync`), `NodeState`, `IpcValue`, `PeerPermissions`, `SessionHandshake`, `BINDING_CAPABILITIES` |
|
|
55
|
+
| `@lazily-hub/lazily-js/reactive` | Reactive dependency graph: `Context`, `Cell`, `Slot`, `Signal`, `Effect` |
|
|
56
|
+
| `@lazily-hub/lazily-js/state-machine` | Flat finite-state-machine kernel backed by a reactive `Cell` |
|
|
57
|
+
| `@lazily-hub/lazily-js/statechart` | Harel/SCXML chart interpreter plus `ChartBuilder`, `StateBuilder`, `TransitionBuilder` |
|
|
58
|
+
| `@lazily-hub/lazily-js/collections` | `CellMap`, `CellTree`, keyed reconciliation, and LIS move minimization |
|
|
59
|
+
| `@lazily-hub/lazily-js/sem-tree` | Memoized semantic tree over `CellTree`-shaped data |
|
|
60
|
+
| `@lazily-hub/lazily-js/seq-crdt` | Move-aware sequence CRDT using independent LWW value / position / deletion registers |
|
|
61
|
+
| `@lazily-hub/lazily-js/text-crdt` | Fugue/RGA character CRDT |
|
|
62
|
+
| `@lazily-hub/lazily-js/stable-id` | Manufactured text identity: anchors, content hashes, similarity alignment |
|
|
63
|
+
| `@lazily-hub/lazily-js/state-projection` | koffi FFI consumer for agent-doc `DocumentStateProjection` |
|
|
64
|
+
|
|
65
|
+
## Reactive graph
|
|
66
|
+
|
|
67
|
+
`Context` mirrors the single-threaded lazily-rs `Context` semantics in native
|
|
68
|
+
JavaScript. The family is:
|
|
69
|
+
|
|
70
|
+
- **Slot** - lazy, memoized derived value;
|
|
71
|
+
- **Cell** - mutable source value;
|
|
72
|
+
- **Signal** - eager derived value that re-materializes as soon as a dependency
|
|
73
|
+
invalidates;
|
|
74
|
+
- **Effect** - side-effecting observer with cleanup.
|
|
75
|
+
|
|
76
|
+
Dependencies are discovered dynamically while a slot/effect/signal computes.
|
|
77
|
+
Invalidation is pull-based and glitch-free; `memo`/`Signal` use equality guards
|
|
78
|
+
to suppress downstream work when a recompute produces the same value. `batch`
|
|
79
|
+
coalesces invalidations and effect reruns.
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
import { Context } from "@lazily-hub/lazily-js/reactive";
|
|
83
|
+
|
|
84
|
+
const ctx = new Context();
|
|
85
|
+
const a = ctx.cell(2);
|
|
86
|
+
const b = ctx.cell(3);
|
|
87
|
+
|
|
88
|
+
const sum = ctx.memo(() => ctx.getCell(a) + ctx.getCell(b));
|
|
89
|
+
ctx.get(sum); // 5
|
|
90
|
+
|
|
91
|
+
ctx.setCell(a, 10);
|
|
92
|
+
ctx.get(sum); // 13, recomputed lazily on read
|
|
93
|
+
|
|
94
|
+
const parity = ctx.signal(() => (ctx.getCell(a) % 2 === 0 ? "even" : "odd"));
|
|
95
|
+
ctx.setCell(a, 11);
|
|
96
|
+
ctx.getSignal(parity); // "odd", already materialized
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## State machine and state charts
|
|
100
|
+
|
|
101
|
+
`StateMachine` is the flat finite-state-machine kernel: a pure
|
|
102
|
+
`(state, event) -> nextState | null` transition backed by a reactive `Cell`.
|
|
103
|
+
Accepted self-transitions to an equal state are suppressed by the Cell equality
|
|
104
|
+
guard.
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
import { Context } from "@lazily-hub/lazily-js/reactive";
|
|
108
|
+
import { StateMachine } from "@lazily-hub/lazily-js/state-machine";
|
|
109
|
+
|
|
110
|
+
const ctx = new Context();
|
|
111
|
+
const light = new StateMachine(ctx, "Red", (state, event) =>
|
|
112
|
+
event === "advance" ? { Red: "Green", Green: "Yellow", Yellow: "Red" }[state] : null,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
light.send("advance"); // true
|
|
116
|
+
light.state; // "Green"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`StateChart` implements the full Harel/SCXML subset from
|
|
120
|
+
[`lazily-spec/docs/state-charts.md`][statecharts]: compound states, parallel
|
|
121
|
+
regions, shallow/deep history, entry/exit/transition actions, named guards
|
|
122
|
+
(fail-closed), internal/external transitions, and final leaves. Charts are
|
|
123
|
+
**compute, not protocol**: the chart itself is not serialized as a special wire
|
|
124
|
+
kind; only an application-level active configuration would cross IPC as ordinary
|
|
125
|
+
payload state.
|
|
126
|
+
|
|
127
|
+
The normative definition path is the declarative JSON chart consumed by the
|
|
128
|
+
shared conformance fixtures:
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
import { ChartDef, StateChart } from "@lazily-hub/lazily-js/statechart";
|
|
132
|
+
|
|
133
|
+
const def = ChartDef.fromChart({
|
|
134
|
+
initial: "root",
|
|
135
|
+
states: {
|
|
136
|
+
root: { parallel: true },
|
|
137
|
+
flow: { parent: "root", initial: "idle" },
|
|
138
|
+
idle: { parent: "flow", on: { go: { target: "done", guard: "ready" } } },
|
|
139
|
+
done: { parent: "flow", kind: "final" },
|
|
140
|
+
net: { parent: "root", initial: "up" },
|
|
141
|
+
up: { parent: "net", on: { drop: "down" } },
|
|
142
|
+
down: { parent: "net", on: { restore: "up" } },
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const chart = new StateChart(def);
|
|
147
|
+
chart.activeLeaves(); // ["idle", "up"]
|
|
148
|
+
chart.send("drop"); // true
|
|
149
|
+
chart.send("go", { ready: true }); // true
|
|
150
|
+
chart.matches("done"); // true
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
For typed JavaScript/TypeScript authoring, `ChartBuilder` builds the same
|
|
154
|
+
`ChartDef` through the same validation/assembly path. It is an ergonomic API,
|
|
155
|
+
not a second semantics:
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
import {
|
|
159
|
+
ChartBuilder,
|
|
160
|
+
StateBuilder,
|
|
161
|
+
StateChart,
|
|
162
|
+
} from "@lazily-hub/lazily-js/statechart";
|
|
163
|
+
|
|
164
|
+
const def = new ChartBuilder()
|
|
165
|
+
.state(StateBuilder.parallel("root"))
|
|
166
|
+
.state(StateBuilder.compound("flow", "idle").parent("root"))
|
|
167
|
+
.state(StateBuilder.atomic("idle").parent("flow").onGuarded("go", "done", "ready"))
|
|
168
|
+
.state(StateBuilder.final("done").parent("flow"))
|
|
169
|
+
.state(StateBuilder.compound("net", "up").parent("root"))
|
|
170
|
+
.state(StateBuilder.atomic("up").parent("net").on("drop", "down"))
|
|
171
|
+
.state(StateBuilder.atomic("down").parent("net").on("restore", "up"))
|
|
172
|
+
.build();
|
|
173
|
+
|
|
174
|
+
const chart = new StateChart(def);
|
|
175
|
+
chart.send("go", { ready: false }); // false, guards fail closed
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Keyed collections and semantic tree
|
|
179
|
+
|
|
180
|
+
`CellMap` and `CellTree` implement the lazily-spec keyed collections layer:
|
|
181
|
+
value, membership, and order readers invalidate independently; stable handles
|
|
182
|
+
survive moves; and an atomic move bumps order without touching values.
|
|
183
|
+
`reconcileCollections` emits the LIS-minimized `{ insert, remove, move, update }`
|
|
184
|
+
operation set. `SemTree` adds a memoized ancestor-chain fold: editing one leaf
|
|
185
|
+
recomputes only that leaf's ancestor path, and equal folded results are
|
|
186
|
+
suppressed by the memo guard.
|
|
187
|
+
|
|
188
|
+
```js
|
|
189
|
+
import { CellMap, reconcileCollections } from "@lazily-hub/lazily-js/collections";
|
|
190
|
+
import { Context } from "@lazily-hub/lazily-js/reactive";
|
|
191
|
+
import { SemTree } from "@lazily-hub/lazily-js/sem-tree";
|
|
192
|
+
|
|
193
|
+
const map = CellMap.from({ order: ["a", "b"], values: { a: 1, b: 2 } });
|
|
194
|
+
map.moveBefore("b", "a"); // order reader invalidates; value readers do not
|
|
195
|
+
|
|
196
|
+
reconcileCollections(
|
|
197
|
+
{ order: ["a", "b"], values: { a: 1, b: 2 } },
|
|
198
|
+
{ order: ["b", "a", "c"], values: { a: 1, b: 2, c: 3 } },
|
|
199
|
+
).ops; // move-minimized patch
|
|
200
|
+
|
|
201
|
+
const ctx = new Context();
|
|
202
|
+
const rootSpec = {
|
|
203
|
+
id: "root",
|
|
204
|
+
value: 0,
|
|
205
|
+
children: {
|
|
206
|
+
order: ["leaf"],
|
|
207
|
+
values: { leaf: { id: "leaf", value: 1 } },
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
const tree = new SemTree(ctx, rootSpec, (value, children) =>
|
|
211
|
+
value + children.reduce((sum, child) => sum + child, 0),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
tree.value(); // 1
|
|
215
|
+
tree.setValue("leaf", 99); // only the ancestor chain recomputes
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## CRDTs
|
|
219
|
+
|
|
220
|
+
`SeqCrdt` is the move-aware sequence CRDT: each element has independent LWW
|
|
221
|
+
registers for value, position, and deletion, so a move is one position
|
|
222
|
+
assignment rather than delete plus reinsert. `TextCrdt` is a Fugue/RGA
|
|
223
|
+
character CRDT: concurrent same-point inserts are preserved, deletes are sticky
|
|
224
|
+
tombstones, and merge is commutative / associative / idempotent. Both expose
|
|
225
|
+
tombstone GC behind caller-supplied causal-stability watermarks.
|
|
226
|
+
|
|
227
|
+
```js
|
|
228
|
+
import { SeqCrdt } from "@lazily-hub/lazily-js/seq-crdt";
|
|
229
|
+
import { TextCrdt } from "@lazily-hub/lazily-js/text-crdt";
|
|
230
|
+
|
|
231
|
+
const seq = new SeqCrdt(1);
|
|
232
|
+
seq.insertBack("a", 0, 1);
|
|
233
|
+
seq.moveAfter("a", "b", 10);
|
|
234
|
+
|
|
235
|
+
const text = TextCrdt.fromStr(1, "hi");
|
|
236
|
+
const peer = text.fork(2);
|
|
237
|
+
peer.insert(2, "!");
|
|
238
|
+
text.merge(peer); // converges
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## IPC wire types and capability negotiation
|
|
242
|
+
|
|
243
|
+
Every IPC value round-trips the canonical externally-tagged
|
|
244
|
+
[`lazily-spec`][spec] JSON shape through `toWire()` / `fromWire()`.
|
|
245
|
+
`IpcMessage` adds `encodeJson()` / `decodeJson()`. `Snapshot` and `Delta`
|
|
246
|
+
represent the single-writer graph-state plane; `CrdtSync` carries the
|
|
247
|
+
multi-writer CRDT anti-entropy plane. `PeerPermissions` is default-deny and
|
|
248
|
+
filters unreadable nodes/ops out of snapshots and deltas. `SessionHandshake`
|
|
249
|
+
performs the fail-closed protocol/version/codec/feature check before graph
|
|
250
|
+
frames flow.
|
|
251
|
+
|
|
252
|
+
```js
|
|
253
|
+
import {
|
|
254
|
+
Delta,
|
|
255
|
+
DeltaOp,
|
|
256
|
+
IpcMessage,
|
|
257
|
+
NodeSnapshot,
|
|
258
|
+
PeerPermissions,
|
|
259
|
+
RemoteOp,
|
|
260
|
+
Snapshot,
|
|
261
|
+
} from "@lazily-hub/lazily-js";
|
|
262
|
+
|
|
263
|
+
const snapshot = new Snapshot({
|
|
264
|
+
epoch: 7,
|
|
265
|
+
nodes: [NodeSnapshot.payload(1, "counter", new Uint8Array([42]))],
|
|
266
|
+
roots: [1],
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const wire = IpcMessage.snapshot(snapshot).encodeJson();
|
|
270
|
+
IpcMessage.decodeJson(wire).snapshot.epoch; // 7
|
|
271
|
+
|
|
272
|
+
const delta = Delta.next(7, [DeltaOp.cellSet(1, [43])]);
|
|
273
|
+
IpcMessage.delta(delta).toWire();
|
|
274
|
+
|
|
275
|
+
const permissions = new PeerPermissions();
|
|
276
|
+
permissions.allow(10, RemoteOp.read(1));
|
|
277
|
+
snapshot.filterReadable(permissions, 10).nodes.length; // 1
|
|
278
|
+
snapshot.filterReadable(permissions, 11).nodes.length; // 0
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
`BINDING_CAPABILITIES` advertises the JS binding truthfully: reactive core,
|
|
282
|
+
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.
|
|
288
|
+
|
|
289
|
+
## Conformance
|
|
290
|
+
|
|
291
|
+
lazily-js replays the shared `lazily-spec` fixtures for IPC, agent-doc state,
|
|
292
|
+
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.
|
|
295
|
+
|
|
296
|
+
`npm test` builds the [`lazily-formal`][formal] Lean 4 model when that sibling
|
|
297
|
+
checkout and the `lake` toolchain are present. The script exits successfully
|
|
298
|
+
when they are absent, so npm tarball consumers and shallow clones are not forced
|
|
299
|
+
to install Lean; full CI verifies the proofs.
|
|
300
|
+
|
|
301
|
+
Each formal module with a JS counterpart has a matching property test that
|
|
302
|
+
names the Lean theorems it mirrors:
|
|
303
|
+
|
|
304
|
+
| lazily-formal module | JS test file | Mirrored theorems |
|
|
305
|
+
|----------------------|--------------|-------------------|
|
|
306
|
+
| `StateMachine` | `state-machine.test.js` | `guard_rejection_preserves_state`, `accepted_transition_advances_state`, `send_preserves_transition` |
|
|
307
|
+
| `StateChart` | `statechart-properties.test.js` | `enabled_empty_rejects`, `parallel_region_confluence`, `single_region_refines_flat_machine`, `single_region_enabled_at_most_one`, `recordHistory_idempotent`, `send_actions_empty_when_rejected`, `send_preserves_chart`, determinism-by-construction |
|
|
308
|
+
| `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` |
|
|
309
|
+
| `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
|
+
| `Tree` | `tree-properties.test.js` | `setNodeValue_preserves_{other_nodes,node_signals}`, `moveChild_preserves_{non_parent,parent_value}`, `moveChild_advances_order_signal_only` |
|
|
311
|
+
| `Reconciliation` | `reconciliation-properties.test.js` | `lisBy_longest`, `reconcile_move_minimized`, `reconcile_stable_not_invalidated` |
|
|
312
|
+
|
|
313
|
+
`AsyncSlotState`, `AsyncEffect`, and thread-safe context theorems are not mirrored
|
|
314
|
+
because lazily-js does not ship async or thread-safe context surfaces.
|
|
315
|
+
|
|
316
|
+
## The lazily family
|
|
317
|
+
|
|
318
|
+
| Binding | Language | Package / role |
|
|
319
|
+
|---------|----------|----------------|
|
|
320
|
+
| [`lazily-rs`][rs] | Rust | `lazily` on crates.io; single-threaded, thread-safe, and async context layers |
|
|
321
|
+
| [`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 |
|
|
323
|
+
| [`lazily-zig`][zig] | Zig | Zig library / FFI-oriented embedding surface |
|
|
324
|
+
| [`lazily-kt`][kt] | Kotlin/JVM | Kotlin reactive core plus typed state charts |
|
|
325
|
+
| [`lazily-dart`][dart] | Dart | Dart binding with statechart conformance |
|
|
326
|
+
| [`lazily-spec`][spec] | Specification | wire protocol, JSON Schemas, conformance fixtures |
|
|
327
|
+
| [`lazily-formal`][formal] | Lean 4 | executable formal model for the shared primitives, FSM, and state charts |
|
|
328
|
+
|
|
329
|
+
## Development
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
make check # npm run build && npm test
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
- `npm run build` runs `node --check` over every shipped module.
|
|
336
|
+
- `npm run test:formal` builds `lazily-formal` when the sibling checkout and
|
|
337
|
+
`lake` are present.
|
|
338
|
+
- `npm test` runs the formal check and the Node test suite.
|
|
339
|
+
|
|
340
|
+
## See also
|
|
341
|
+
|
|
342
|
+
- [`lazily-spec`][spec] - language-agnostic wire protocol, schemas, and
|
|
343
|
+
conformance fixtures.
|
|
344
|
+
- [`lazily-formal`][formal] - Lean 4 formal model behind the shared behavioral
|
|
345
|
+
guarantees.
|
|
346
|
+
- [`lazily-rs`][rs] / [`lazily-py`][py] / [`lazily-zig`][zig] /
|
|
347
|
+
[`lazily-kt`][kt] / [`lazily-dart`][dart] - sibling bindings.
|
|
348
|
+
|
|
349
|
+
[rs]: https://github.com/lazily-hub/lazily-rs
|
|
350
|
+
[py]: https://github.com/lazily-hub/lazily-py
|
|
351
|
+
[zig]: https://github.com/lazily-hub/lazily-zig
|
|
352
|
+
[kt]: https://github.com/lazily-hub/lazily-kt
|
|
353
|
+
[dart]: https://github.com/lazily-hub/lazily-dart
|
|
354
|
+
[spec]: https://github.com/lazily-hub/lazily-spec
|
|
355
|
+
[formal]: https://github.com/lazily-hub/lazily-formal
|
|
356
|
+
[statecharts]: https://github.com/lazily-hub/lazily-spec/blob/main/docs/state-charts.md
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lazily-hub/lazily-js",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Native JavaScript port of the lazily reactive core: a full reactive graph (Cell/Slot/Signal/Effect), the lazily-spec IPC wire types, keyed cell collections + LIS reconciliation, the memoized semantic tree, the move-aware sequence CRDT, the Fugue/RGA text CRDT, manufactured text identity, full-Harel state charts, and an FFI state-projection consumer.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"types": "src/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.d.ts",
|
|
11
|
+
"default": "./src/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./reactive": {
|
|
14
|
+
"types": "./src/reactive.d.ts",
|
|
15
|
+
"default": "./src/reactive.js"
|
|
16
|
+
},
|
|
17
|
+
"./state-machine": {
|
|
18
|
+
"types": "./src/state-machine.d.ts",
|
|
19
|
+
"default": "./src/state-machine.js"
|
|
20
|
+
},
|
|
21
|
+
"./statechart": {
|
|
22
|
+
"types": "./src/statechart.d.ts",
|
|
23
|
+
"default": "./src/statechart.js"
|
|
24
|
+
},
|
|
25
|
+
"./collections": {
|
|
26
|
+
"types": "./src/collections.d.ts",
|
|
27
|
+
"default": "./src/collections.js"
|
|
28
|
+
},
|
|
29
|
+
"./sem-tree": {
|
|
30
|
+
"types": "./src/sem-tree.d.ts",
|
|
31
|
+
"default": "./src/sem-tree.js"
|
|
32
|
+
},
|
|
33
|
+
"./stable-id": {
|
|
34
|
+
"types": "./src/stable-id.d.ts",
|
|
35
|
+
"default": "./src/stable-id.js"
|
|
36
|
+
},
|
|
37
|
+
"./seq-crdt": {
|
|
38
|
+
"types": "./src/seq-crdt.d.ts",
|
|
39
|
+
"default": "./src/seq-crdt.js"
|
|
40
|
+
},
|
|
41
|
+
"./text-crdt": {
|
|
42
|
+
"types": "./src/text-crdt.d.ts",
|
|
43
|
+
"default": "./src/text-crdt.js"
|
|
44
|
+
},
|
|
45
|
+
"./state-projection": {
|
|
46
|
+
"types": "./src/state-projection.d.ts",
|
|
47
|
+
"default": "./src/state-projection.js"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"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",
|
|
52
|
+
"test:formal": "node scripts/formal-check.mjs",
|
|
53
|
+
"test": "npm run test:formal && node --test test/*.test.js"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"koffi": "^3.0.2"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/node": "^22.0.0",
|
|
60
|
+
"ajv": "^8.20.0",
|
|
61
|
+
"tsx": "^4.19.0",
|
|
62
|
+
"typescript": "^5.6.0"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export type CollectionKey = string;
|
|
2
|
+
|
|
3
|
+
export type InvalidationReport = {
|
|
4
|
+
value: CollectionKey[];
|
|
5
|
+
membership: boolean;
|
|
6
|
+
order: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type CellMapOp =
|
|
10
|
+
| { type: "set_value"; key: CollectionKey; value: unknown }
|
|
11
|
+
| { type: "insert"; key: CollectionKey; value: unknown; at?: "end" | "start" | number | CollectionKey }
|
|
12
|
+
| { type: "remove"; key: CollectionKey }
|
|
13
|
+
| { type: "move_to"; key: CollectionKey; index: number }
|
|
14
|
+
| { type: "move_before"; key: CollectionKey; before: CollectionKey }
|
|
15
|
+
| { type: "move_after"; key: CollectionKey; after: CollectionKey };
|
|
16
|
+
|
|
17
|
+
export type CellMapSnapshot = {
|
|
18
|
+
order: CollectionKey[];
|
|
19
|
+
values: Record<string, unknown>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export class CellMap {
|
|
23
|
+
constructor(initial?: { order?: CollectionKey[]; values?: Record<string, unknown> });
|
|
24
|
+
order: CollectionKey[];
|
|
25
|
+
values: Map<CollectionKey, unknown>;
|
|
26
|
+
keys(): CollectionKey[];
|
|
27
|
+
has(key: CollectionKey): boolean;
|
|
28
|
+
get(key: CollectionKey): unknown;
|
|
29
|
+
handle(key: CollectionKey): number | undefined;
|
|
30
|
+
snapshot(): CellMapSnapshot;
|
|
31
|
+
apply(op: CellMapOp): InvalidationReport;
|
|
32
|
+
setValue(key: CollectionKey, value: unknown): InvalidationReport;
|
|
33
|
+
insert(
|
|
34
|
+
key: CollectionKey,
|
|
35
|
+
value: unknown,
|
|
36
|
+
at?: "end" | "start" | number | CollectionKey,
|
|
37
|
+
): InvalidationReport;
|
|
38
|
+
remove(key: CollectionKey): InvalidationReport;
|
|
39
|
+
moveTo(key: CollectionKey, index: number): InvalidationReport;
|
|
40
|
+
moveBefore(key: CollectionKey, beforeKey: CollectionKey): InvalidationReport;
|
|
41
|
+
moveAfter(key: CollectionKey, afterKey: CollectionKey): InvalidationReport;
|
|
42
|
+
static from(initial: { order?: CollectionKey[]; values?: Record<string, unknown> }): CellMap;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type ReconcileOp =
|
|
46
|
+
| { type: "remove"; key: CollectionKey }
|
|
47
|
+
| { type: "move"; key: CollectionKey; after: CollectionKey | null }
|
|
48
|
+
| { type: "insert"; key: CollectionKey; value: unknown; after: CollectionKey | null };
|
|
49
|
+
|
|
50
|
+
export type ReconcileResult = {
|
|
51
|
+
ops: ReconcileOp[];
|
|
52
|
+
result_order: CollectionKey[];
|
|
53
|
+
stable_keys_not_invalidated: CollectionKey[];
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function reconcileCollections(
|
|
57
|
+
prior: { order: CollectionKey[]; values?: Record<string, unknown> },
|
|
58
|
+
target: { order: CollectionKey[]; values?: Record<string, unknown> },
|
|
59
|
+
): ReconcileResult;
|
|
60
|
+
|
|
61
|
+
// Ordered keyed tree (cell-model.md § Ordered keyed tree).
|
|
62
|
+
export type TreeNodeSpec = {
|
|
63
|
+
id?: string;
|
|
64
|
+
value?: unknown;
|
|
65
|
+
children?: { order?: CollectionKey[]; values?: Record<string, TreeNodeSpec> };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type TreeNodeSnapshot = {
|
|
69
|
+
id?: string;
|
|
70
|
+
value?: unknown;
|
|
71
|
+
children: { order: CollectionKey[]; values: Record<string, TreeNodeSnapshot> };
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type TreeInvalidationReport = {
|
|
75
|
+
path: CollectionKey[];
|
|
76
|
+
value: CollectionKey[];
|
|
77
|
+
membership: boolean;
|
|
78
|
+
order: boolean;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export class TreeNode {
|
|
82
|
+
constructor(id: unknown, value: unknown, children: CellMap);
|
|
83
|
+
id: unknown;
|
|
84
|
+
value: unknown;
|
|
85
|
+
children: CellMap;
|
|
86
|
+
snapshot(): TreeNodeSnapshot;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class CellTree {
|
|
90
|
+
constructor(rootSpec: TreeNodeSpec | TreeNode);
|
|
91
|
+
root: TreeNode;
|
|
92
|
+
static from(rootSpec: TreeNodeSpec | TreeNode): CellTree;
|
|
93
|
+
nodeAt(path: CollectionKey[] | CollectionKey): TreeNode | undefined;
|
|
94
|
+
getValue(path: CollectionKey[] | CollectionKey): unknown;
|
|
95
|
+
setValue(path: CollectionKey[] | CollectionKey, value: unknown): TreeInvalidationReport;
|
|
96
|
+
hasChild(path: CollectionKey[] | CollectionKey, key: CollectionKey): boolean;
|
|
97
|
+
childKeys(path: CollectionKey[] | CollectionKey): CollectionKey[];
|
|
98
|
+
childHandle(path: CollectionKey[] | CollectionKey, key: CollectionKey): number | undefined;
|
|
99
|
+
insertChild(
|
|
100
|
+
path: CollectionKey[] | CollectionKey,
|
|
101
|
+
key: CollectionKey,
|
|
102
|
+
childSpec: TreeNodeSpec | TreeNode,
|
|
103
|
+
at?: "end" | "start" | number | CollectionKey,
|
|
104
|
+
): TreeInvalidationReport;
|
|
105
|
+
removeChild(path: CollectionKey[] | CollectionKey, key: CollectionKey): TreeInvalidationReport;
|
|
106
|
+
moveChildTo(path: CollectionKey[] | CollectionKey, key: CollectionKey, index: number): TreeInvalidationReport;
|
|
107
|
+
moveChildBefore(path: CollectionKey[] | CollectionKey, key: CollectionKey, beforeKey: CollectionKey): TreeInvalidationReport;
|
|
108
|
+
moveChildAfter(path: CollectionKey[] | CollectionKey, key: CollectionKey, afterKey: CollectionKey): TreeInvalidationReport;
|
|
109
|
+
snapshot(): TreeNodeSnapshot;
|
|
110
|
+
}
|