@kestack/kabuki 0.0.0-stage → 0.0.1

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.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +274 -2
  3. package/dist/core/actor/actor.d.ts +42 -0
  4. package/dist/core/actor/actor.d.ts.map +1 -0
  5. package/dist/core/actor/actor.js +168 -0
  6. package/dist/core/actor/command.d.ts +24 -0
  7. package/dist/core/actor/command.d.ts.map +1 -0
  8. package/dist/core/actor/command.js +31 -0
  9. package/dist/core/actor/index.d.ts +6 -0
  10. package/dist/core/actor/index.d.ts.map +1 -0
  11. package/dist/core/actor/index.js +3 -0
  12. package/dist/core/actor/types.d.ts +295 -0
  13. package/dist/core/actor/types.d.ts.map +1 -0
  14. package/dist/core/actor/types.js +2 -0
  15. package/dist/core/history/history.d.ts +67 -0
  16. package/dist/core/history/history.d.ts.map +1 -0
  17. package/dist/core/history/history.js +145 -0
  18. package/dist/core/history/index.d.ts +5 -0
  19. package/dist/core/history/index.d.ts.map +1 -0
  20. package/dist/core/history/index.js +2 -0
  21. package/dist/core/history/replay.d.ts +34 -0
  22. package/dist/core/history/replay.d.ts.map +1 -0
  23. package/dist/core/history/replay.js +82 -0
  24. package/dist/core/history/saved.d.ts +7 -0
  25. package/dist/core/history/saved.d.ts.map +1 -0
  26. package/dist/core/history/saved.js +126 -0
  27. package/dist/core/index.d.ts +9 -0
  28. package/dist/core/index.d.ts.map +1 -0
  29. package/dist/core/index.js +4 -0
  30. package/dist/core/runtime/index.d.ts +4 -0
  31. package/dist/core/runtime/index.d.ts.map +1 -0
  32. package/dist/core/runtime/index.js +1 -0
  33. package/dist/core/runtime/instance/command.d.ts +4 -0
  34. package/dist/core/runtime/instance/command.d.ts.map +1 -0
  35. package/dist/core/runtime/instance/command.js +12 -0
  36. package/dist/core/runtime/instance/index.d.ts +4 -0
  37. package/dist/core/runtime/instance/index.d.ts.map +1 -0
  38. package/dist/core/runtime/instance/index.js +1 -0
  39. package/dist/core/runtime/instance/instance.d.ts +16 -0
  40. package/dist/core/runtime/instance/instance.d.ts.map +1 -0
  41. package/dist/core/runtime/instance/instance.js +113 -0
  42. package/dist/core/runtime/instance/self.d.ts +35 -0
  43. package/dist/core/runtime/instance/self.d.ts.map +1 -0
  44. package/dist/core/runtime/instance/self.js +42 -0
  45. package/dist/core/runtime/instance/worker.d.ts +16 -0
  46. package/dist/core/runtime/instance/worker.d.ts.map +1 -0
  47. package/dist/core/runtime/instance/worker.js +52 -0
  48. package/dist/core/runtime/mailbox.d.ts +10 -0
  49. package/dist/core/runtime/mailbox.d.ts.map +1 -0
  50. package/dist/core/runtime/mailbox.js +64 -0
  51. package/dist/core/runtime/runtime.d.ts +36 -0
  52. package/dist/core/runtime/runtime.d.ts.map +1 -0
  53. package/dist/core/runtime/runtime.js +93 -0
  54. package/dist/core/runtime/time.d.ts +11 -0
  55. package/dist/core/runtime/time.d.ts.map +1 -0
  56. package/dist/core/runtime/time.js +22 -0
  57. package/dist/core/snapshot/handle.d.ts +14 -0
  58. package/dist/core/snapshot/handle.d.ts.map +1 -0
  59. package/dist/core/snapshot/handle.js +70 -0
  60. package/dist/core/snapshot/index.d.ts +8 -0
  61. package/dist/core/snapshot/index.d.ts.map +1 -0
  62. package/dist/core/snapshot/index.js +4 -0
  63. package/dist/core/snapshot/invoke.d.ts +17 -0
  64. package/dist/core/snapshot/invoke.d.ts.map +1 -0
  65. package/dist/core/snapshot/invoke.js +119 -0
  66. package/dist/core/snapshot/restore.d.ts +16 -0
  67. package/dist/core/snapshot/restore.d.ts.map +1 -0
  68. package/dist/core/snapshot/restore.js +63 -0
  69. package/dist/core/snapshot/snapshot.d.ts +61 -0
  70. package/dist/core/snapshot/snapshot.d.ts.map +1 -0
  71. package/dist/core/snapshot/snapshot.js +4 -0
  72. package/dist/core/snapshot/tree.d.ts +26 -0
  73. package/dist/core/snapshot/tree.d.ts.map +1 -0
  74. package/dist/core/snapshot/tree.js +77 -0
  75. package/dist/react/app.d.ts +49 -0
  76. package/dist/react/app.d.ts.map +1 -0
  77. package/dist/react/app.js +214 -0
  78. package/dist/react/child.d.ts +32 -0
  79. package/dist/react/child.d.ts.map +1 -0
  80. package/dist/react/child.js +80 -0
  81. package/dist/react/drawn.d.ts +4 -0
  82. package/dist/react/drawn.d.ts.map +1 -0
  83. package/dist/react/drawn.js +118 -0
  84. package/dist/react/index.d.ts +7 -0
  85. package/dist/react/index.d.ts.map +1 -0
  86. package/dist/react/index.js +3 -0
  87. package/dist/react/registry.d.ts +12 -0
  88. package/dist/react/registry.d.ts.map +1 -0
  89. package/dist/react/registry.js +25 -0
  90. package/dist/react/view.d.ts +80 -0
  91. package/dist/react/view.d.ts.map +1 -0
  92. package/dist/react/view.js +47 -0
  93. package/package.json +55 -3
@@ -0,0 +1,64 @@
1
+ /*
2
+ * The one Mailbox. A Message is stamped with its Time and handled before
3
+ * `send` returns, unless another is being handled (a Worker sending as it
4
+ * starts, say): then it waits its turn, so every Message sees a settled app
5
+ * and Times only grow along the queue. Subscribers are told once the queue
6
+ * is empty: many Messages make one render. A Message sent while the Mailbox
7
+ * is closed is dropped. One whose Update gives data that does not match its
8
+ * Schemas changes nothing; the rest of the queue is still handled, then the
9
+ * `send` that drained it throws.
10
+ */
11
+ export const makeMailbox = (now, deliver, drained) => {
12
+ const waiting = [];
13
+ let open = false;
14
+ let busy = false;
15
+ const drain = () => {
16
+ if (busy)
17
+ return;
18
+ busy = true;
19
+ let failed;
20
+ try {
21
+ for (let next = waiting.shift(); next; next = waiting.shift())
22
+ try {
23
+ deliver(next);
24
+ }
25
+ catch (error) {
26
+ failed ??= { error };
27
+ }
28
+ }
29
+ finally {
30
+ busy = false;
31
+ }
32
+ drained();
33
+ if (failed)
34
+ throw failed.error;
35
+ };
36
+ return {
37
+ open: () => {
38
+ open = true;
39
+ },
40
+ close: () => {
41
+ open = false;
42
+ waiting.length = 0;
43
+ },
44
+ send: (destination, message, source) => {
45
+ if (!open)
46
+ return;
47
+ waiting.push({ destination, message, source, at: now() });
48
+ drain();
49
+ },
50
+ /** Run `f` without handling Messages, then handle what it sent. */
51
+ hold: (f) => {
52
+ const draining = busy;
53
+ busy = true;
54
+ try {
55
+ f();
56
+ }
57
+ finally {
58
+ busy = draining;
59
+ if (!draining)
60
+ drain();
61
+ }
62
+ },
63
+ };
64
+ };
@@ -0,0 +1,36 @@
1
+ import { Effect, Layer } from 'effect';
2
+ import type { Scope } from 'effect';
3
+ import type { AnyActor, Tagged, Types } from '../actor/index.ts';
4
+ import type { Snapshot, Source } from '../snapshot/index.ts';
5
+ import { History } from '../history/index.ts';
6
+ import type { WorkerStatus } from './instance/index.ts';
7
+ /** Capabilities an Actor's tree still needs from the app's Layer. */
8
+ export type Needs<A> = Types<A>['open'];
9
+ /** The app's Layer, which must cover every Capability the tree still needs. */
10
+ type LayerFor<A> = [Needs<A>] extends [never] ? [layer?: Layer.Layer<never>] : [layer: Layer.Layer<Needs<A>>];
11
+ export interface Running<A> {
12
+ readonly history: History<A>;
13
+ /** The live Snapshot; the last one while stopped. */
14
+ readonly snapshot: () => Snapshot;
15
+ readonly running: () => boolean;
16
+ /** The Time now, in milliseconds. It stands still while stopped. */
17
+ readonly now: () => number;
18
+ /** Hand a Message to any Instance, from outside the tree unless said otherwise. Dropped while stopped. */
19
+ readonly send: (instance: string, message: Tagged & {
20
+ readonly [field: string]: unknown;
21
+ }, source?: Source) => void;
22
+ /** Called once the Mailbox is empty, and on every start and stop. */
23
+ readonly subscribe: (listener: () => void) => () => void;
24
+ /** What an Instance's Workers are doing now. Empty while stopped. */
25
+ readonly workers: (instance: string) => ReadonlyArray<WorkerStatus>;
26
+ /** Start at the Head, or move the Head to an entry first (`null`: init). Stops first if running. */
27
+ readonly start: (from?: number | null) => Effect.Effect<void>;
28
+ readonly stop: () => Effect.Effect<void>;
29
+ }
30
+ declare const start: <A extends AnyActor>(from: A | History<A>, ...layer: LayerFor<A>) => Effect.Effect<Running<A>, never, Scope.Scope>;
31
+ /** Runs an Actor, or carries a History on, until its Scope closes. */
32
+ export declare const Runtime: {
33
+ start: typeof start;
34
+ };
35
+ export {};
36
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../../src/core/runtime/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,MAAM,EAAS,KAAK,EAAE,MAAM,QAAQ,CAAC;AAC9D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAEjE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,OAAO,EAAa,MAAM,qBAAqB,CAAC;AAEzD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAoBxD,qEAAqE;AACrE,MAAM,MAAM,KAAK,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;AAExC,+EAA+E;AAC/E,KAAK,QAAQ,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GACzC,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAC5B,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAEnC,MAAM,WAAW,OAAO,CAAC,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAC7B,qDAAqD;IACrD,QAAQ,CAAC,QAAQ,EAAE,MAAM,QAAQ,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC;IAChC,oEAAoE;IACpE,QAAQ,CAAC,GAAG,EAAE,MAAM,MAAM,CAAC;IAC3B,0GAA0G;IAC1G,QAAQ,CAAC,IAAI,EAAE,CACb,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GAAG;QAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,EACvD,MAAM,CAAC,EAAE,MAAM,KACZ,IAAI,CAAC;IACV,qEAAqE;IACrE,QAAQ,CAAC,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAC;IACzD,qEAAqE;IACrE,QAAQ,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,aAAa,CAAC,YAAY,CAAC,CAAC;IACpE,oGAAoG;IACpG,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC9D,QAAQ,CAAC,IAAI,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;CAC1C;AAED,QAAA,MAAM,KAAK,GAAI,CAAC,SAAS,QAAQ,QACzB,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,YACV,QAAQ,CAAC,CAAC,CAAC,KACpB,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAgH3C,CAAC;AAEL,sEAAsE;AACtE,eAAO,MAAM,OAAO;;CAAY,CAAC"}
@@ -0,0 +1,93 @@
1
+ import { Clock, Context, Effect, Fiber, Layer } from 'effect';
2
+ import { actorAt, isRestore } from '../snapshot/index.js';
3
+ import { History, isHistory } from '../history/index.js';
4
+ import { ActorInstance } from './instance/index.js';
5
+ import { makeMailbox } from './mailbox.js';
6
+ import { makeTime } from './time.js';
7
+ const start = (from, ...layer) => Effect.gen(function* () {
8
+ const history = isHistory(from) ? from : History.make(from);
9
+ const root = history.actor.definition;
10
+ const scope = yield* Effect.scope;
11
+ const clock = yield* Clock.Clock;
12
+ const built = yield* Layer.build(layer[0] ?? Layer.empty);
13
+ const services = Context.add(built, Clock.Clock, clock);
14
+ const time = makeTime(() => clock.monotonicTimeNanosUnsafe());
15
+ const listeners = new Set();
16
+ const notify = () => listeners.forEach((listener) => listener());
17
+ let snapshot;
18
+ let instance;
19
+ let release = () => { };
20
+ const mailbox = makeMailbox(time.now, (envelope) => {
21
+ const handled = instance.receive(envelope);
22
+ snapshot = handled.snapshot;
23
+ history.append(envelope, handled);
24
+ }, notify);
25
+ // 2. Send
26
+ const send = (id, message, source) => {
27
+ if (!isRestore(message))
28
+ actorAt(root, snapshot, id)?.check.message(message, `Message ${JSON.stringify(message._tag)}`);
29
+ mailbox.send(id, message, source);
30
+ };
31
+ // 1. Start
32
+ const go = Effect.gen(function* () {
33
+ snapshot = yield* history.seek();
34
+ time.start(history.branch().at(-1)?.at ?? 0);
35
+ release = history.lock();
36
+ mailbox.open();
37
+ mailbox.hold(() => {
38
+ instance = ActorInstance.start(root, snapshot, { send, now: time.now, services }, scope);
39
+ });
40
+ notify();
41
+ });
42
+ // 3. Stop
43
+ const halt = Effect.suspend(() => {
44
+ if (!instance)
45
+ return Effect.void;
46
+ const stopping = instance;
47
+ instance = undefined;
48
+ mailbox.close();
49
+ time.stop();
50
+ release();
51
+ notify();
52
+ return stopping.stop();
53
+ });
54
+ const restart = (from) => Effect.gen(function* () {
55
+ yield* halt;
56
+ if (from !== undefined)
57
+ history.moveHead(from);
58
+ yield* go;
59
+ });
60
+ /** A new start or stop interrupts the one still under way. */
61
+ let current;
62
+ let closed = false;
63
+ const latest = (effect) => Effect.suspend(() => {
64
+ if (closed)
65
+ return Effect.void;
66
+ current?.interruptUnsafe();
67
+ const fiber = Effect.runForkWith(services)(effect);
68
+ current = fiber;
69
+ return Fiber.join(fiber);
70
+ });
71
+ yield* go;
72
+ yield* Effect.addFinalizer(() => Effect.suspend(() => {
73
+ closed = true;
74
+ current?.interruptUnsafe();
75
+ return halt;
76
+ }));
77
+ return {
78
+ history,
79
+ snapshot: () => snapshot,
80
+ running: () => instance !== undefined,
81
+ now: time.now,
82
+ send: (id, message, source = { kind: 'outside' }) => send(id, message, source),
83
+ subscribe: (listener) => {
84
+ listeners.add(listener);
85
+ return () => listeners.delete(listener);
86
+ },
87
+ workers: (id) => instance?.workers(id) ?? [],
88
+ start: (from) => latest(restart(from)),
89
+ stop: () => latest(halt),
90
+ };
91
+ });
92
+ /** Runs an Actor, or carries a History on, until its Scope closes. */
93
+ export const Runtime = { start };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The app's Time, in milliseconds: a starting Time plus what the clock has run
3
+ * since. Stopped, it stands still, and time spent stopped is never counted.
4
+ */
5
+ export declare const makeTime: (nanos: () => bigint) => {
6
+ now: () => number;
7
+ /** Carry on from `from`, counting from now. */
8
+ start: (from: number) => void;
9
+ stop: () => void;
10
+ };
11
+ //# sourceMappingURL=time.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"time.d.ts","sourceRoot":"","sources":["../../../src/core/runtime/time.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,QAAQ,UAAW,MAAM,MAAM;;IAOxC,+CAA+C;kBACjC,MAAM;;CASvB,CAAC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The app's Time, in milliseconds: a starting Time plus what the clock has run
3
+ * since. Stopped, it stands still, and time spent stopped is never counted.
4
+ */
5
+ export const makeTime = (nanos) => {
6
+ let base = 0;
7
+ let origin = nanos();
8
+ let stoppedAt = 0;
9
+ const now = () => stoppedAt ?? base + Number(nanos() - origin) / 1_000_000;
10
+ return {
11
+ now,
12
+ /** Carry on from `from`, counting from now. */
13
+ start: (from) => {
14
+ base = from;
15
+ origin = nanos();
16
+ stoppedAt = undefined;
17
+ },
18
+ stop: () => {
19
+ stoppedAt = now();
20
+ },
21
+ };
22
+ };
@@ -0,0 +1,14 @@
1
+ import type { ActorDefinition } from '../actor/index.ts';
2
+ import type { Envelope, Snapshot } from './snapshot.ts';
3
+ /** What handling one Envelope did. Never kept in the History: handling it again says the same. */
4
+ export interface Handled {
5
+ readonly outcome: 'handled' | 'ignored' | 'dropped';
6
+ /** The State tag before and after; empty when dropped. */
7
+ readonly from: string;
8
+ readonly to: string;
9
+ readonly snapshot: Snapshot;
10
+ /** The Commands the Update asked for, to run in the Instance it was for. */
11
+ readonly commands: ReadonlyArray<unknown>;
12
+ }
13
+ export declare const handle: (root: ActorDefinition, snapshot: Snapshot, { destination: id, message, at }: Envelope) => Handled;
14
+ //# sourceMappingURL=handle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handle.d.ts","sourceRoot":"","sources":["../../../src/core/snapshot/handle.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAGzD,OAAO,KAAK,EAAE,QAAQ,EAAY,QAAQ,EAAE,MAAM,eAAe,CAAC;AAsBlE,kGAAkG;AAClG,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;IACpD,0DAA0D;IAC1D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,4EAA4E;IAC5E,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;CAC3C;AAQD,eAAO,MAAM,MAAM,SACX,eAAe,YACX,QAAQ,oCACgB,QAAQ,KACzC,OA6CF,CAAC"}
@@ -0,0 +1,70 @@
1
+ import { Array as Arr } from 'effect';
2
+ import { enter, follow } from './invoke.js';
3
+ import { checkRestore, isRestore } from './restore.js';
4
+ import { pathTo, replace } from './tree.js';
5
+ const nothing = (snapshot, outcome, tag = '') => ({ outcome, from: tag, to: tag, snapshot, commands: [] });
6
+ export const handle = (root, snapshot, { destination: id, message, at }) => {
7
+ if (isRestore(message))
8
+ return restored(root, snapshot, id, message);
9
+ // 1. Find
10
+ const path = pathTo(root, snapshot, id);
11
+ if (!path)
12
+ return nothing(snapshot, 'dropped');
13
+ const { instance, definition } = path.at(-1);
14
+ const from = instance.state._tag;
15
+ // 2. Update
16
+ const rule = (definition.update[from]?.[message._tag] ??
17
+ definition.update['*']?.[message._tag]);
18
+ if (!rule)
19
+ return nothing(snapshot, 'ignored', from);
20
+ const next = rule(message, {
21
+ model: instance.model,
22
+ state: instance.state,
23
+ props: instance.props,
24
+ at,
25
+ });
26
+ const by = `the Update for ${JSON.stringify(message._tag)} in ${from} gave ${id}`;
27
+ if (next.model !== undefined && next.model !== instance.model)
28
+ definition.check.model(next.model, `the Model ${by}`);
29
+ if (next.state !== undefined && next.state !== instance.state)
30
+ definition.check.state(next.state, `the State ${by}`);
31
+ // 3. Apply
32
+ const model = next.model ?? instance.model;
33
+ const state = next.state ?? instance.state;
34
+ const changed = model !== instance.model || state !== instance.state;
35
+ const after = !changed
36
+ ? instance
37
+ : state._tag !== from
38
+ ? enter(definition, { ...instance, model, state })
39
+ : follow(definition, { ...instance, model, state });
40
+ // 4. Replace
41
+ return {
42
+ outcome: 'handled',
43
+ from,
44
+ to: state._tag,
45
+ snapshot: after === instance ? snapshot : replace(path, after),
46
+ commands: next.command === undefined ? [] : Arr.ensure(next.command),
47
+ };
48
+ };
49
+ const warned = new WeakSet();
50
+ const restored = (root, snapshot, id, message) => {
51
+ const from = snapshot.state._tag;
52
+ if (id !== snapshot.id)
53
+ return nothing(snapshot, 'dropped');
54
+ const checked = checkRestore(root, message);
55
+ if ('reason' in checked) {
56
+ if (!warned.has(message)) {
57
+ warned.add(message);
58
+ console.warn(`[@kestack/kabuki] a Restore no longer matches ${root.name}, and is ignored: ${checked.reason}`);
59
+ }
60
+ return nothing(snapshot, 'ignored', from);
61
+ }
62
+ const after = checked.snapshot;
63
+ return {
64
+ outcome: 'handled',
65
+ from,
66
+ to: after.state._tag,
67
+ snapshot: after,
68
+ commands: [],
69
+ };
70
+ };
@@ -0,0 +1,8 @@
1
+ export { init } from './snapshot.ts';
2
+ export type { Envelope, Instance, Snapshot, Source } from './snapshot.ts';
3
+ export { handle } from './handle.ts';
4
+ export type { Handled } from './handle.ts';
5
+ export { actorAt, instanceAt, isMany, within } from './tree.ts';
6
+ export { RESTORE, isRestore, restore } from './restore.ts';
7
+ export type { Restore } from './restore.ts';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/snapshot/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AACrC,YAAY,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC1E,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAChE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC3D,YAAY,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { init } from './snapshot.js';
2
+ export { handle } from './handle.js';
3
+ export { actorAt, instanceAt, isMany, within } from './tree.js';
4
+ export { RESTORE, isRestore, restore } from './restore.js';
@@ -0,0 +1,17 @@
1
+ import type { ActorDefinition } from '../actor/index.ts';
2
+ import type { Instance } from './snapshot.ts';
3
+ /** What a parent hands one Child: its Input, read once by init, and its Props, read live. */
4
+ type Given = {
5
+ readonly input?: unknown;
6
+ readonly props?: unknown;
7
+ };
8
+ export declare const create: (definition: ActorDefinition, id: string, key: string | undefined, { input, props }: Given) => Instance;
9
+ /**
10
+ * Enter the Instance's State: every Child the State writes is Invoked fresh.
11
+ * A Child written at the top lasts through the Transition; then every Child
12
+ * follows the parent's data.
13
+ */
14
+ export declare const enter: (definition: ActorDefinition, instance: Instance) => Instance;
15
+ export declare const follow: (definition: ActorDefinition, instance: Instance) => Instance;
16
+ export {};
17
+ //# sourceMappingURL=invoke.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invoke.d.ts","sourceRoot":"","sources":["../../../src/core/snapshot/invoke.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAGzD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAmB9C,6FAA6F;AAC7F,KAAK,KAAK,GAAG;IAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAMpE,eAAO,MAAM,MAAM,eACL,eAAe,MACvB,MAAM,OACL,MAAM,GAAG,SAAS,oBACL,KAAK,KACtB,QAkBF,CAAC;AAGF;;;;GAIG;AACH,eAAO,MAAM,KAAK,eACJ,eAAe,YACjB,QAAQ,KACjB,QA0BF,CAAC;AAGF,eAAO,MAAM,MAAM,eACL,eAAe,YACjB,QAAQ,KACjB,QAmCF,CAAC"}
@@ -0,0 +1,119 @@
1
+ import { Equal } from 'effect';
2
+ import { childId, counterOf } from './tree.js';
3
+ const SINGLE = { _tag: 'Single' };
4
+ // 1. Create
5
+ export const create = (definition, id, key, { input, props }) => {
6
+ definition.check.input(input, `the Input of ${id}`);
7
+ definition.check.props(props, `the Props of ${id}`);
8
+ const init = definition.init(input);
9
+ const model = init.model ?? {};
10
+ const state = init.state ?? SINGLE;
11
+ definition.check.model(model, `the Model init gave ${id}`);
12
+ definition.check.state(state, `the State init gave ${id}`);
13
+ return enter(definition, {
14
+ id,
15
+ actor: definition.name,
16
+ ...(key === undefined ? {} : { key }),
17
+ model,
18
+ state,
19
+ ...(props === undefined ? {} : { props }),
20
+ children: {},
21
+ invoked: {},
22
+ });
23
+ };
24
+ // 2. Enter
25
+ /**
26
+ * Enter the Instance's State: every Child the State writes is Invoked fresh.
27
+ * A Child written at the top lasts through the Transition; then every Child
28
+ * follows the parent's data.
29
+ */
30
+ export const enter = (definition, instance) => {
31
+ const slots = definition.children[instance.state._tag];
32
+ if (!slots)
33
+ return { ...instance, children: {} };
34
+ const invocation = invocationOf(definition, instance);
35
+ const invoked = { ...instance.invoked };
36
+ const children = {};
37
+ for (const [slot, { definition: child, many }] of Object.entries(slots)) {
38
+ const kept = definition.lasting.includes(slot)
39
+ ? instance.children[slot]
40
+ : undefined;
41
+ children[slot] =
42
+ kept ??
43
+ (many
44
+ ? keyedIn(invocation, slot).map((given) => invoke(child, instance.id, slot, given.key, given, invoked))
45
+ : invoke(child, instance.id, slot, undefined, givenIn(invocation, slot), invoked));
46
+ }
47
+ return follow(definition, { ...instance, children, invoked });
48
+ };
49
+ // 3. Follow
50
+ export const follow = (definition, instance) => {
51
+ const slots = Object.entries(definition.children[instance.state._tag] ?? {});
52
+ if (slots.length === 0)
53
+ return instance;
54
+ const invocation = invocationOf(definition, instance);
55
+ let invoked;
56
+ let children;
57
+ for (const [slot, { definition: child, many }] of slots) {
58
+ if (!many) {
59
+ const held = instance.children[slot];
60
+ if (!held)
61
+ continue;
62
+ const handed = handedProps(child, held, givenIn(invocation, slot).props);
63
+ if (handed !== held)
64
+ (children ??= { ...instance.children })[slot] = handed;
65
+ continue;
66
+ }
67
+ const before = (instance.children[slot] ?? []);
68
+ const byKey = new Map(before.map((kept) => [kept.key, kept]));
69
+ const wanted = keyedIn(invocation, slot);
70
+ let changed = wanted.length !== before.length;
71
+ const after = wanted.map((given, index) => {
72
+ const kept = byKey.get(given.key);
73
+ if (kept) {
74
+ const handed = handedProps(child, kept, given.props);
75
+ if (before[index] !== handed)
76
+ changed = true;
77
+ return handed;
78
+ }
79
+ changed = true;
80
+ invoked ??= { ...instance.invoked };
81
+ return invoke(child, instance.id, slot, given.key, given, invoked);
82
+ });
83
+ if (changed)
84
+ (children ??= { ...instance.children })[slot] = after;
85
+ }
86
+ return children
87
+ ? { ...instance, children, invoked: invoked ?? instance.invoked }
88
+ : instance;
89
+ };
90
+ /** A Child kept, with the Props it is handed now: the same Instance while they say the same. */
91
+ const handedProps = (definition, child, props) => {
92
+ if (Equal.equals(child.props, props))
93
+ return child;
94
+ definition.check.props(props, `the Props of ${child.id}`);
95
+ const { props: _, ...rest } = child;
96
+ return follow(definition, props === undefined ? rest : { ...rest, props });
97
+ };
98
+ const invoke = (definition, parent, slot, key, given, invoked) => {
99
+ const counter = counterOf(slot, key);
100
+ const generation = (invoked[counter] ?? 0) + 1;
101
+ invoked[counter] = generation;
102
+ return create(definition, childId(parent, slot, key, generation), key, given);
103
+ };
104
+ const invocationOf = (definition, instance) => definition.invoke[instance.state._tag]?.({
105
+ model: instance.model,
106
+ state: instance.state,
107
+ props: instance.props,
108
+ }) ?? {};
109
+ const givenIn = (invocation, slot) => (invocation[slot] ?? {});
110
+ const keyedIn = (invocation, slot) => {
111
+ const keyed = (invocation[slot] ?? []);
112
+ const seen = new Set();
113
+ for (const { key } of keyed) {
114
+ if (seen.has(key))
115
+ throw new Error(`[@kestack/kabuki] ${slot}: key "${key}" is Invoked twice`);
116
+ seen.add(key);
117
+ }
118
+ return keyed;
119
+ };
@@ -0,0 +1,16 @@
1
+ import type { ActorDefinition, Tagged } from '../actor/index.ts';
2
+ import type { Snapshot } from './snapshot.ts';
3
+ export declare const RESTORE = "@kestack/kabuki/Restore";
4
+ export interface Restore extends Tagged {
5
+ readonly _tag: typeof RESTORE;
6
+ readonly snapshot: Snapshot;
7
+ }
8
+ export declare const isRestore: (message: Tagged) => message is Restore;
9
+ export declare const restore: (snapshot: Snapshot) => Restore;
10
+ /** The saved Snapshot if every Instance in it matches its Actor, else why not. */
11
+ export declare const checkRestore: (root: ActorDefinition, { snapshot }: Restore) => {
12
+ readonly snapshot: Snapshot;
13
+ } | {
14
+ readonly reason: string;
15
+ };
16
+ //# sourceMappingURL=restore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"restore.d.ts","sourceRoot":"","sources":["../../../src/core/snapshot/restore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,KAAK,EAAY,QAAQ,EAAE,MAAM,eAAe,CAAC;AAYxD,eAAO,MAAM,OAAO,4BAA4B,CAAC;AAEjD,MAAM,WAAW,OAAQ,SAAQ,MAAM;IACrC,QAAQ,CAAC,IAAI,EAAE,OAAO,OAAO,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;CAC7B;AAED,eAAO,MAAM,SAAS,YAAa,MAAM,KAAG,OAAO,IAAI,OAC7B,CAAC;AAE3B,eAAO,MAAM,OAAO,aAAc,QAAQ,KAAG,OAG3C,CAAC;AAEH,kFAAkF;AAClF,eAAO,MAAM,YAAY,SACjB,eAAe,gBACP,OAAO,KACpB;IAAE,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAK7D,CAAC"}
@@ -0,0 +1,63 @@
1
+ import { counterOf, isMany, slotToward } from './tree.js';
2
+ /*
3
+ * Restore: a Message to the root that replaces the whole Snapshot with one
4
+ * saved before. The only Message no Update sees. Before it is put in place,
5
+ * every Instance is checked against its Actor: its ID, its Actor's name, its
6
+ * State, its Model, its Props and its Children's slots, every one its State
7
+ * Invokes and no other; one that does not
8
+ * match is a reason, and the Restore is ignored.
9
+ */
10
+ export const RESTORE = '@kestack/kabuki/Restore';
11
+ export const isRestore = (message) => message._tag === RESTORE;
12
+ export const restore = (snapshot) => ({
13
+ _tag: RESTORE,
14
+ snapshot,
15
+ });
16
+ /** The saved Snapshot if every Instance in it matches its Actor, else why not. */
17
+ export const checkRestore = (root, { snapshot }) => {
18
+ if (snapshot.id !== root.name)
19
+ return { reason: `its root is ${snapshot.id}, not ${root.name}` };
20
+ const reason = check(root, snapshot, undefined);
21
+ return reason === undefined ? { snapshot } : { reason };
22
+ };
23
+ const check = (definition, instance, key) => {
24
+ const { id } = instance;
25
+ if (instance.actor !== definition.name)
26
+ return `${id} is a ${instance.actor}, not a ${definition.name}`;
27
+ if (instance.key !== key)
28
+ return `${id} has the key ${instance.key}`;
29
+ const tag = instance.state._tag;
30
+ if (!definition.states.includes(tag))
31
+ return `${id} is in ${tag}, which ${definition.name} has no State for`;
32
+ try {
33
+ definition.check.model(instance.model, `the Model of ${id}`);
34
+ definition.check.state(instance.state, `the State of ${id}`);
35
+ definition.check.props(instance.props, `the Props of ${id}`);
36
+ }
37
+ catch (error) {
38
+ return error.message;
39
+ }
40
+ const slots = definition.children[tag] ?? {};
41
+ for (const slot of Object.keys(slots))
42
+ if (!(slot in instance.children))
43
+ return `${id} is in ${tag}, which Invokes "${slot}", but holds no such Child`;
44
+ for (const [slot, held] of Object.entries(instance.children)) {
45
+ const wanted = slots[slot];
46
+ if (!wanted)
47
+ return `${id} has a Child "${slot}" that ${tag} does not Invoke`;
48
+ if (isMany(held) !== wanted.many)
49
+ return `${id}'s Child "${slot}" is ${wanted.many ? '' : 'not '}keyed`;
50
+ for (const child of isMany(held) ? held : [held]) {
51
+ if (slotToward(id, child.id) !== slot)
52
+ return `${child.id} is not under ${id}'s "${slot}"`;
53
+ const generation = Number(child.id.slice(child.id.lastIndexOf('#') + 1));
54
+ const counted = instance.invoked[counterOf(slot, child.key)] ?? 0;
55
+ if (!(generation <= counted))
56
+ return `${id} has Invoked "${slot}" ${counted} times, but holds ${child.id}`;
57
+ const reason = check(wanted.definition, child, isMany(held) ? child.key : undefined);
58
+ if (reason !== undefined)
59
+ return reason;
60
+ }
61
+ }
62
+ return undefined;
63
+ };
@@ -0,0 +1,61 @@
1
+ import type { ActorDefinition, Tagged } from '../actor/index.ts';
2
+ /** One running Actor in the tree. */
3
+ export interface Instance {
4
+ readonly id: string;
5
+ /** Its Actor's name. */
6
+ readonly actor: string;
7
+ /** Its key, if it is a keyed Child. */
8
+ readonly key?: string;
9
+ readonly state: Tagged;
10
+ readonly model: unknown;
11
+ /**
12
+ * What its parent hands it now, if its Actor declares Props. Worked out by
13
+ * the parent, never written by the Instance.
14
+ */
15
+ readonly props?: unknown;
16
+ /** A fixed Child by its name; keyed Children by their slot, in the Model's order. */
17
+ readonly children: Readonly<Record<string, Instance | ReadonlyArray<Instance>>>;
18
+ /** How many times this Instance has Invoked each Child, for the next ID. */
19
+ readonly invoked: Readonly<Record<string, number>>;
20
+ }
21
+ /** The whole app: its root Instance. */
22
+ export type Snapshot = Instance;
23
+ /**
24
+ * Who Sent a Message: the Instance, and what in it Sent: its View, a Command
25
+ * by its name, a Worker by its name and key, or what it Provides. Only a
26
+ * Request comes from another Instance than its destination. A Message from outside the tree, by
27
+ * a test or a tool, comes from no Instance; a Restore no one Sent: it was
28
+ * given to the app.
29
+ */
30
+ export type Source = {
31
+ readonly kind: 'view';
32
+ readonly instance: string;
33
+ } | {
34
+ readonly kind: 'command';
35
+ readonly instance: string;
36
+ readonly name: string;
37
+ } | {
38
+ readonly kind: 'worker';
39
+ readonly instance: string;
40
+ readonly name: string;
41
+ /** The key an `each` Worker runs for. */
42
+ readonly key?: string;
43
+ } | {
44
+ readonly kind: 'provide';
45
+ readonly instance: string;
46
+ } | {
47
+ readonly kind: 'outside';
48
+ } | {
49
+ readonly kind: 'restore';
50
+ };
51
+ /** A Message, the Instance it is for, who Sent it, and its Time. */
52
+ export interface Envelope {
53
+ /** The ID of the Instance it is for. */
54
+ readonly destination: string;
55
+ readonly message: Tagged;
56
+ readonly source: Source;
57
+ readonly at: number;
58
+ }
59
+ /** The app right after init: the root, named after its Actor, and every Child its State Invokes. */
60
+ export declare const init: (root: ActorDefinition, input?: unknown) => Snapshot;
61
+ //# sourceMappingURL=snapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../../../src/core/snapshot/snapshot.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAWjE,qCAAqC;AACrC,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,wBAAwB;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,uCAAuC;IACvC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,qFAAqF;IACrF,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CACzB,MAAM,CAAC,MAAM,EAAE,QAAQ,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC,CACnD,CAAC;IACF,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACpD;AAED,wCAAwC;AACxC,MAAM,MAAM,QAAQ,GAAG,QAAQ,CAAC;AAEhC;;;;;;GAMG;AACH,MAAM,MAAM,MAAM,GACd;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACpD;IACE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,yCAAyC;IACzC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;CACvB,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACvD;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAEjC,oEAAoE;AACpE,MAAM,WAAW,QAAQ;IACvB,wCAAwC;IACxC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAGD,oGAAoG;AACpG,eAAO,MAAM,IAAI,SAAU,eAAe,UAAU,OAAO,KAAG,QACf,CAAC"}
@@ -0,0 +1,4 @@
1
+ import { create } from './invoke.js';
2
+ // 1. Init
3
+ /** The app right after init: the root, named after its Actor, and every Child its State Invokes. */
4
+ export const init = (root, input) => create(root, root.name, undefined, { input });