@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,26 @@
1
+ import type { ActorDefinition } from '../actor/index.ts';
2
+ import type { Instance, Snapshot } from './snapshot.ts';
3
+ export declare const childId: (parent: string, slot: string, key: string | undefined, generation: number) => string;
4
+ /** What the parent counts its Invocations of a Child under. */
5
+ export declare const counterOf: (slot: string, key: string | undefined) => string;
6
+ /** Whether `id` is `ancestor` or below it. */
7
+ export declare const within: (ancestor: string, id: string) => boolean;
8
+ /** The slot under `parent` on the way down to `id`, if `id` is below it. */
9
+ export declare const slotToward: (parent: string, id: string) => string | undefined;
10
+ /** Tells a keyed Child slot from a fixed one in an Instance's Children. */
11
+ export declare const isMany: (held: Instance | ReadonlyArray<Instance>) => held is ReadonlyArray<Instance>;
12
+ /** One Instance on the way down, with its Actor and the slot it sits in. */
13
+ export interface Step {
14
+ readonly instance: Instance;
15
+ readonly definition: ActorDefinition;
16
+ readonly slot: string;
17
+ }
18
+ /** The Instances from the root down to `id`, with their Actors; none if it is gone. */
19
+ export declare const pathTo: (root: ActorDefinition, snapshot: Snapshot, id: string) => ReadonlyArray<Step> | undefined;
20
+ /** The Snapshot with the last Instance of `path` replaced by `after`. Every Instance off the path stays the same object. */
21
+ export declare const replace: (path: ReadonlyArray<Step>, after: Instance) => Snapshot;
22
+ /** The Instance with this ID, if it is in the Snapshot. */
23
+ export declare const instanceAt: (snapshot: Snapshot, id: string) => Instance | undefined;
24
+ /** The Actor of the Instance with this ID, if it is in the Snapshot. */
25
+ export declare const actorAt: (root: ActorDefinition, snapshot: Snapshot, id: string) => ActorDefinition | undefined;
26
+ //# sourceMappingURL=tree.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../../../src/core/snapshot/tree.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAcxD,eAAO,MAAM,OAAO,WACV,MAAM,QACR,MAAM,OACP,MAAM,GAAG,SAAS,cACX,MAAM,KACjB,MAC0F,CAAC;AAE9F,+DAA+D;AAC/D,eAAO,MAAM,SAAS,SAAU,MAAM,OAAO,MAAM,GAAG,SAAS,KAAG,MACA,CAAC;AAEnE,8CAA8C;AAC9C,eAAO,MAAM,MAAM,aAAc,MAAM,MAAM,MAAM,KAAG,OACJ,CAAC;AAEnD,4EAA4E;AAC5E,eAAO,MAAM,UAAU,WAAY,MAAM,MAAM,MAAM,KAAG,MAAM,GAAG,SAKhE,CAAC;AAEF,2EAA2E;AAC3E,eAAO,MAAM,MAAM,SACX,QAAQ,GAAG,aAAa,CAAC,QAAQ,CAAC,KACvC,IAAI,IAAI,aAAa,CAAC,QAAQ,CAAwB,CAAC;AAE1D,4EAA4E;AAC5E,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,eAAe,CAAC;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,uFAAuF;AACvF,eAAO,MAAM,MAAM,SACX,eAAe,YACX,QAAQ,MACd,MAAM,KACT,aAAa,CAAC,IAAI,CAAC,GAAG,SAexB,CAAC;AAEF,4HAA4H;AAC5H,eAAO,MAAM,OAAO,SAAU,aAAa,CAAC,IAAI,CAAC,SAAS,QAAQ,KAAG,QAa1D,CAAC;AAEZ,2DAA2D;AAC3D,eAAO,MAAM,UAAU,aACX,QAAQ,MACd,MAAM,KACT,QAAQ,GAAG,SAQb,CAAC;AAEF,wEAAwE;AACxE,eAAO,MAAM,OAAO,SACZ,eAAe,YACX,QAAQ,MACd,MAAM,KACT,eAAe,GAAG,SAC2B,CAAC"}
@@ -0,0 +1,77 @@
1
+ /*
2
+ * The tree: where each Instance sits. An Instance's ID is its path from the
3
+ * root: its parent's ID, the Child's name, its key if it has one, and how
4
+ * many times the parent has Invoked that Child. So the same Messages always
5
+ * give the same IDs, an Instance Invoked again gets a new one, and finding
6
+ * an Instance walks straight down to it.
7
+ *
8
+ * Auth the root, named after its Actor
9
+ * Auth/api#2 the second `api` Auth Invoked
10
+ * Auth/api#2/rows[a]#1 the first `rows` keyed `a` under it
11
+ */
12
+ export const childId = (parent, slot, key, generation) => `${parent}/${slot}${key === undefined ? '' : `[${encodeURIComponent(key)}]`}#${generation}`;
13
+ /** What the parent counts its Invocations of a Child under. */
14
+ export const counterOf = (slot, key) => key === undefined ? slot : `${slot}[${encodeURIComponent(key)}]`;
15
+ /** Whether `id` is `ancestor` or below it. */
16
+ export const within = (ancestor, id) => id === ancestor || id.startsWith(`${ancestor}/`);
17
+ /** The slot under `parent` on the way down to `id`, if `id` is below it. */
18
+ export const slotToward = (parent, id) => {
19
+ if (!id.startsWith(`${parent}/`))
20
+ return undefined;
21
+ const rest = id.slice(parent.length + 1);
22
+ const end = rest.search(/[[#]/);
23
+ return end < 0 ? undefined : rest.slice(0, end);
24
+ };
25
+ /** Tells a keyed Child slot from a fixed one in an Instance's Children. */
26
+ export const isMany = (held) => Array.isArray(held);
27
+ /** The Instances from the root down to `id`, with their Actors; none if it is gone. */
28
+ export const pathTo = (root, snapshot, id) => {
29
+ const path = [
30
+ { instance: snapshot, definition: root, slot: '' },
31
+ ];
32
+ for (let step = path[0]; step.instance.id !== id;) {
33
+ const slot = slotToward(step.instance.id, id);
34
+ if (slot === undefined)
35
+ return undefined;
36
+ const definition = step.definition.children[step.instance.state._tag]?.[slot]?.definition;
37
+ const child = childToward(step.instance.children[slot], id);
38
+ if (!definition || !child)
39
+ return undefined;
40
+ step = { instance: child, definition, slot };
41
+ path.push(step);
42
+ }
43
+ return path;
44
+ };
45
+ /** The Snapshot with the last Instance of `path` replaced by `after`. Every Instance off the path stays the same object. */
46
+ export const replace = (path, after) => path.slice(0, -1).reduceRight((child, { instance }, index) => {
47
+ const { slot } = path[index + 1];
48
+ const held = instance.children[slot];
49
+ return {
50
+ ...instance,
51
+ children: {
52
+ ...instance.children,
53
+ [slot]: isMany(held)
54
+ ? held.map((kept) => (kept.id === child.id ? child : kept))
55
+ : child,
56
+ },
57
+ };
58
+ }, after);
59
+ /** The Instance with this ID, if it is in the Snapshot. */
60
+ export const instanceAt = (snapshot, id) => {
61
+ let instance = snapshot;
62
+ while (instance && instance.id !== id) {
63
+ const slot = slotToward(instance.id, id);
64
+ instance =
65
+ slot === undefined ? undefined : childToward(instance.children[slot], id);
66
+ }
67
+ return instance;
68
+ };
69
+ /** The Actor of the Instance with this ID, if it is in the Snapshot. */
70
+ export const actorAt = (root, snapshot, id) => pathTo(root, snapshot, id)?.at(-1)?.definition;
71
+ const childToward = (held, id) => held === undefined
72
+ ? undefined
73
+ : isMany(held)
74
+ ? held.find((child) => within(child.id, id))
75
+ : within(held.id, id)
76
+ ? held
77
+ : undefined;
@@ -0,0 +1,49 @@
1
+ import type { ReactNode } from 'react';
2
+ import { Cause } from 'effect';
3
+ import type { Layer } from 'effect';
4
+ import { History } from '../core/index.ts';
5
+ import type { AnyActor, Entry, Needs, SavedHistory, WorkerStatus } from '../core/index.ts';
6
+ import type { ViewOf } from './view.tsx';
7
+ type Unmet<A, Provided> = [Exclude<Needs<A>, Provided>] extends [never] ? [] : [unmet: {
8
+ readonly missingCapabilities: Exclude<Needs<A>, Provided>;
9
+ }];
10
+ /** A Step: right after an entry, or right after init. */
11
+ type Step = number | 'init';
12
+ export interface AppOptions {
13
+ /**
14
+ * What the app is called: the History its last mount leaves is picked up
15
+ * by the next mount of an app with this name, after a hot reload too. By
16
+ * default the root Actor's name.
17
+ */
18
+ readonly name?: string;
19
+ /** A History saved before: the app is first drawn at its Head, and its Runtime carries on from there. */
20
+ readonly saved?: SavedHistory;
21
+ }
22
+ /** The running app, for code outside the tree: a timeline, a devtool. */
23
+ export interface AppRuntime<A = AnyActor> {
24
+ readonly history: History<A>;
25
+ /** The current Branch: every entry from the first to the Head. */
26
+ readonly branch: ReadonlyArray<Entry>;
27
+ readonly head: number | null;
28
+ readonly running: boolean;
29
+ /** The Step the Views show, or `null` for live. */
30
+ readonly shown: Step | null;
31
+ readonly show: (step: Step | null) => void;
32
+ /** Start at the Head, or at an entry (`null`: init), growing a new Branch from it. */
33
+ readonly start: (from?: number | null) => void;
34
+ readonly stop: () => void;
35
+ /** Forget the whole History and go live from init. */
36
+ readonly clear: () => void;
37
+ /** What an Instance's Workers are doing now. Empty while stopped. */
38
+ readonly workers: (instance: string) => ReadonlyArray<WorkerStatus>;
39
+ /** Why the last start, stop or clear failed, also logged; `null` once one succeeds. */
40
+ readonly failure: Cause.Cause<unknown> | null;
41
+ }
42
+ export declare const toReact: <A extends AnyActor, Provided>(actor: A, view: ViewOf<A>, layer: Layer.Layer<Provided, never, never>, options?: AppOptions, ..._unmet: Unmet<A, Provided>) => (({ fallback }: {
43
+ readonly fallback?: ReactNode;
44
+ }) => import("react").JSX.Element) & {
45
+ displayName: string;
46
+ useRuntime: () => AppRuntime<A>;
47
+ };
48
+ export {};
49
+ //# sourceMappingURL=app.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/react/app.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,EAAE,KAAK,EAA8B,MAAM,QAAQ,CAAC;AAC3D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,EAAE,OAAO,EAAW,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EACV,QAAQ,EACR,KAAK,EACL,KAAK,EAEL,YAAY,EAEZ,YAAY,EACb,MAAM,kBAAkB,CAAC;AAI1B,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAkBzC,KAAK,KAAK,CAAC,CAAC,EAAE,QAAQ,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GACnE,EAAE,GACF,CAAC,KAAK,EAAE;IAAE,QAAQ,CAAC,mBAAmB,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAA;CAAE,CAAC,CAAC;AAE3E,yDAAyD;AACzD,KAAK,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5B,MAAM,WAAW,UAAU;IACzB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,yGAAyG;IACzG,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAC;CAC/B;AAED,yEAAyE;AACzE,MAAM,WAAW,UAAU,CAAC,CAAC,GAAG,QAAQ;IACtC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,KAAK,CAAC,CAAC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,mDAAmD;IACnD,QAAQ,CAAC,KAAK,EAAE,IAAI,GAAG,IAAI,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,KAAK,IAAI,CAAC;IAC3C,sFAAsF;IACtF,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,IAAI,CAAC;IAC1B,sDAAsD;IACtD,QAAQ,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC;IAC3B,qEAAqE;IACrE,QAAQ,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,aAAa,CAAC,YAAY,CAAC,CAAC;IACpE,uFAAuF;IACvF,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;CAC/C;AAID,eAAO,MAAM,OAAO,GAAI,CAAC,SAAS,QAAQ,EAAE,QAAQ,SAC3C,CAAC,QACF,MAAM,CAAC,CAAC,CAAC,SACR,KAAK,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,EAAE,KAAK,CAAC,YACjC,UAAU,aACR,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,qBAoMK;IAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;CAAE;;sBAWjD,UAAU,CAAC,CAAC,CAAC;CAqBhC,CAAC"}
@@ -0,0 +1,214 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // oxlint-disable-next-line no-restricted-imports -- the app's one mount effect: counting mounts starts and stops the Runtime, which lives outside React.
3
+ import { Suspense, use, useEffect, useSyncExternalStore } from 'react';
4
+ import { Cause, Effect, Exit, Fiber, Scope } from 'effect';
5
+ import { History, Runtime } from '../core/index.js';
6
+ import { makeDrawn } from './drawn.js';
7
+ import { registry } from './registry.js';
8
+ import { AppContext } from './view.js';
9
+ const NO_WORKERS = [];
10
+ export const toReact = (actor, view, layer, options = {}, ..._unmet) => {
11
+ const RootView = view;
12
+ const name = options.name ?? actor.definition.name;
13
+ const root = { id: actor.definition.name };
14
+ const app = {};
15
+ const own = () => options.saved ? History.load(actor, options.saved) : History.make(actor);
16
+ /** The History an app of this name holds or left, read with this code, else its own. */
17
+ const adopt = () => {
18
+ const left = registry.history(name);
19
+ return left ? History.load(actor, left.save()) : own();
20
+ };
21
+ let history;
22
+ let first;
23
+ let runtime;
24
+ let shown = null;
25
+ let past;
26
+ let failure = null;
27
+ const listeners = new Set();
28
+ let seen;
29
+ const changed = () => {
30
+ seen = undefined;
31
+ for (const listener of listeners)
32
+ listener();
33
+ };
34
+ let unwatch = () => { };
35
+ const take = (next) => {
36
+ unwatch();
37
+ history = next;
38
+ first = undefined;
39
+ unwatch = next.subscribe(changed);
40
+ return next;
41
+ };
42
+ const historyOf = () => history ?? take(adopt());
43
+ /** What came of a start, stop or clear: an interruption is a newer one taking over, not a failure. */
44
+ const settled = (what) => (exit) => {
45
+ if (Exit.isSuccess(exit)) {
46
+ if (failure === null)
47
+ return;
48
+ failure = null;
49
+ }
50
+ else {
51
+ if (Cause.hasInterruptsOnly(exit.cause))
52
+ return;
53
+ console.error(`[@kestack/kabuki] ${name} could not ${what}:\n${Cause.pretty(exit.cause)}`);
54
+ failure = exit.cause;
55
+ }
56
+ changed();
57
+ };
58
+ // 1. Draw
59
+ /** Start replaying the History to its Head, once: what the app draws before its Runtime starts. */
60
+ const firstDraw = () => {
61
+ if (first)
62
+ return first;
63
+ const from = historyOf();
64
+ const draw = { snapshot: undefined, replaying: undefined };
65
+ draw.replaying =
66
+ from.head() === null
67
+ ? fulfilled((draw.snapshot = from.snapshotAt(null)))
68
+ : tracked(Effect.runPromise(from.seek()).then((snapshot) => {
69
+ draw.snapshot = snapshot;
70
+ changed();
71
+ return snapshot;
72
+ }));
73
+ return (first = draw);
74
+ };
75
+ /** Before the Runtime starts, wait for the first Snapshot: React shows the fallback until it is replayed. */
76
+ const waitForFirstDraw = () => {
77
+ const { snapshot, replaying } = firstDraw();
78
+ if (!snapshot)
79
+ use(replaying);
80
+ };
81
+ const snapshot = () => shown === null ? (runtime?.snapshot() ?? first?.snapshot) : past;
82
+ const drawn = makeDrawn(snapshot, () => first?.snapshot, (id, message) => {
83
+ if (shown === null)
84
+ runtime?.send(id, message, { kind: 'view', instance: id });
85
+ }, (listener) => {
86
+ listeners.add(listener);
87
+ return () => listeners.delete(listener);
88
+ });
89
+ // 2. Mount
90
+ let mounts = 0;
91
+ let close = () => { };
92
+ let unsubscribe = () => { };
93
+ const mount = () => {
94
+ if (mounts++ === 0) {
95
+ if (!registry.claim(name, app, historyOf())) {
96
+ console.error(`[@kestack/kabuki] two apps named ${name} are mounted at once; they cannot share a History`);
97
+ take(own());
98
+ changed();
99
+ }
100
+ let open = true;
101
+ close = () => {
102
+ open = false;
103
+ };
104
+ firstDraw().replaying.then(() => {
105
+ if (!open)
106
+ return;
107
+ close = host(historyOf(), layer, (exit) => {
108
+ if (Exit.isSuccess(exit)) {
109
+ unsubscribe();
110
+ runtime = exit.value;
111
+ unsubscribe = exit.value.subscribe(changed);
112
+ changed();
113
+ }
114
+ settled('start')(exit);
115
+ });
116
+ }, (error) => {
117
+ console.error(`[@kestack/kabuki] ${name} could not be drawn, so its Runtime did not start`, error);
118
+ });
119
+ }
120
+ return () => {
121
+ if (--mounts === 0) {
122
+ registry.release(name, app);
123
+ close();
124
+ }
125
+ };
126
+ };
127
+ // 3. Show
128
+ let showing;
129
+ const show = (step) => {
130
+ showing?.interruptUnsafe();
131
+ if (step === null) {
132
+ shown = null;
133
+ past = undefined;
134
+ return changed();
135
+ }
136
+ showing = Effect.runFork(historyOf()
137
+ .seek(step === 'init' ? null : step)
138
+ .pipe(Effect.map((snapshot) => {
139
+ past = snapshot;
140
+ shown = step;
141
+ changed();
142
+ })));
143
+ };
144
+ const control = (what, effect) => {
145
+ if (effect)
146
+ Effect.runFork(effect).addObserver(settled(what));
147
+ };
148
+ const start = (from) => control('start', runtime?.start(from));
149
+ const stop = () => control('stop', runtime?.stop());
150
+ const clear = () => control('clear its History', Effect.gen(function* () {
151
+ yield* runtime?.stop() ?? Effect.void;
152
+ historyOf().clear();
153
+ yield* runtime?.start() ?? Effect.void;
154
+ }));
155
+ const read = () => (seen ??= {
156
+ head: historyOf().head(),
157
+ running: runtime?.running() ?? false,
158
+ shown,
159
+ failure,
160
+ });
161
+ const Drawn = () => {
162
+ if (!runtime)
163
+ waitForFirstDraw();
164
+ return (_jsx(AppContext, { value: drawn, children: _jsx(RootView, { instance: root }) }));
165
+ };
166
+ const App = ({ fallback = null }) => {
167
+ useEffect(mount, []);
168
+ return (_jsx(Suspense, { fallback: fallback, children: _jsx(Drawn, {}) }));
169
+ };
170
+ return Object.assign(App, {
171
+ displayName: name,
172
+ useRuntime: () => {
173
+ const { head, running, shown, failure } = useSyncExternalStore(drawn.subscribe, read, read);
174
+ return {
175
+ history: historyOf(),
176
+ branch: historyOf().branch(head),
177
+ head,
178
+ running,
179
+ shown,
180
+ show,
181
+ start,
182
+ stop,
183
+ clear,
184
+ workers: (instance) => runtime?.workers(instance) ?? NO_WORKERS,
185
+ failure,
186
+ };
187
+ },
188
+ });
189
+ };
190
+ const fulfilled = (value) => Object.assign(Promise.resolve(value), { status: 'fulfilled', value });
191
+ const tracked = (promise) => {
192
+ const thenable = Object.assign(promise, { status: 'pending' });
193
+ promise.then((value) => Object.assign(thenable, { status: 'fulfilled', value }), (reason) => Object.assign(thenable, { status: 'rejected', reason }));
194
+ return thenable;
195
+ };
196
+ /**
197
+ * Start the Runtime under one Scope, and say how the start went; a failed
198
+ * one closes the Scope. Returns a function that closes the Scope: every
199
+ * Command and Worker ends, then the Layer.
200
+ */
201
+ const host = (history, layer, onSettled) => {
202
+ const scope = Scope.makeUnsafe();
203
+ const starting = Effect.runFork(Runtime.start(history, ...[layer]).pipe(Scope.provide(scope)));
204
+ starting.addObserver((exit) => {
205
+ if (Exit.isFailure(exit))
206
+ void Effect.runPromise(Scope.close(scope, exit));
207
+ onSettled(exit);
208
+ });
209
+ // A start still under way is interrupted first, so nothing is built into a
210
+ // closed Scope (React StrictMode unmounts right after the first mount).
211
+ return () => {
212
+ void Effect.runPromise(Fiber.interrupt(starting).pipe(Effect.andThen(Scope.close(scope, Exit.void))));
213
+ };
214
+ };
@@ -0,0 +1,32 @@
1
+ import type { Handle, ViewProps } from './view.tsx';
2
+ /**
3
+ * What a parent View can read of one Child: what the Child's own View draws
4
+ * from, but not its Children and nothing to Send with. Reading never changes
5
+ * the app, and it reaches one level down only.
6
+ */
7
+ export type ChildData<A> = ViewProps<A> extends infer P ? P extends unknown ? Omit<P, 'children' | 'send' | 'root'> : never : never;
8
+ interface UseChild {
9
+ <A>(handle: Handle<A>): ChildData<A> | undefined;
10
+ <A, S>(handle: Handle<A>, select: (child: ChildData<A>) => S): S | undefined;
11
+ }
12
+ /**
13
+ * Reads one fixed Child from its parent's View: its Model, State and Props,
14
+ * at the Step drawn; `undefined` once the Child has stopped. The component
15
+ * re-renders when they change, or, given a selector, only when what it picks
16
+ * changes. An escape hatch for drawing only: what a parent must act on, it
17
+ * owns.
18
+ */
19
+ export declare const useChild: UseChild;
20
+ interface UseChildren {
21
+ <A>(handles: ReadonlyArray<Handle<A>>): ReadonlyArray<ChildData<A>>;
22
+ <A, S>(handles: ReadonlyArray<Handle<A>>, select: (child: ChildData<A>) => S): ReadonlyArray<S>;
23
+ }
24
+ /**
25
+ * Reads a slot of keyed Children from their parent's View, in the slot's
26
+ * order, leaving out any that stopped. The component re-renders when one of
27
+ * them changes or the list does, or, given a selector, only when what it
28
+ * picks from one of them changes or the list does.
29
+ */
30
+ export declare const useChildren: UseChildren;
31
+ export {};
32
+ //# sourceMappingURL=child.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"child.d.ts","sourceRoot":"","sources":["../../src/react/child.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAsB,MAAM,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAExE;;;;GAIG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,IACrB,SAAS,CAAC,CAAC,CAAC,SAAS,MAAM,CAAC,GACxB,CAAC,SAAS,OAAO,GACf,IAAI,CAAC,CAAC,EAAE,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC,GACrC,KAAK,GACP,KAAK,CAAC;AA6CZ,UAAU,QAAQ;IAChB,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;IACjD,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;CAC9E;AAED;;;;;;GAMG;AACH,eAAO,MAAM,QAAQ,EAAE,QAiBtB,CAAC;AAEF,UAAU,WAAW;IACnB,CAAC,CAAC,EAAE,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;IACpE,CAAC,CAAC,EAAE,CAAC,EACH,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EACjC,MAAM,EAAE,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,GACjC,aAAa,CAAC,CAAC,CAAC,CAAC;CACrB;AAED;;;;;GAKG;AACH,eAAO,MAAM,WAAW,EAAE,WAyBzB,CAAC"}
@@ -0,0 +1,80 @@
1
+ import { use, useRef, useSyncExternalStore } from 'react';
2
+ import { AppContext } from './view.js';
3
+ // One read-only copy per drawn Instance, so it stays the same object while
4
+ // the Instance does.
5
+ const reads = new WeakMap();
6
+ const readOf = (drawn) => {
7
+ if (!drawn)
8
+ return undefined;
9
+ let read = reads.get(drawn);
10
+ if (!read) {
11
+ const { _tag, id, model, state, props } = drawn;
12
+ reads.set(drawn, (read = { _tag, id, model, state, props }));
13
+ }
14
+ return read;
15
+ };
16
+ const useDrawn = (hook) => {
17
+ const drawn = use(AppContext);
18
+ if (!drawn)
19
+ throw new Error(`[@kestack/kabuki] ${hook} is used outside its app`);
20
+ return drawn;
21
+ };
22
+ /**
23
+ * What a selector picked from each Child, kept per Child read, so a Child
24
+ * that did not change is not picked from again. A new selector, as an inline
25
+ * one is on every render, starts over.
26
+ */
27
+ const usePicked = (select) => {
28
+ const memo = useRef(undefined);
29
+ if (!memo.current || memo.current.select !== select)
30
+ memo.current = { select, picked: new WeakMap() };
31
+ const { picked } = memo.current;
32
+ return (read) => {
33
+ if (!select)
34
+ return read;
35
+ if (!picked.has(read))
36
+ picked.set(read, select(read));
37
+ return picked.get(read);
38
+ };
39
+ };
40
+ /**
41
+ * Reads one fixed Child from its parent's View: its Model, State and Props,
42
+ * at the Step drawn; `undefined` once the Child has stopped. The component
43
+ * re-renders when they change, or, given a selector, only when what it picks
44
+ * changes. An escape hatch for drawing only: what a parent must act on, it
45
+ * owns.
46
+ */
47
+ export const useChild = (handle, select) => {
48
+ const drawn = useDrawn('useChild');
49
+ const pick = usePicked(select);
50
+ const last = useRef(undefined);
51
+ const from = (read) => () => {
52
+ const child = readOf(read(handle.id));
53
+ const next = child && pick(child);
54
+ return Object.is(next, last.current) ? last.current : (last.current = next);
55
+ };
56
+ return useSyncExternalStore(drawn.subscribe, from(drawn.instance), from(drawn.served));
57
+ };
58
+ /**
59
+ * Reads a slot of keyed Children from their parent's View, in the slot's
60
+ * order, leaving out any that stopped. The component re-renders when one of
61
+ * them changes or the list does, or, given a selector, only when what it
62
+ * picks from one of them changes or the list does.
63
+ */
64
+ export const useChildren = (handles, select) => {
65
+ const drawn = useDrawn('useChildren');
66
+ const pick = usePicked(select);
67
+ const last = useRef([]);
68
+ const from = (read) => () => {
69
+ const next = handles.flatMap((handle) => {
70
+ const child = readOf(read(handle.id));
71
+ return child ? [pick(child)] : [];
72
+ });
73
+ const was = last.current;
74
+ if (next.length === was.length &&
75
+ next.every((value, index) => Object.is(value, was[index])))
76
+ return was;
77
+ return (last.current = next);
78
+ };
79
+ return useSyncExternalStore(drawn.subscribe, from(drawn.instance), from(drawn.served));
80
+ };
@@ -0,0 +1,4 @@
1
+ import type { Snapshot, Tagged } from '../core/index.ts';
2
+ import type { DrawnApp } from './view.tsx';
3
+ export declare const makeDrawn: (drawn: () => Snapshot | undefined, served: () => Snapshot | undefined, deliver: (id: string, message: Tagged) => void, subscribe: DrawnApp['subscribe']) => DrawnApp;
4
+ //# sourceMappingURL=drawn.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drawn.d.ts","sourceRoot":"","sources":["../../src/react/drawn.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAsB,QAAQ,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC7E,OAAO,KAAK,EAAY,QAAQ,EAAU,MAAM,YAAY,CAAC;AAW7D,eAAO,MAAM,SAAS,UACb,MAAM,QAAQ,GAAG,SAAS,UACzB,MAAM,QAAQ,GAAG,SAAS,WACzB,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,aACnC,QAAQ,CAAC,WAAW,CAAC,KAC/B,QA0FF,CAAC"}
@@ -0,0 +1,118 @@
1
+ import { instanceAt, isMany } from '../core/index.js';
2
+ /*
3
+ * What Views read: what any Instance is drawn from, out of the Snapshot drawn
4
+ * now. It stays the same object while the Instance's Model, State, Props and
5
+ * Children's IDs stay the same, so a View re-renders only when its own
6
+ * Instance changes.
7
+ * A View of the past, of a stopped app, or of an app whose Runtime has not
8
+ * started cannot Send. What it keeps for an Instance is let go once the
9
+ * Instance is in neither Snapshot.
10
+ */
11
+ export const makeDrawn = (drawn, served, deliver, subscribe) => {
12
+ const handles = new Map();
13
+ const sends = new Map();
14
+ const cache = new Map();
15
+ const handleOf = ({ id, key }) => {
16
+ let handle = handles.get(id);
17
+ if (!handle)
18
+ handles.set(id, (handle = key === undefined ? { id } : { id, key }));
19
+ return handle;
20
+ };
21
+ const sendTo = (id) => {
22
+ let send = sends.get(id);
23
+ if (!send)
24
+ sends.set(id, (send = (message) => {
25
+ deliver(id, message);
26
+ }));
27
+ return send;
28
+ };
29
+ const propsIn = (cache, snapshot, id) => {
30
+ const instance = snapshot && instanceAt(snapshot, id);
31
+ const hit = cache.get(id);
32
+ if (!instance) {
33
+ cache.delete(id);
34
+ return undefined;
35
+ }
36
+ if (hit && (hit.instance === instance || same(hit.instance, instance))) {
37
+ hit.instance = instance;
38
+ return hit.props;
39
+ }
40
+ const next = {
41
+ _tag: instance.state._tag,
42
+ id,
43
+ model: instance.model,
44
+ state: instance.state,
45
+ props: instance.props,
46
+ // The same object while the Children's IDs stay the same, so a memoized
47
+ // component or a `useMemo` given them sees no change.
48
+ children: hit && sameIds(hit.instance, instance)
49
+ ? hit.props.children
50
+ : Object.fromEntries(Object.entries(instance.children).map(([slot, held]) => [
51
+ slot,
52
+ isMany(held) ? held.map(handleOf) : handleOf(held),
53
+ ])),
54
+ send: sendTo(id),
55
+ };
56
+ cache.set(id, { instance, props: next });
57
+ return next;
58
+ };
59
+ const onServer = new Map();
60
+ /** Forget every Instance in neither Snapshot, once per new Snapshot drawn, so keyed Children coming and going leave nothing behind. */
61
+ let pruned;
62
+ const prune = () => {
63
+ const now = drawn();
64
+ if (now === pruned)
65
+ return;
66
+ pruned = now;
67
+ const live = new Set();
68
+ idsIn(now, live);
69
+ idsIn(served(), live);
70
+ for (const kept of [handles, sends, cache, onServer])
71
+ for (const id of kept.keys())
72
+ if (!live.has(id))
73
+ kept.delete(id);
74
+ };
75
+ return {
76
+ subscribe,
77
+ instance: (id) => {
78
+ prune();
79
+ return propsIn(cache, drawn(), id);
80
+ },
81
+ served: (id) => {
82
+ prune();
83
+ const snapshot = served();
84
+ return snapshot === drawn()
85
+ ? propsIn(cache, snapshot, id)
86
+ : propsIn(onServer, snapshot, id);
87
+ },
88
+ };
89
+ };
90
+ const idsIn = (instance, ids) => {
91
+ if (!instance)
92
+ return;
93
+ ids.add(instance.id);
94
+ for (const held of Object.values(instance.children))
95
+ for (const child of isMany(held) ? held : [held])
96
+ idsIn(child, ids);
97
+ };
98
+ const same = (a, b) => a.model === b.model &&
99
+ a.state === b.state &&
100
+ a.props === b.props &&
101
+ sameIds(a, b);
102
+ const sameIds = (a, b) => {
103
+ if (a.children === b.children)
104
+ return true;
105
+ const slots = Object.keys(b.children);
106
+ if (slots.length !== Object.keys(a.children).length)
107
+ return false;
108
+ return slots.every((slot) => {
109
+ const was = a.children[slot];
110
+ const is = b.children[slot];
111
+ if (!was)
112
+ return false;
113
+ if (!isMany(is) || !isMany(was))
114
+ return !isMany(is) && !isMany(was) && is.id === was.id;
115
+ return (is.length === was.length &&
116
+ is.every((kid, index) => kid.id === was[index].id));
117
+ });
118
+ };
@@ -0,0 +1,7 @@
1
+ export { toReact } from './app.tsx';
2
+ export { View } from './view.tsx';
3
+ export { useChild, useChildren } from './child.ts';
4
+ export type { Handle, Root, ViewOf, ViewProps } from './view.tsx';
5
+ export type { ChildData } from './child.ts';
6
+ export type { AppOptions, AppRuntime } from './app.tsx';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAClC,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACnD,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAClE,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { toReact } from './app.js';
2
+ export { View } from './view.js';
3
+ export { useChild, useChildren } from './child.js';
@@ -0,0 +1,12 @@
1
+ import type { History } from '../core/index.ts';
2
+ export declare const registry: {
3
+ /** The History of the app mounted under this name, else the one its last mount left. */
4
+ history: (name: string) => History | undefined;
5
+ /** Note that `app` is mounted with its History; false if another app of that name already is. */
6
+ claim: (name: string, app: object, history: History) => boolean;
7
+ /** Leave the History as the app closes. */
8
+ release: (name: string, app: object) => void;
9
+ /** Forget every app: for tests. */
10
+ clear: () => void;
11
+ };
12
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/react/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAyBhD,eAAO,MAAM,QAAQ;IACnB,wFAAwF;IACxF,OAAO,SAAS,MAAM,KAAG,OAAO,GAAG,SAAS;IAC5C,iGAAiG;IACjG,KAAK,SAAS,MAAM,OAAO,MAAM,WAAW,OAAO,KAAG,OAAO;IAM7D,2CAA2C;IAC3C,OAAO,SAAS,MAAM,OAAO,MAAM;IAInC,mCAAmC;IACnC,KAAK;CACN,CAAC"}