@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.
@@ -0,0 +1,678 @@
1
+ // Full Harel/SCXML state charts — native JavaScript, conforming to
2
+ // `lazily-spec/docs/state-charts.md` and the Lean `LazilyFormal.StateChart`
3
+ // formal model (`lazily-formal`).
4
+ //
5
+ // A chart is **compute, not protocol**: it is never serialized as a distinct
6
+ // wire kind. lazily-js is a state-projection *consumer* with no reactive
7
+ // graph, so the active configuration is a plain `Set` (not a `Cell`); the
8
+ // transition is pure logic with zero system dependencies, exactly as the spec
9
+ // requires for lazily-js / lazily-kt.
10
+ //
11
+ // Implemented subset (per the spec's implementation-status note): compound
12
+ // states, orthogonal (parallel) regions, shallow + deep history, entry/exit/
13
+ // transition actions, named guards, external + internal transitions. Extended
14
+ // state `{"expr": …}` guards and `run` actions are rejected explicitly; `final`
15
+ // states are accepted as leaves without raising completion (`done`) events.
16
+
17
+ const HISTORY_SHALLOW = "shallow";
18
+ const HISTORY_DEEP = "deep";
19
+
20
+ function assertObject(value, name) {
21
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
22
+ throw new TypeError(`${name} must be an object`);
23
+ }
24
+ return value;
25
+ }
26
+
27
+ function parseActionList(raw, label) {
28
+ if (raw === undefined || raw === null) {
29
+ return [];
30
+ }
31
+ if (!Array.isArray(raw)) {
32
+ throw new TypeError(`${label} must be an array of strings`);
33
+ }
34
+ return raw.map((item) => {
35
+ if (typeof item !== "string") {
36
+ throw new TypeError(`${label} actions must be strings`);
37
+ }
38
+ return item;
39
+ });
40
+ }
41
+
42
+ function parseTransition(raw, stateId, event) {
43
+ if (typeof raw === "string") {
44
+ return { target: raw, guard: null, action: [], internal: false };
45
+ }
46
+ const obj = assertObject(raw, `transition ${stateId}.${event}`);
47
+ const target = obj.target;
48
+ if (typeof target !== "string") {
49
+ throw new TypeError(`transition ${stateId}.${event} requires a string \`target\``);
50
+ }
51
+ let guard = null;
52
+ if (obj.guard !== undefined && obj.guard !== null) {
53
+ if (typeof obj.guard === "string") {
54
+ guard = obj.guard;
55
+ } else if (typeof obj.guard === "object" && obj.guard !== null && "expr" in obj.guard) {
56
+ throw new TypeError(
57
+ `transition ${stateId}.${event}: context-expression \`{expr: …}\` guards are not supported (rejecting explicitly per spec)`,
58
+ );
59
+ } else {
60
+ throw new TypeError(`transition ${stateId}.${event}: guard must be a string`);
61
+ }
62
+ }
63
+ const action = parseActionList(obj.action, `transition ${stateId}.${event} action`);
64
+ const internal = obj.internal === true;
65
+ return { target, guard, action, internal };
66
+ }
67
+
68
+ function parseState(id, raw) {
69
+ const obj = assertObject(raw, `state ${id}`);
70
+ const parent = typeof obj.parent === "string" ? obj.parent : null;
71
+ const initial = typeof obj.initial === "string" ? obj.initial : null;
72
+ const defaultTarget = typeof obj.default === "string" ? obj.default : null;
73
+
74
+ if (obj.run !== undefined && obj.run !== null) {
75
+ throw new TypeError(
76
+ `state ${id} uses \`run\` actions, which are not supported (rejecting explicitly per spec)`,
77
+ );
78
+ }
79
+
80
+ let kind;
81
+ let history = null;
82
+ if (typeof obj.history === "string") {
83
+ if (obj.history !== HISTORY_SHALLOW && obj.history !== HISTORY_DEEP) {
84
+ throw new TypeError(`state ${id}: unknown history kind \`${obj.history}\``);
85
+ }
86
+ history = obj.history;
87
+ kind = "history";
88
+ } else if (obj.parallel === true) {
89
+ kind = "parallel";
90
+ } else if (obj.kind === "final") {
91
+ kind = "final";
92
+ } else if (initial !== null) {
93
+ kind = "compound";
94
+ } else {
95
+ kind = "atomic";
96
+ }
97
+
98
+ const entry = parseActionList(obj.entry, `state ${id} entry`);
99
+ const exit = parseActionList(obj.exit, `state ${id} exit`);
100
+
101
+ const transitions = new Map();
102
+ if (obj.on !== undefined && obj.on !== null) {
103
+ const on = assertObject(obj.on, `state ${id}.on`);
104
+ for (const [event, rawT] of Object.entries(on)) {
105
+ transitions.set(event, parseTransition(rawT, id, event));
106
+ }
107
+ }
108
+
109
+ return { id, parent, kind, history, initial, default: defaultTarget, transitions, entry, exit };
110
+ }
111
+
112
+ function computeDepth(states, id, current, depth) {
113
+ depth.set(id, current);
114
+ for (const def of states.values()) {
115
+ if (def.parent === id) {
116
+ computeDepth(states, def.id, current + 1, depth);
117
+ }
118
+ }
119
+ }
120
+
121
+ /**
122
+ * A parsed, immutable chart definition.
123
+ */
124
+ export class ChartDef {
125
+ constructor({ states, children, order, depth, root }) {
126
+ this.states = states;
127
+ this.children = children;
128
+ this.order = order;
129
+ this.depth = depth;
130
+ this.root = root;
131
+ Object.freeze(this);
132
+ }
133
+
134
+ /**
135
+ * Parse a chart definition from the declarative JSON form
136
+ * (per `schemas/statechart.json`). Throws on malformed charts or unsupported
137
+ * features (`run` actions, `{expr: …}` guards).
138
+ *
139
+ * @param {any} value the declarative chart object
140
+ * @returns {ChartDef}
141
+ */
142
+ static fromChart(value) {
143
+ const obj = assertObject(value, "chart");
144
+ // `chart.initial` presence is validated; descent uses each compound's own
145
+ // `initial` from the root, so the value itself is not stored.
146
+ if (typeof obj.initial !== "string") {
147
+ throw new TypeError("chart.initial is required");
148
+ }
149
+ const statesObj = assertObject(obj.states, "chart.states");
150
+
151
+ const states = new Map();
152
+ const order = new Map();
153
+ let idx = 0;
154
+ for (const [id, raw] of Object.entries(statesObj)) {
155
+ order.set(id, idx);
156
+ idx += 1;
157
+ states.set(id, parseState(id, raw));
158
+ }
159
+
160
+ return ChartDef.fromStates(states, order);
161
+ }
162
+
163
+ /**
164
+ * Assemble a validated {@link ChartDef} from parsed states plus their document
165
+ * order. Shared by {@link ChartDef.fromChart} and the {@link ChartBuilder}, so
166
+ * both definition paths derive parent→children, the single parent-less root,
167
+ * and per-node depth identically. `order` maps each state id to the position
168
+ * that fixes deterministic parallel-region descent.
169
+ *
170
+ * @param {Map<string, any>} states id → parsed state def (each carrying `id`)
171
+ * @param {Map<string, number>} order id → document position
172
+ * @returns {ChartDef}
173
+ */
174
+ static fromStates(states, order) {
175
+ // Derived structure: children, root.
176
+ const children = new Map();
177
+ let root = null;
178
+ for (const def of states.values()) {
179
+ if (def.parent !== null) {
180
+ let list = children.get(def.parent);
181
+ if (!list) {
182
+ list = [];
183
+ children.set(def.parent, list);
184
+ }
185
+ list.push(def.id);
186
+ } else {
187
+ if (root !== null) {
188
+ throw new TypeError("chart has more than one root (parent-less state)");
189
+ }
190
+ root = def.id;
191
+ }
192
+ }
193
+ // Sort children by document order for deterministic parallel descent.
194
+ for (const list of children.values()) {
195
+ list.sort((a, b) => (order.get(a) ?? Infinity) - (order.get(b) ?? Infinity));
196
+ }
197
+ if (root === null) {
198
+ throw new TypeError("chart has no root (parent-less state)");
199
+ }
200
+
201
+ const depth = new Map();
202
+ computeDepth(states, root, 0, depth);
203
+
204
+ return new ChartDef({ states, children, order, depth, root });
205
+ }
206
+
207
+ kind(id) {
208
+ return this.states.get(id)?.kind ?? "atomic";
209
+ }
210
+
211
+ isLeaf(id) {
212
+ const k = this.kind(id);
213
+ return k === "atomic" || k === "final";
214
+ }
215
+
216
+ ancestorsInclusive(id) {
217
+ const out = [];
218
+ let cur = id;
219
+ while (cur !== null) {
220
+ const def = this.states.get(cur);
221
+ if (!def) break;
222
+ out.push(cur);
223
+ cur = def.parent;
224
+ }
225
+ return out;
226
+ }
227
+
228
+ lca(a, b) {
229
+ const ancA = new Set(this.ancestorsInclusive(a));
230
+ for (const cid of this.ancestorsInclusive(b)) {
231
+ if (ancA.has(cid)) {
232
+ return cid;
233
+ }
234
+ }
235
+ return this.root;
236
+ }
237
+
238
+ isProperDescendant(desc, anc) {
239
+ return desc !== anc && this.ancestorsInclusive(desc).includes(anc);
240
+ }
241
+
242
+ depthOf(id) {
243
+ return this.depth.get(id) ?? 0;
244
+ }
245
+ }
246
+
247
+ function enterSubtree(def, state, enter, actions) {
248
+ const node = def.states.get(state);
249
+ enter.add(state);
250
+ if (actions && node) {
251
+ for (const a of node.entry) actions.push(a);
252
+ }
253
+ if (!node) return;
254
+ switch (node.kind) {
255
+ case "compound":
256
+ if (node.initial !== null) {
257
+ enterSubtree(def, node.initial, enter, actions);
258
+ }
259
+ break;
260
+ case "parallel":
261
+ for (const region of def.children.get(state) ?? []) {
262
+ enterSubtree(def, region, enter, actions);
263
+ }
264
+ break;
265
+ default:
266
+ break;
267
+ }
268
+ }
269
+
270
+ /** Path from just-below `lca` down to `target` (exclusive lca, inclusive target). */
271
+ function pathBelow(def, lca, target) {
272
+ const chain = def.ancestorsInclusive(target); // [target, ..., root]
273
+ const idx = chain.findIndex((x) => x === lca);
274
+ const end = idx === -1 ? chain.length : idx;
275
+ return chain.slice(0, end).reverse(); // [child-of-lca, ..., target]
276
+ }
277
+
278
+ function historyChildOf(def, region) {
279
+ for (const k of def.children.get(region) ?? []) {
280
+ if (def.kind(k) === "history") return k;
281
+ }
282
+ return null;
283
+ }
284
+
285
+ function recordRegion(def, region, histChild, config, history) {
286
+ const histDef = def.states.get(histChild);
287
+ if (!histDef || histDef.kind !== "history") return;
288
+ if (histDef.history === HISTORY_SHALLOW) {
289
+ // Record the direct child of `region` that was active.
290
+ for (const c of def.children.get(region) ?? []) {
291
+ if (config.has(c) && def.kind(c) !== "history") {
292
+ history.set(histChild, { kind: "shallow", child: c });
293
+ return;
294
+ }
295
+ }
296
+ } else {
297
+ // Record every active state strictly below `region`.
298
+ const set = new Set();
299
+ for (const s of config) {
300
+ if (def.isProperDescendant(s, region)) set.add(s);
301
+ }
302
+ history.set(histChild, { kind: "deep", set });
303
+ }
304
+ }
305
+
306
+ function guardPasses(transition, guards) {
307
+ if (transition.guard === null) return true;
308
+ return guards.get(transition.guard) === true; // fail-closed
309
+ }
310
+
311
+ /**
312
+ * A native full-Harel state chart. The active configuration lives in a plain
313
+ * `Set` (lazily-js is a state-projection consumer with no reactive graph).
314
+ *
315
+ * Deterministic by construction (mirroring the Lean `StateChart.send` total
316
+ * function): a given `(chart, history, configuration, event, guards)` yields a
317
+ * unique result — the confluence guarantee every binding inherits by replaying
318
+ * the shared conformance fixtures.
319
+ */
320
+ export class StateChart {
321
+ #def;
322
+ #config;
323
+ #history;
324
+ #lastActions;
325
+
326
+ /**
327
+ * Enter the initial configuration by descending from the root via each
328
+ * compound's `initial` (and every region for parallel states). Initial entry
329
+ * actions are recorded and available via {@link lastActions}.
330
+ * @param {ChartDef} def
331
+ */
332
+ constructor(def) {
333
+ this.#def = def;
334
+ const enter = new Set();
335
+ const actions = [];
336
+ enterSubtree(def, def.root, enter, actions);
337
+ this.#config = enter;
338
+ this.#history = new Map();
339
+ this.#lastActions = actions;
340
+ }
341
+
342
+ /** Ordered action names fired by the initial entry or the most recent
343
+ * {@link send} (exit → transition → entry). */
344
+ lastActions() {
345
+ return [...this.#lastActions];
346
+ }
347
+
348
+ /** The full active configuration (active leaves plus all active ancestors),
349
+ * as a sorted array of state ids. */
350
+ configuration() {
351
+ return [...this.#config].sort();
352
+ }
353
+
354
+ /** Active atomic leaves, sorted (one per parallel region; one for single-region). */
355
+ activeLeaves() {
356
+ return [...this.#config]
357
+ .filter((id) => this.#def.isLeaf(id))
358
+ .sort();
359
+ }
360
+
361
+ /** Hierarchical "state-in" predicate: `true` iff `id` is active. */
362
+ matches(id) {
363
+ return this.#config.has(id);
364
+ }
365
+
366
+ /**
367
+ * Send an event. Returns `true` if any transition was taken, `false` if
368
+ * rejected (configuration unchanged, no actions fired). `guards` resolves
369
+ * named guards for this send (absent/unknown name → fail-closed `false`).
370
+ *
371
+ * @param {string} event
372
+ * @param {Record<string, boolean> | Map<string, boolean>} [guards]
373
+ * @returns {boolean}
374
+ */
375
+ send(event, guards = {}) {
376
+ const guardMap = guards instanceof Map ? guards : new Map(Object.entries(guards));
377
+ const config = this.#config;
378
+ const def = this.#def;
379
+
380
+ // 1. Enabled transitions: per active leaf, innermost passing match.
381
+ const leaves = [...config].filter((id) => def.isLeaf(id));
382
+ /** @type {{source: string, transition: object, leaf: string}[]} */
383
+ const candidates = [];
384
+ for (const leaf of leaves) {
385
+ for (const anc of def.ancestorsInclusive(leaf)) {
386
+ const stateDef = def.states.get(anc);
387
+ const t = stateDef?.transitions.get(event);
388
+ if (t && guardPasses(t, guardMap)) {
389
+ candidates.push({ source: anc, transition: t, leaf });
390
+ break; // innermost wins for this leaf's chain
391
+ }
392
+ }
393
+ }
394
+
395
+ if (candidates.length === 0) {
396
+ this.#lastActions = [];
397
+ return false;
398
+ }
399
+
400
+ // 2. Conflict resolution: order by source depth desc, then document order;
401
+ // take greedily, skipping any whose exit set intersects the taken union.
402
+ candidates.sort((a, b) => {
403
+ const byDepth = def.depthOf(b.source) - def.depthOf(a.source);
404
+ if (byDepth !== 0) return byDepth;
405
+ return (def.order.get(a.source) ?? Infinity) - (def.order.get(b.source) ?? Infinity);
406
+ });
407
+
408
+ const exitUnion = new Set();
409
+ const enterUnion = new Set();
410
+ const takenTransitions = [];
411
+ for (const cand of candidates) {
412
+ const { exitSet, enterSet } = computeExitEnter(def, cand.source, cand.transition, cand.leaf, config, this.#history);
413
+ let conflicts = false;
414
+ for (const s of exitSet) {
415
+ if (exitUnion.has(s)) {
416
+ conflicts = true;
417
+ break;
418
+ }
419
+ }
420
+ if (conflicts) continue;
421
+ for (const s of exitSet) exitUnion.add(s);
422
+ for (const s of enterSet) enterUnion.add(s);
423
+ takenTransitions.push(cand.transition);
424
+ }
425
+
426
+ if (takenTransitions.length === 0) {
427
+ this.#lastActions = [];
428
+ return false;
429
+ }
430
+
431
+ // 3. Record history for regions being exited that own a history child.
432
+ for (const s of exitUnion) {
433
+ const hChild = historyChildOf(def, s);
434
+ if (hChild) {
435
+ recordRegion(def, s, hChild, config, this.#history);
436
+ }
437
+ }
438
+
439
+ // 4. Action trace: exit (innermost-first) → transition → entry (outermost-first).
440
+ const actions = [];
441
+ const exitSorted = [...exitUnion].sort((a, b) => {
442
+ const byDepth = def.depthOf(b) - def.depthOf(a); // depth desc
443
+ if (byDepth !== 0) return byDepth;
444
+ return a < b ? -1 : a > b ? 1 : 0; // alphabetical for stable tie-break
445
+ });
446
+ for (const s of exitSorted) {
447
+ for (const a of def.states.get(s)?.exit ?? []) actions.push(a);
448
+ }
449
+ for (const t of takenTransitions) {
450
+ for (const a of t.action) actions.push(a);
451
+ }
452
+ const enterSorted = [...enterUnion].sort((a, b) => {
453
+ const byDepth = def.depthOf(a) - def.depthOf(b); // depth asc
454
+ if (byDepth !== 0) return byDepth;
455
+ return a < b ? -1 : a > b ? 1 : 0;
456
+ });
457
+ for (const s of enterSorted) {
458
+ for (const a of def.states.get(s)?.entry ?? []) actions.push(a);
459
+ }
460
+
461
+ // 5. Apply new configuration.
462
+ for (const s of exitUnion) config.delete(s);
463
+ for (const s of enterUnion) config.add(s);
464
+
465
+ this.#lastActions = actions;
466
+ return true;
467
+ }
468
+ }
469
+
470
+ function computeExitEnter(def, source, transition, leaf, config, history) {
471
+ const target = transition.target;
472
+ const internal =
473
+ transition.internal && (target === source || def.isProperDescendant(target, source));
474
+ const lca = internal ? source : def.lca(leaf, target);
475
+
476
+ // Exit set: active proper-descendants of the lca.
477
+ const exitSet = new Set();
478
+ for (const s of config) {
479
+ if (def.isProperDescendant(s, lca)) exitSet.add(s);
480
+ }
481
+
482
+ // Enter set.
483
+ const enter = new Set();
484
+ if (def.kind(target) === "history") {
485
+ const targetDef = def.states.get(target);
486
+ const region = targetDef?.parent ?? def.root;
487
+ for (const s of pathBelow(def, lca, region)) enter.add(s);
488
+ restoreViaHistory(def, history, target, region, enter);
489
+ } else {
490
+ for (const s of pathBelow(def, lca, target)) enter.add(s);
491
+ enterSubtree(def, target, enter, null);
492
+ }
493
+
494
+ return { exitSet, enterSet: enter };
495
+ }
496
+
497
+ function restoreViaHistory(def, history, hist, region, enter) {
498
+ const recording = history.get(hist);
499
+ if (!recording) {
500
+ // First entry: descend via `default`, else the region's `initial`.
501
+ const histDef = def.states.get(hist);
502
+ const regionDef = def.states.get(region);
503
+ const start = histDef?.default ?? regionDef?.initial ?? null;
504
+ if (start !== null) {
505
+ for (const s of pathBelow(def, region, start)) enter.add(s);
506
+ enterSubtree(def, start, enter, null);
507
+ }
508
+ return;
509
+ }
510
+ if (recording.kind === "shallow") {
511
+ const child = recording.child;
512
+ enter.add(child);
513
+ enterSubtree(def, child, enter, null);
514
+ } else {
515
+ for (const s of recording.set) enter.add(s);
516
+ }
517
+ }
518
+
519
+ // -- Typed builder ------------------------------------------------------------
520
+ // Define a ChartDef in typed JS instead of (or alongside) the declarative JSON
521
+ // form. Both paths funnel through ChartDef.fromStates, so a built chart is
522
+ // behaviourally identical to the same chart parsed via ChartDef.fromChart.
523
+
524
+ /** A single transition, built in JS. Equivalent to a JSON transition object. */
525
+ export class TransitionBuilder {
526
+ /** @param {string} target external transition target */
527
+ constructor(target) {
528
+ this._t = { target, guard: null, action: [], internal: false };
529
+ }
530
+
531
+ /** An external transition to `target` with no guard or actions. */
532
+ static to(target) {
533
+ return new TransitionBuilder(target);
534
+ }
535
+
536
+ /** Attach a named boolean guard (resolved at send time; absent → false). */
537
+ guard(name) {
538
+ this._t.guard = name;
539
+ return this;
540
+ }
541
+
542
+ /** Append a transition action name. */
543
+ action(name) {
544
+ this._t.action.push(name);
545
+ return this;
546
+ }
547
+
548
+ /** Mark this transition internal (no exit/re-entry when target is source or a descendant). */
549
+ internal() {
550
+ this._t.internal = true;
551
+ return this;
552
+ }
553
+ }
554
+
555
+ /** A single chart state, built in JS. Mirrors the JSON state object. */
556
+ export class StateBuilder {
557
+ constructor(id, kind, { initial = null, history = null } = {}) {
558
+ this.id = id;
559
+ this._def = {
560
+ id,
561
+ parent: null,
562
+ kind,
563
+ history,
564
+ initial,
565
+ default: null,
566
+ transitions: new Map(),
567
+ entry: [],
568
+ exit: [],
569
+ };
570
+ }
571
+
572
+ /** An atomic leaf state. */
573
+ static atomic(id) {
574
+ return new StateBuilder(id, "atomic");
575
+ }
576
+
577
+ /** A compound state with the given initial child. */
578
+ static compound(id, initial) {
579
+ return new StateBuilder(id, "compound", { initial });
580
+ }
581
+
582
+ /** A parallel (orthogonal) state; all child regions are entered together. */
583
+ static parallel(id) {
584
+ return new StateBuilder(id, "parallel");
585
+ }
586
+
587
+ /** A final leaf state (accepted as a leaf; raises no completion event). */
588
+ static final(id) {
589
+ return new StateBuilder(id, "final");
590
+ }
591
+
592
+ /** A shallow-history pseudostate for its parent region. */
593
+ static historyShallow(id) {
594
+ return new StateBuilder(id, "history", { history: "shallow" });
595
+ }
596
+
597
+ /** A deep-history pseudostate for its parent region. */
598
+ static historyDeep(id) {
599
+ return new StateBuilder(id, "history", { history: "deep" });
600
+ }
601
+
602
+ /** Set the parent state id. Omit only for the single chart root. */
603
+ parent(parent) {
604
+ this._def.parent = parent;
605
+ return this;
606
+ }
607
+
608
+ /** Set the default target used on a history pseudostate's first entry. */
609
+ defaultChild(target) {
610
+ this._def.default = target;
611
+ return this;
612
+ }
613
+
614
+ /** Append an entry action name. */
615
+ entry(action) {
616
+ this._def.entry.push(action);
617
+ return this;
618
+ }
619
+
620
+ /** Append an exit action name. */
621
+ exit(action) {
622
+ this._def.exit.push(action);
623
+ return this;
624
+ }
625
+
626
+ /** Add an unguarded external transition on `event` to `target`. */
627
+ on(event, target) {
628
+ return this.onTransition(event, TransitionBuilder.to(target));
629
+ }
630
+
631
+ /** Add a guarded external transition on `event` to `target`. */
632
+ onGuarded(event, target, guard) {
633
+ return this.onTransition(event, TransitionBuilder.to(target).guard(guard));
634
+ }
635
+
636
+ /** Add a fully-specified transition on `event`. */
637
+ onTransition(event, transitionBuilder) {
638
+ this._def.transitions.set(event, transitionBuilder._t);
639
+ return this;
640
+ }
641
+ }
642
+
643
+ /**
644
+ * Fluent builder assembling a {@link ChartDef} from typed JS states — the
645
+ * definition path parallel to {@link ChartDef.fromChart}. State insertion order
646
+ * fixes deterministic parallel-region descent, exactly as JSON key order does.
647
+ */
648
+ export class ChartBuilder {
649
+ constructor() {
650
+ this._states = [];
651
+ }
652
+
653
+ /** Add a state. The first parent-less state added becomes the root. */
654
+ state(stateBuilder) {
655
+ this._states.push(stateBuilder);
656
+ return this;
657
+ }
658
+
659
+ /**
660
+ * Validate and assemble the {@link ChartDef}. Throws on a duplicate state id,
661
+ * or on zero / more than one parent-less root, matching the JSON path.
662
+ * @returns {ChartDef}
663
+ */
664
+ build() {
665
+ const states = new Map();
666
+ const order = new Map();
667
+ let idx = 0;
668
+ for (const sb of this._states) {
669
+ if (states.has(sb.id)) {
670
+ throw new TypeError(`duplicate state id \`${sb.id}\``);
671
+ }
672
+ order.set(sb.id, idx);
673
+ idx += 1;
674
+ states.set(sb.id, sb._def);
675
+ }
676
+ return ChartDef.fromStates(states, order);
677
+ }
678
+ }
@@ -0,0 +1,23 @@
1
+ export class OpId {
2
+ readonly counter: number;
3
+ readonly peer: number;
4
+ constructor(counter: number, peer: number);
5
+ compareTo(other: OpId): number;
6
+ }
7
+
8
+ export class TextCrdt {
9
+ constructor(peer: number);
10
+ static fromStr(peer: number, str: string): TextCrdt;
11
+ fork(peer: number): TextCrdt;
12
+ clone(): TextCrdt;
13
+ insert(index: number, ch: string): void;
14
+ insertStr(index: number, str: string): void;
15
+ delete(index: number): void;
16
+ text(): string;
17
+ len(): number;
18
+ is_empty(): boolean;
19
+ tombstoneCount(): number;
20
+ clock(): OpId;
21
+ merge(other: TextCrdt): boolean;
22
+ gcWith(isStable: (deleteOpId: OpId) => boolean): number;
23
+ }