babavoss 0.0.1 → 0.0.2

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 (158) hide show
  1. package/NOTICE +7 -0
  2. package/bin/voss.ts +48 -0
  3. package/gui/babavoss-web.js +234 -0
  4. package/gui/chunk-5hpp1ypv.js +11710 -0
  5. package/gui/chunk-9rq662fd.js +8627 -0
  6. package/gui/chunk-k3cm8j6r.js +433 -0
  7. package/gui/chunk-pk09y98y.js +50 -0
  8. package/gui/chunk-smz02qa6.js +186 -0
  9. package/gui/chunk-wwqypxre.js +49 -0
  10. package/gui/gui.js +2015 -0
  11. package/gui/react-compiler-runtime.js +39 -0
  12. package/gui/react-dom-client.js +21 -0
  13. package/gui/react-dom.js +44 -0
  14. package/gui/react-jsx-runtime.js +19 -0
  15. package/gui/react.js +105 -0
  16. package/gui/theme.css +3053 -0
  17. package/index.ts +15 -0
  18. package/package.json +50 -4
  19. package/src/baba/check.ts +90 -0
  20. package/src/baba/config.ts +261 -0
  21. package/src/baba/find.ts +12 -0
  22. package/src/baba/init.ts +176 -0
  23. package/src/baba/node.ts +483 -0
  24. package/src/baba/project.ts +63 -0
  25. package/src/baba/registry.ts +35 -0
  26. package/src/baba/worker.ts +55 -0
  27. package/src/bench/index.ts +6 -0
  28. package/src/bench/measure.ts +132 -0
  29. package/src/bench/scenarios.ts +136 -0
  30. package/src/build/builder.ts +74 -0
  31. package/src/build/failure.ts +78 -0
  32. package/src/build/guard.ts +85 -0
  33. package/src/build/mdx-register.ts +3 -0
  34. package/src/build/mdx.ts +38 -0
  35. package/src/build/project.ts +38 -0
  36. package/src/build/views.ts +146 -0
  37. package/src/builder/main.ts +29 -0
  38. package/src/desktop/bob.ts +76 -0
  39. package/src/desktop/desktop.css +111 -0
  40. package/src/desktop/icons.ts +50 -0
  41. package/src/desktop/index.ts +323 -0
  42. package/src/desktop/routes.ts +98 -0
  43. package/src/desktop/view.tsx +673 -0
  44. package/src/door/core.ts +384 -0
  45. package/src/ecs/baba.ts +431 -0
  46. package/src/ecs/codec.ts +334 -0
  47. package/src/ecs/handles.ts +91 -0
  48. package/src/ecs/replica.ts +150 -0
  49. package/src/ecs/runtime.ts +603 -0
  50. package/src/ecs/scheduler.ts +75 -0
  51. package/src/ecs/snapshot.ts +102 -0
  52. package/src/ecs/state.ts +759 -0
  53. package/src/ecs/system.ts +256 -0
  54. package/src/ecs/table.ts +420 -0
  55. package/src/ecs/testbed.ts +97 -0
  56. package/src/exec/host.ts +177 -0
  57. package/src/exec/main.ts +98 -0
  58. package/src/exec/watch.ts +7 -0
  59. package/src/exec/wire.ts +29 -0
  60. package/src/generated/build.ts +4 -0
  61. package/src/gui/css.d.ts +1 -0
  62. package/src/gui/gui.tsx +245 -0
  63. package/src/gui/index.ts +51 -0
  64. package/src/gui/inspector.tsx +47 -0
  65. package/src/gui/levels.tsx +73 -0
  66. package/src/gui/promptware.tsx +68 -0
  67. package/src/gui/runner.tsx +118 -0
  68. package/src/gui/theme.css +498 -0
  69. package/src/gui/theme.ts +25 -0
  70. package/src/gui/wizard.tsx +227 -0
  71. package/src/guide/add-a-desktop.mdx +100 -0
  72. package/src/guide/compose-an-interface.mdx +84 -0
  73. package/src/guide/index.ts +13 -0
  74. package/src/guide/reach-outside.mdx +112 -0
  75. package/src/guide/spec-a-system.mdx +93 -0
  76. package/src/guide/systems-together.mdx +72 -0
  77. package/src/guide/write-a-system.mdx +183 -0
  78. package/src/guide/write-promptware.mdx +90 -0
  79. package/src/http/server.ts +310 -0
  80. package/src/kernel/build.ts +21 -0
  81. package/src/kernel/builder.ts +105 -0
  82. package/src/kernel/children.ts +117 -0
  83. package/src/kernel/context.ts +90 -0
  84. package/src/kernel/lock.ts +46 -0
  85. package/src/kernel/names.ts +14 -0
  86. package/src/kernel/schema.ts +130 -0
  87. package/src/kernel/where.ts +12 -0
  88. package/src/kit/index.ts +232 -0
  89. package/src/maker/system.ts +213 -0
  90. package/src/mcp/daemon.ts +61 -0
  91. package/src/mcp/main.ts +208 -0
  92. package/src/mcp/rpc.ts +64 -0
  93. package/src/mcp/tools.ts +125 -0
  94. package/src/prompt/evals.ts +42 -0
  95. package/src/prompt/index.ts +242 -0
  96. package/src/prompt/jsx-dev-runtime.ts +1 -0
  97. package/src/prompt/jsx-runtime.ts +49 -0
  98. package/src/prompt/mdx.d.ts +1 -0
  99. package/src/promptware/compile.ts +183 -0
  100. package/src/promptware/define.ts +16 -0
  101. package/src/promptware/disk.ts +72 -0
  102. package/src/promptware/markdown.d.ts +6 -0
  103. package/src/promptware/sync.ts +437 -0
  104. package/src/promptware/system.ts +215 -0
  105. package/src/runtime/bridge.ts +85 -0
  106. package/src/runtime/connect.ts +54 -0
  107. package/src/runtime/env.ts +35 -0
  108. package/src/runtime/harness.ts +80 -0
  109. package/src/runtime/main.ts +119 -0
  110. package/src/runtime/worker.ts +33 -0
  111. package/src/server/edge.ts +332 -0
  112. package/src/server/main.ts +45 -0
  113. package/src/server/messages.ts +97 -0
  114. package/src/server/protocol.ts +37 -0
  115. package/src/services/args.ts +45 -0
  116. package/src/services/exec.ts +69 -0
  117. package/src/services/fs.ts +139 -0
  118. package/src/services/http.ts +30 -0
  119. package/src/services/index.ts +113 -0
  120. package/src/services/secrets.ts +18 -0
  121. package/src/shell/address.ts +21 -0
  122. package/src/shell/args.ts +219 -0
  123. package/src/shell/client.ts +107 -0
  124. package/src/shell/codes.ts +26 -0
  125. package/src/shell/positional.ts +20 -0
  126. package/src/shell/run.ts +470 -0
  127. package/src/shell/service.ts +167 -0
  128. package/src/shell/state.ts +204 -0
  129. package/src/spec/adapters.ts +72 -0
  130. package/src/spec/diff.ts +26 -0
  131. package/src/spec/files.ts +17 -0
  132. package/src/spec/index.ts +155 -0
  133. package/src/spec/run.ts +97 -0
  134. package/src/spec/take.ts +54 -0
  135. package/src/test/index.ts +8 -0
  136. package/src/test/prove.ts +56 -0
  137. package/src/test/records.ts +23 -0
  138. package/src/test/specs.ts +56 -0
  139. package/src/test/steps.ts +100 -0
  140. package/src/test/voss-dir.ts +17 -0
  141. package/src/transport/messages.ts +110 -0
  142. package/src/transport/transport.ts +62 -0
  143. package/src/wall/probe.ts +67 -0
  144. package/src/wall/profile.ts +103 -0
  145. package/src/wall/spawn.ts +59 -0
  146. package/src/web/app.tsx +53 -0
  147. package/src/web/core.tsx +140 -0
  148. package/src/web/form.ts +155 -0
  149. package/src/web/hooks.ts +135 -0
  150. package/src/web/index.tsx +17 -0
  151. package/src/web/list.ts +19 -0
  152. package/src/web/maker.tsx +766 -0
  153. package/src/web/objects.tsx +213 -0
  154. package/src/web/socket.ts +84 -0
  155. package/src/web/state.tsx +69 -0
  156. package/src/web/store.ts +221 -0
  157. package/src/web/ui.tsx +135 -0
  158. package/README.md +0 -5
@@ -0,0 +1,759 @@
1
+ // The state of a baba: entities with typed components, resources that exist
2
+ // once, and the jobs the port runs for it. Every declaration belongs to a
3
+ // system, and a system's steps write only what the system declared. Only data:
4
+ // a value is what its schema admits, a reference to another entity is its
5
+ // id. Job results live one round. Pure: no IO. Values are checked at the
6
+ // boundary, where they come from outside the type system; a system's own
7
+ // writes are trusted unless `checking` is on, as it is in a testbed.
8
+ import { SchemaError, type Schema, type Json } from "../kernel/schema.ts";
9
+ import type { Context } from "../kernel/context.ts";
10
+ import { keyField, mapEntities } from "./handles.ts";
11
+ import { Table } from "./table.ts";
12
+ import type { Delta } from "./snapshot.ts";
13
+
14
+ export type Entity = number;
15
+
16
+ /**
17
+ * A component: per entity. An `accept` is an ask: any system may add it to an
18
+ * entity, and only its owner replaces or removes it. `O` is the owning
19
+ * system's name, so one system's handle never passes for another's.
20
+ */
21
+ export interface Component<N extends string = string, T = unknown, O extends string = string> { readonly kind: "component"; readonly name: N; readonly owner: O; readonly schema: Schema<T>; readonly accept: boolean }
22
+ /** An ask, as a system refers to another's: addable by any system that reads its owner. */
23
+ export interface Ask<N extends string = string, T = unknown, O extends string = string> extends Component<N, T, O> { readonly accept: true }
24
+ /** A resource: once per baba, always present. */
25
+ export interface Resource<N extends string = string, T = unknown, O extends string = string> { readonly kind: "resource"; readonly name: N; readonly owner: O; readonly schema: Schema<T>; readonly initial: T }
26
+ /**
27
+ * An effect: the port. A request's job runs its handler outside the round
28
+ * once, and the result is visible for one round. A source's job keeps
29
+ * answering: `list` the slice of the outside as it is, `watch` its changes;
30
+ * a source with a `mirror` keeps that component of its system, and no system
31
+ * code writes it; one without lands events a step reads with `w.events`.
32
+ */
33
+ export interface Effect<N extends string = string, A = unknown, R = unknown, V = unknown, O extends string = string> {
34
+ readonly kind: "effect";
35
+ readonly name: N;
36
+ readonly owner: O;
37
+ readonly args: Schema<A>;
38
+ readonly result: Schema<R>;
39
+ readonly shape: "request" | "source";
40
+ /** A source's: the component it keeps, that component's entity field the job's entity goes in, the schema of a stream's events. */
41
+ readonly mirror: string | null;
42
+ readonly of: string | null;
43
+ readonly event: Schema<V> | null;
44
+ list: ((args: any, ctx: Context) => Promise<unknown[]> | unknown[]) | null;
45
+ watch: ((args: any, ctx: Context, last: any) => AsyncIterable<unknown>) | null;
46
+ /** Milliseconds before a failed job runs again; null to fail once. */
47
+ readonly retry: number | null;
48
+ /** Milliseconds between runs for the system itself; null for an effect only asked for. */
49
+ readonly every: number | null;
50
+ /** Safe to run again after a crash: a job that was running when the state was kept starts again; when false it lands as failed, "interrupted", once. */
51
+ readonly repeat: boolean;
52
+ readonly summary: string;
53
+ run(args: A, ctx: Context): Promise<R> | R;
54
+ }
55
+ export type Declaration = Component | Resource | Effect;
56
+
57
+ export type Value<C> = C extends Component<string, infer T> ? T : C extends Resource<string, infer T> ? T : never;
58
+ export type Args<E> = E extends Effect<string, infer A, any> ? A : never;
59
+ export type Result<E> = E extends Effect<string, any, infer R, any> ? R : never;
60
+ /** What a stream yields, as `w.events` answers it. */
61
+ export type EventOf<E> = E extends Effect<string, any, any, infer V> ? V : unknown;
62
+
63
+ /** The components a query's rows carry: its component terms, and the components of its `added` and `changed` terms. */
64
+ type Cols<Cs extends readonly Term[]> = Extract<Cs[number], Component> | Extract<Cs[number], Change<Component, "added" | "changed">>["component"];
65
+ /** A query's rows: the entity, then each component's value under its name; a `removed` term's last value under its name too. */
66
+ export type Row<Cs extends readonly Term[]> = Flat<{ entity: Entity } & { [C in Cols<Cs> as C["name"]]: Value<C> } & { [C in Extract<Cs[number], Change<Component, "removed">>["component"] as C["name"]]: Value<C> }>;
67
+ type Flat<T> = { [K in keyof T]: T[K] } & {};
68
+ /** The values `each` hands over, in term order: one per component term and per `added` or `changed` term. */
69
+ export type Values<Cs extends readonly unknown[]> = Cs extends readonly [infer H, ...infer T]
70
+ ? H extends Component ? [Value<H>, ...Values<T>] : H extends Change<infer C, "added" | "changed"> ? [Value<C>, ...Values<T>] : Values<T>
71
+ : [];
72
+
73
+ /** In a query: the entities that have none of these. `w.query(task, not(state, missing))`. */
74
+ export interface Not { readonly kind: "not"; readonly components: readonly Component[] }
75
+ export function not(...components: Component[]): Not { return { kind: "not", components }; }
76
+ /**
77
+ * In a query: what changed since `w.since`, which the runtime sets to the
78
+ * moment this system's steps last ran. `added(c)`: entities that gained c;
79
+ * `changed(c)`: entities whose c was written, gained included; `removed(c)`:
80
+ * entities that lost c, despawned ones included, with the value they had.
81
+ */
82
+ export interface Change<C extends Component = Component, How extends "added" | "changed" | "removed" = "added" | "changed" | "removed"> { readonly kind: "changed"; readonly how: How; readonly component: C }
83
+ export function added<C extends Component>(component: C): Change<C, "added"> { return { kind: "changed", how: "added", component }; }
84
+ export function changed<C extends Component>(component: C): Change<C, "changed"> { return { kind: "changed", how: "changed", component }; }
85
+ export function removed<C extends Component>(component: C): Change<C, "removed"> { return { kind: "changed", how: "removed", component }; }
86
+ /** A query's terms: any component of the state, since reads are open to every system, `not(…)`, and the change terms. */
87
+ export type Term = Component | Not | Change;
88
+
89
+ /**
90
+ * A query declared once, `select(fish, not(target)).where((r) => r.fish.hunger > 10)`,
91
+ * or kept in an order with `.by(fish, (a, b) => a.x - b.x)`;
92
+ * and run with `w.each(q, fn)` or `w.query(q)`: the state keeps its matched
93
+ * set live from the first run, so a step pays only the predicate. One
94
+ * with change terms is run like any other.
95
+ */
96
+ export interface Select<Cs extends readonly Term[] = readonly Term[]> {
97
+ readonly kind: "select";
98
+ readonly terms: Cs;
99
+ readonly pred: ((row: Row<Cs>) => boolean) | null;
100
+ /** The order its entities are kept in, by one component's value; null for id order. */
101
+ readonly order: { component: Component; cmp: (a: any, b: any) => number } | null;
102
+ /** The same select with a predicate over the row. */
103
+ where(pred: (row: Row<Cs>) => boolean): Select<Cs>;
104
+ /** The same select kept in the order `cmp` gives the values of `c`, which must be one of its terms. */
105
+ by<K extends Component>(c: K, cmp: (a: Value<K>, b: Value<K>) => number): Select<Cs>;
106
+ }
107
+ export function select<const Cs extends readonly Term[]>(...terms: Cs): Select<Cs> {
108
+ const build = (pred: Select<Cs>["pred"], order: Select<Cs>["order"]): Select<Cs> => ({
109
+ kind: "select", terms, pred, order,
110
+ where: (p) => build(p, order),
111
+ by: (c: Component, cmp: (a: unknown, b: unknown) => number) => {
112
+ if (!terms.some((t) => (t as { kind?: string; name?: string }).kind === "component" && (t as { name: string }).name === c.name)) throw new Error(`select: ${c.name} is not one of its terms`);
113
+ return build(pred, { component: c, cmp });
114
+ },
115
+ }) as Select<Cs>;
116
+ return build(null, null);
117
+ }
118
+
119
+ /**
120
+ * A job: one run of an effect, for an entity or for the system. `attempt`
121
+ * counts its failures so far, `since` is when it was asked or last failed,
122
+ * `error` the last failure's message. "interrupted": it was running when the
123
+ * state was kept and its effect is not safe to repeat; the next round lands
124
+ * it as failed.
125
+ */
126
+ export interface Job {
127
+ effect: Effect; entity: Entity | null; args: unknown; status: "waiting" | "running" | "retrying" | "interrupted"; attempt: number; since: number; error: string | null;
128
+ /** Which run this is: an answer from an earlier run of the same key is dropped. */
129
+ instance: number;
130
+ /** A source's: where it is, how many events it has landed, the round's time of the last. */
131
+ phase: "list" | "watch" | null; events: number; last: number;
132
+ }
133
+ /** What a mirror's watch may yield: an item of the component's shape, a key gone, or a word that the slice should be listed again. */
134
+ export type MirrorEvent = Record<string, unknown> | { gone: string | number } | { changed: true } | { resync: true };
135
+ export interface Done<E extends Effect = Effect> { entity: Entity | null; args: Args<E>; result: Result<E> }
136
+ export interface Failed<E extends Effect = Effect> { entity: Entity | null; args: Args<E>; error: string }
137
+
138
+ /** How a job last ended for an entity, or for a system's own job of an effect: `at` is the round's time it landed. */
139
+ export interface Settlement { effect: string; ok: boolean; error: string | null; at: number }
140
+
141
+ /**
142
+ * The whole state as JSON: the round, the resources, every entity, the jobs.
143
+ * `next` is the entity counter, so an id is never reused; `settled` each
144
+ * entity's last settlement by its id, a system's own jobs' by "system:<effect>";
145
+ * `keys` the answers remembered by idempotency key. Those three, and a job's
146
+ * attempt, since and error, are absent from snapshots kept before they existed.
147
+ */
148
+ export interface Snapshot {
149
+ round: number;
150
+ resources: Record<string, unknown>;
151
+ entities: Record<string, Record<string, unknown>>;
152
+ jobs: { effect: string; entity: Entity | null; args: unknown; status: Job["status"]; attempt?: number; since?: number; error?: string | null; shape?: "request" | "source"; phase?: Job["phase"]; events?: number; last?: number }[];
153
+ next?: number;
154
+ settled?: Record<string, Settlement>;
155
+ keys?: Record<string, Remembered>;
156
+ /** Which system made each entity, for those a system made: the one that may despawn it whatever it carries. */
157
+ made?: Record<string, string>;
158
+ /** The round's time, as `w.now` had it: when this state is from. */
159
+ now?: number;
160
+ /** The state of `w.random()`, so a kept state draws on as it would have. */
161
+ rng?: number;
162
+ }
163
+
164
+ /** An action's answer, remembered under its idempotency key, with the round's time it ran. */
165
+ export interface Remembered { value: unknown; at: number }
166
+
167
+ /** How long, and how many, idempotency keys are remembered. */
168
+ export const KEY_TTL_MS = 24 * 60 * 60 * 1000;
169
+ export const KEY_LIMIT = 1000;
170
+
171
+ /** What a baba declared, by kind, in system order. */
172
+ export interface Model { components: Component[]; resources: Resource[]; effects: Effect[] }
173
+
174
+ /** How many rounds back a delta can be measured from; a follower further behind gets the whole. */
175
+ export const DELTA_WINDOW = 256;
176
+
177
+ export class StateError extends Error {
178
+ constructor(readonly code: "entity" | "value" | "name" | "owner", msg: string) { super(msg); }
179
+ }
180
+
181
+ export const jobKey = (effect: Effect, entity: Entity | null) => `${effect.name}#${entity ?? ""}`;
182
+ /** Where a settlement is kept: the entity's id, or "system:<effect>" for a system's own job. */
183
+ export const settledKey = (effect: Effect | string, entity: Entity | null) => entity === null ? `system:${typeof effect === "string" ? effect : effect.name}` : String(entity);
184
+
185
+ /** The references of a system, or of every system of a baba, as the union of their values. */
186
+ type RefsOf<X> = X extends { kind: "baba"; systems: infer Cs extends readonly unknown[] } ? Cs[number] : X;
187
+ type ValuesOf<T> = T extends unknown ? T[keyof T] : never;
188
+ /** The components, resources and effects of a system or baba `X`, from its references; any of them for an untyped state. */
189
+ export type Comps<X> = unknown extends X ? Component : Extract<ValuesOf<RefsOf<X>>, Component>;
190
+ export type Ress<X> = unknown extends X ? Resource : Extract<ValuesOf<RefsOf<X>>, Resource>;
191
+ export type Effs<X> = unknown extends X ? Effect : Extract<ValuesOf<RefsOf<X>>, Effect>;
192
+
193
+ /**
194
+ * The state as a step sees it, typed by the system or baba `X` it belongs
195
+ * to. `writer` is the system whose code is running; null is the runtime
196
+ * itself, which may write anything. Checked at runtime for every caller.
197
+ */
198
+ export class State<X = any> {
199
+ /** The round's time, milliseconds since the epoch; set by the runtime before each round. */
200
+ now = 0;
201
+ /** Milliseconds since the previous round's time; 0 in the first. */
202
+ elapsed = 0;
203
+ /** The round running, or the last run. */
204
+ round = 0;
205
+ /** What change terms measure from: the moment this system's steps last ran, as the runtime sets it; 0 is ever. */
206
+ since = 0;
207
+ /** The system writing now, or null for the runtime and the shell. */
208
+ writer: string | null = null;
209
+ /** Every write checked against its schema, as a testbed wants; off, the system's code is trusted and only the boundary checks. */
210
+ checking = false;
211
+ /** The runtime's, while a source keeps its mirror: the one way a mirrored component is written. */
212
+ sourcing = false;
213
+ private readonly mirrored = new Map<string, Effect>();
214
+ /** The system that made each entity, when a system did. */
215
+ private made = new Map<Entity, string>();
216
+ private instances = 0;
217
+ private rng = 1;
218
+ /** The events landed this round, by effect name: what `w.events` answers. */
219
+ private landed = new Map<string, { entity: Entity | null; event: unknown }[]>();
220
+ private next: Entity = 1;
221
+ private readonly table: Table;
222
+ private values = new Map<Resource, unknown>();
223
+ private rtick = new Map<Resource, number>();
224
+ private byName = new Map<string, Declaration>();
225
+ private lastNow: number | null = null;
226
+ /** The sequence at the end of each recent round, for deltas. */
227
+ private ends = new Map<number, number>();
228
+ /** When the jobs, the settlements and the remembered answers last changed, as seen at a round's end: a delta carries each only when it did. */
229
+ private jobsTick = 0; private settledTick = 0; private keysTick = 0;
230
+ private jobsText = "[]";
231
+ readonly jobs = new Map<string, Job>();
232
+ private settledDone: Done[] = [];
233
+ private settledFailed: Failed[] = [];
234
+ private effectOf = new WeakMap<object, string>();
235
+ /** The jobs answered this round: asking for one again, this round, is nothing, whatever the order of steps. */
236
+ private answered = new Set<string>();
237
+ /** Each entity's last settlement, and each system job's under "system:<effect>". */
238
+ private settledLast = new Map<string, Settlement>();
239
+ /** Answers remembered by idempotency key, oldest first. */
240
+ private keys = new Map<string, Remembered>();
241
+ /** The undo list of the transaction open, if any. */
242
+ private journal: (() => void)[] | null = null;
243
+
244
+ constructor(readonly model: Model = { components: [], resources: [], effects: [] }) {
245
+ for (const d of [...model.components, ...model.resources, ...model.effects]) {
246
+ if (this.byName.has(d.name)) throw new StateError("name", `two declarations are named ${d.name}`);
247
+ this.byName.set(d.name, d);
248
+ }
249
+ this.table = new Table((name) => {
250
+ const c = this.byName.get(name);
251
+ const field = c?.kind === "component" ? keyField(c.schema.json) : null;
252
+ return field === null ? null : field === "" ? (v) => v : (v) => (v as Record<string, unknown>)[field];
253
+ });
254
+ for (const c of model.components) this.table.register(c.name);
255
+ for (const r of model.resources) this.values.set(r, r.schema.check(r.initial, r.name));
256
+ for (const e of model.effects) if (e.mirror) this.mirrored.set(e.mirror, e);
257
+ }
258
+
259
+ /** The source that keeps `c`, if one does. */
260
+ sourceOf(c: Component): Effect | undefined { return this.mirrored.get(c.name); }
261
+
262
+ /** The declaration of a name, if any. */
263
+ declaration(name: string): Declaration | undefined { return this.byName.get(name); }
264
+
265
+ private declared(d: Declaration): void {
266
+ if (this.byName.get(d.name) !== d) throw new StateError("name", `${d.kind} ${d.name} is not one of this state's`);
267
+ }
268
+
269
+ private owned(d: Declaration): void {
270
+ this.declared(d);
271
+ if (this.writer !== null && d.owner !== this.writer) throw new StateError("owner", `${d.kind} ${d.name} belongs to ${d.owner}; ${this.writer} may read it, not write it`);
272
+ this.kept(d);
273
+ }
274
+
275
+ /** The part of `owned` that holds for the owner too: a mirrored component is the source's. */
276
+ private kept(d: Declaration): void {
277
+ if (this.writer !== null && !this.sourcing && d.kind === "component" && this.mirrored.has(d.name)) throw new StateError("owner", `component ${d.name} is kept by the source ${this.mirrored.get(d.name)!.name}; ${this.writer} reads it, the outside writes it`);
278
+ }
279
+
280
+ /**
281
+ * Runs `fn` all-or-nothing: if it throws, every spawn, add, remove,
282
+ * despawn, set, run and cancel it made is undone, in reverse, and the
283
+ * error rethrown. A transaction inside another joins it.
284
+ */
285
+ transaction<T>(fn: () => T): T {
286
+ if (this.journal) return fn();
287
+ const undo: (() => void)[] = [];
288
+ this.journal = undo;
289
+ try { return fn(); } catch (err) {
290
+ this.journal = null;
291
+ for (let i = undo.length - 1; i >= 0; i--) undo[i]!();
292
+ throw err;
293
+ } finally { this.journal = null; }
294
+ }
295
+
296
+ private undo(f: () => void): void { this.journal?.push(f); }
297
+
298
+ // ---- entities and components ----------------------------------------
299
+
300
+ spawn(): Entity;
301
+ spawn<K extends Comps<X> | Ask>(c: K, value: Value<K>): Entity;
302
+ spawn(c?: Component, value?: unknown): Entity {
303
+ const e = this.next++;
304
+ this.table.spawn(e);
305
+ if (this.writer !== null) this.made.set(e, this.writer);
306
+ this.undo(() => { this.table.despawn(e); this.table.gone.delete(e); this.made.delete(e); this.next = e; });
307
+ if (c) this.add(e, c as never, value as never);
308
+ return e;
309
+ }
310
+
311
+ /** The system that made `e`, if a system did. */
312
+ maker(e: Entity): string | undefined { return this.made.get(e); }
313
+
314
+ /** A number in [0, 1) from the state's own generator: the same state, the same draws, so a round can be reproduced. Seed it with `seed()`. */
315
+ random(): number {
316
+ let x = this.rng >>> 0 || 1;
317
+ x ^= x << 13; x >>>= 0; x ^= x >>> 17; x ^= x << 5; x >>>= 0;
318
+ this.rng = x;
319
+ return x / 4294967296;
320
+ }
321
+ /** Seeds `random()`; the runtime seeds a fresh state from its clock, a testbed from 1. */
322
+ seed(n: number): void { this.rng = (n >>> 0) || 1; }
323
+
324
+ alive(e: Entity): boolean { return this.table.exists(e); }
325
+
326
+ /**
327
+ * Removes the entity, everything on it, and cancels its jobs. The system
328
+ * that made the entity despawns it whatever it carries: the asks it put
329
+ * there and the answers they drew go with it. Another system despawns only
330
+ * an entity carrying nothing but its own.
331
+ */
332
+ despawn(e: Entity): void {
333
+ this.exists(e);
334
+ const parts = this.table.partsOf(e);
335
+ if (this.writer !== null) for (const n of Object.keys(parts)) {
336
+ const c = this.byName.get(n)!;
337
+ if (!this.sourcing && this.mirrored.has(n)) throw new StateError("owner", `entity ${e} carries ${n}, kept by the source ${this.mirrored.get(n)!.name}; ${this.writer} may not despawn it`);
338
+ if (c.owner !== this.writer && this.made.get(e) !== this.writer) throw new StateError("owner", `entity ${e} carries ${n} of ${c.owner}; ${this.writer} did not make it and may not despawn it`);
339
+ }
340
+ const maker = this.made.get(e);
341
+ this.made.delete(e);
342
+ this.undo(() => { if (maker !== undefined) this.made.set(e, maker); });
343
+ const jobs = [...this.jobs].filter(([, j]) => j.entity === e);
344
+ const settled = this.settledLast.get(String(e));
345
+ this.table.despawn(e);
346
+ for (const [k] of jobs) this.jobs.delete(k);
347
+ if (this.settledLast.delete(String(e))) this.settledTick = ++this.table.seq;
348
+ this.undo(() => {
349
+ this.table.spawn(e);
350
+ for (const [n, v] of Object.entries(parts)) this.table.put(e, n, v);
351
+ for (const [k, j] of jobs) this.jobs.set(k, j);
352
+ if (settled) this.settledLast.set(String(e), settled);
353
+ });
354
+ }
355
+
356
+ /** Attaches or replaces `c` on `e`; a tag needs no value. Checked against its schema when `checking`. */
357
+ add<K extends (Comps<X> | Ask) & Component<string, true>>(e: Entity, c: K): void;
358
+ add<K extends Comps<X> | Ask>(e: Entity, c: K, value: Value<K>): void;
359
+ add(e: Entity, c: Component, value: unknown = true): void {
360
+ this.exists(e);
361
+ // An ask is anyone's to add once; replacing it is the owner's.
362
+ if (c.accept && this.writer !== null && c.owner !== this.writer) { this.declared(c); if (this.table.hasPart(e, c.name)) throw new StateError("owner", `entity ${e} already asks ${c.name}; an ask is not changed, the owner ${c.owner} answers it`); }
363
+ else this.owned(c);
364
+ this.put(e, c, this.checking ? this.check(c, value) : value);
365
+ }
366
+
367
+ /** A value checked against a declaration's schema, as the boundary does. */
368
+ check(d: Component | Resource, value: unknown): unknown {
369
+ try { return d.schema.check(value, d.name); } catch (err) { throw new StateError("value", err instanceof SchemaError ? err.message : String(err)); }
370
+ }
371
+
372
+ private put(e: Entity, c: Component, value: unknown): void {
373
+ const r = this.table.put(e, c.name, value);
374
+ if (this.journal) this.undo(() => { if (r.had) this.table.restore(e, c.name, r.was, r.tick, r.arrived); else this.table.remove(e, c.name); });
375
+ }
376
+
377
+ /** `c` on `e` replaced by what `fn` makes of it: a new value, never a change to the one stored. */
378
+ update<K extends Comps<X>>(e: Entity, c: K, fn: (value: Value<K>) => Value<K>): void;
379
+ update<K extends Ress<X>>(r: K, fn: (value: Value<K>) => Value<K>): void;
380
+ update(a: Entity | Resource, b: Component | ((v: never) => unknown), fn?: (v: never) => unknown): void {
381
+ if (typeof a === "number") {
382
+ const c = b as Component;
383
+ const v = this.get(a, c);
384
+ if (v === undefined) throw new StateError("entity", `entity ${a} has no ${c.name}`);
385
+ this.add(a, c as never, fn!(v as never) as never);
386
+ } else this.set(a as never, (b as (v: never) => unknown)(this.get(a) as never) as never);
387
+ }
388
+
389
+ get<K extends Component>(e: Entity, c: K): Value<K> | undefined;
390
+ get<K extends Resource>(r: K): Value<K>;
391
+ get(a: Entity | Resource, c?: Component): unknown {
392
+ if (typeof a === "number") return this.table.get(a, c!.name);
393
+ this.declared(a);
394
+ return this.values.get(a);
395
+ }
396
+
397
+ set<K extends Ress<X>>(r: K, value: Value<K>): void {
398
+ this.owned(r);
399
+ const was = this.values.get(r), tick = this.rtick.get(r) ?? 0;
400
+ this.values.set(r, this.checking ? this.check(r, value) : value);
401
+ this.rtick.set(r, ++this.table.seq);
402
+ this.undo(() => { this.values.set(r, was); this.rtick.set(r, tick); });
403
+ }
404
+
405
+ has(e: Entity, c: Component): boolean { return this.table.hasPart(e, c.name); }
406
+
407
+ remove(e: Entity, ...cs: Comps<X>[]): void {
408
+ this.exists(e);
409
+ for (const c of cs) {
410
+ this.owned(c);
411
+ const r = this.table.remove(e, c.name);
412
+ if (r.had && this.journal) this.undo(() => this.table.restore(e, c.name, r.was, r.tick, r.arrived));
413
+ }
414
+ }
415
+
416
+ /** The entity whose `c` has the key field equal to `key`, if any. */
417
+ find<K extends Comps<X>>(c: K, key: string | number): Entity | undefined {
418
+ this.declared(c);
419
+ if (keyField(c.schema.json) === null) throw new StateError("name", `component ${c.name} has no key field`);
420
+ return this.table.find(c.name, key);
421
+ }
422
+
423
+ /** Every entity matching the terms, in spawn order, each row keyed by component name. */
424
+ query<const Cs extends readonly Term[]>(...terms: Cs): Row<Cs>[];
425
+ query<Cs extends readonly Term[]>(sel: Select<Cs>): Row<Cs>[];
426
+ query(...terms: unknown[]): unknown[] {
427
+ const t = terms[0];
428
+ if (isSel(t)) { this.table.keep(t as never); return this.table.query(t as never, this.since); }
429
+ return this.table.query(terms as Term[], this.since);
430
+ }
431
+
432
+ /** Visits every entity matching the terms, in spawn order, with its values in term order; no rows are made. */
433
+ each<const Cs extends readonly Term[]>(terms: Cs, fn: (e: Entity, ...values: Values<Cs>) => void): void;
434
+ each<Cs extends readonly Term[]>(sel: Select<Cs>, fn: (e: Entity, ...values: Values<Cs>) => void): void;
435
+ each(terms: unknown, fn: (e: Entity, ...values: never[]) => void): void {
436
+ if (isSel(terms)) { this.table.keep(terms as never); this.table.each(terms as never, this.since, (e, v) => fn(e, ...(v as never[]))); return; }
437
+ this.table.each(terms as Term[], this.since, (e, v) => fn(e, ...(v as never[])));
438
+ }
439
+
440
+ /** How many entities have `c`. */
441
+ count(c: Component): number { return this.table.countOf(c.name); }
442
+ /** The entities that have `c`, in id order. */
443
+ idsOf(c: Component): Entity[] { return this.table.idsOf(c.name); }
444
+
445
+ /** Every component on `e`, by name. */
446
+ of(e: Entity): Record<string, unknown> { this.exists(e); return this.table.partsOf(e); }
447
+
448
+ /** Every entity, in order. */
449
+ all(): Entity[] { return this.table.all(); }
450
+
451
+ // ---- jobs --------------------------------------------------------------
452
+
453
+ /** Asks for a run of `effect`, for `entity` or for the system. Asking while one runs, or in the round its answer landed, is nothing. */
454
+ run<K extends Effs<X>>(effect: K, args?: Args<K>, entity?: Entity): void {
455
+ this.owned(effect);
456
+ if (entity !== undefined && !this.table.exists(entity)) throw new StateError("entity", `no entity ${entity}`);
457
+ const key = jobKey(effect, entity ?? null);
458
+ if (this.jobs.has(key) || this.answered.has(key)) return;
459
+ let checked: unknown;
460
+ try { checked = effect.args.check(args ?? {}, effect.name); } catch (err) { throw new StateError("value", err instanceof SchemaError ? err.message : String(err)); }
461
+ this.jobs.set(key, { effect, entity: entity ?? null, args: checked, status: "waiting", attempt: 0, since: this.now, error: null, instance: ++this.instances, phase: null, events: 0, last: 0 });
462
+ this.undo(() => this.jobs.delete(key));
463
+ }
464
+
465
+ /** What `effect`'s sources landed this round, in order: for a step to read. A mirror's events too, after they were kept. */
466
+ events<E extends Effect>(effect: E): { entity: Entity | null; event: EventOf<E> }[] {
467
+ this.declared(effect);
468
+ return (this.landed.get(effect.name) ?? []) as never;
469
+ }
470
+
471
+ /**
472
+ * The runtime's: a source's batch lands as the owner. A listing keeps the
473
+ * mirror as the slice says: every item put by its key, with the job's
474
+ * entity in `of`, unchanged values left alone, rows the listing did not
475
+ * name taken away; events put, take away, or ask to list again. Items the
476
+ * schema refuses are dropped and told. Answers whether to list again.
477
+ */
478
+ land(effect: Effect, entity: Entity | null, batch: { listed: unknown[] } | { events: unknown[] }, dropped: (what: string) => void): { relist: boolean } {
479
+ const c = effect.mirror ? this.byName.get(effect.mirror) as Component : null;
480
+ const events = "listed" in batch ? batch.listed : batch.events;
481
+ if (!this.landed.has(effect.name)) this.landed.set(effect.name, []);
482
+ const seen = this.landed.get(effect.name)!;
483
+ for (const event of events) seen.push({ entity, event });
484
+ if (!c) {
485
+ if (effect.event) for (const [i, ev] of events.entries()) { try { effect.event.check(ev, effect.name); } catch (err) { dropped(`${effect.name}: event ${i}: ${err instanceof Error ? err.message : String(err)}`); seen.splice(seen.findIndex((s) => s.event === ev), 1); } }
486
+ return { relist: false };
487
+ }
488
+ const field = keyField(c.schema.json)!;
489
+ const keyOf = (v: unknown) => field === "" ? v : (v as Record<string, unknown>)[field];
490
+ if (entity !== null && !effect.of) { dropped(`${effect.name}: a mirror kept for an entity needs an entity field on ${c.name} for the job's entity`); return { relist: false }; }
491
+ const was = this.sourcing; this.sourcing = true;
492
+ let relist = false;
493
+ try {
494
+ const touched = new Set<Entity>();
495
+ const put = (item: unknown) => {
496
+ const value = effect.of && typeof item === "object" && item !== null ? { ...(item as Record<string, unknown>), [effect.of]: entity } : item;
497
+ let checked: unknown;
498
+ try { checked = c.schema.check(value, c.name); } catch (err) { dropped(`${effect.name}: ${err instanceof Error ? err.message : String(err)}`); return; }
499
+ const key = keyOf(checked);
500
+ let e = this.table.find(c.name, key as never);
501
+ if (e === undefined) { e = this.spawn(); this.add(e, c as never, checked as never); }
502
+ else if (JSON.stringify(this.get(e, c)) !== JSON.stringify(checked)) this.add(e, c as never, checked as never);
503
+ touched.add(e);
504
+ };
505
+ const take = (e: Entity) => { this.remove(e, c as never); if (Object.keys(this.table.partsOf(e)).length === 0) this.despawn(e); };
506
+ if ("listed" in batch) {
507
+ for (const item of batch.listed) put(item);
508
+ const mine = this.query(c).filter((r) => !touched.has(r.entity) && (effect.of ? (r as Record<string, Record<string, unknown>>)[c.name]![effect.of] === entity : true));
509
+ for (const r of mine) take(r.entity);
510
+ } else {
511
+ for (const ev of batch.events) {
512
+ if (typeof ev !== "object" || ev === null) { dropped(`${effect.name}: an event that is not an object`); continue; }
513
+ const o = ev as Record<string, unknown>;
514
+ if (o.changed === true || o.resync === true) { relist = true; continue; }
515
+ if ("gone" in o && Object.keys(o).length === 1) { const e = this.table.find(c.name, o.gone as never); if (e !== undefined) take(e); continue; }
516
+ put(o);
517
+ }
518
+ }
519
+ } finally { this.sourcing = was; }
520
+ return { relist };
521
+ }
522
+
523
+ running(effect: Effect, entity?: Entity): boolean { return this.jobs.has(jobKey(effect, entity ?? null)); }
524
+
525
+ cancel(effect: Effs<X>, entity?: Entity): void {
526
+ this.owned(effect);
527
+ const key = jobKey(effect, entity ?? null);
528
+ const job = this.jobs.get(key);
529
+ if (!job) return;
530
+ this.jobs.delete(key);
531
+ this.undo(() => this.jobs.set(key, job));
532
+ }
533
+
534
+ /** How the last job for `entity` ended, or the system's own last job of `effect`; kept until the next one lands, dropped with the entity. */
535
+ settlement(of: Entity | Effect): Settlement | undefined {
536
+ return this.settledLast.get(typeof of === "number" ? String(of) : settledKey(of, null));
537
+ }
538
+
539
+ /** What `effect` answered since the last round: for this round only. */
540
+ done<K extends Effect>(effect: K): Done<K>[] { return this.settledDone.filter((d) => this.effectOf.get(d) === effect.name) as Done<K>[]; }
541
+ failed<K extends Effect>(effect: K): Failed<K>[] { return this.settledFailed.filter((d) => this.effectOf.get(d) === effect.name) as Failed<K>[]; }
542
+
543
+ /** The runtime's: a job's answer, applied before a round. A late answer to a gone entity or a cancelled job is dropped. `retry` false lands a failure once, whatever the effect says. */
544
+ settle(effect: Effect, entity: Entity | null, ok: boolean, value: unknown, retry = true): "kept" | "dropped" | "retry" {
545
+ const key = jobKey(effect, entity);
546
+ const job = this.jobs.get(key);
547
+ if (!job || (entity !== null && !this.table.exists(entity))) return "dropped";
548
+ if (ok) {
549
+ let checked: unknown;
550
+ try { checked = effect.result.check(value, effect.name); } catch (err) { ok = false; value = `${effect.name} returned a bad result: ${err instanceof Error ? err.message : String(err)}`; }
551
+ if (ok) {
552
+ this.jobs.delete(key);
553
+ this.answered.add(key);
554
+ const d: Done = { entity, args: job.args, result: checked };
555
+ this.effectOf.set(d, effect.name);
556
+ this.settledDone.push(d);
557
+ this.settledLast.set(settledKey(effect, entity), { effect: effect.name, ok: true, error: null, at: this.now });
558
+ this.settledTick = ++this.table.seq;
559
+ return "kept";
560
+ }
561
+ }
562
+ const f: Failed = { entity, args: job.args, error: String(value) };
563
+ this.effectOf.set(f, effect.name);
564
+ this.settledFailed.push(f);
565
+ this.settledLast.set(settledKey(effect, entity), { effect: effect.name, ok: false, error: f.error, at: this.now });
566
+ this.settledTick = ++this.table.seq;
567
+ job.error = f.error;
568
+ if (effect.retry === null || !retry) { this.jobs.delete(key); this.answered.add(key); return "kept"; }
569
+ job.status = "retrying";
570
+ job.attempt++;
571
+ job.since = this.now;
572
+ return "retry";
573
+ }
574
+
575
+ /** The runtime's: round `round` begins at `now`; the last round's answers are gone. */
576
+ begin(now: number, round = this.round + 1): void {
577
+ this.elapsed = this.lastNow === null ? 0 : Math.max(0, now - this.lastNow);
578
+ this.lastNow = now;
579
+ this.now = now;
580
+ this.round = round;
581
+ this.settledDone = [];
582
+ this.settledFailed = [];
583
+ this.landed.clear();
584
+ this.answered.clear();
585
+ for (const [k, r] of this.keys) if (r.at < now - KEY_TTL_MS) { this.keys.delete(k); this.keysTick = ++this.table.seq; }
586
+ const old = round - DELTA_WINDOW;
587
+ const at = this.ends.get(old);
588
+ for (const r of this.ends.keys()) if (r < old) this.ends.delete(r);
589
+ if (at !== undefined) this.table.prune(at);
590
+ }
591
+
592
+ /**
593
+ * The runtime's, at the round's end: every value written since `since`
594
+ * checked against its schema, so the state holds only what the model
595
+ * admits whatever a trusted write did. A component that fails is dropped,
596
+ * which the next round sees as a removal; a resource that fails returns
597
+ * to its initial value. Each is told through `dropped`. Costs what
598
+ * changed, not what exists.
599
+ */
600
+ audit(since: number, dropped: (what: string) => void): void {
601
+ for (const [e, name, v] of this.table.changedParts(since)) {
602
+ const c = this.byName.get(name) as Component;
603
+ try { c.schema.check(v, name); } catch (err) {
604
+ this.table.remove(e, name);
605
+ dropped(`entity ${e}: ${err instanceof SchemaError ? err.message : String(err)}; ${name} dropped`);
606
+ }
607
+ }
608
+ for (const [r, tick] of this.rtick) {
609
+ if (tick <= since) continue;
610
+ try { r.schema.check(this.values.get(r), r.name); } catch (err) {
611
+ this.values.set(r, r.schema.check(r.initial, r.name));
612
+ this.rtick.set(r, ++this.table.seq);
613
+ dropped(`resource ${r.name}: ${err instanceof SchemaError ? err.message : String(err)}; its initial value stands`);
614
+ }
615
+ }
616
+ }
617
+
618
+ /** The runtime's: the round is over; what a delta since it measures from is remembered, and whether the jobs, settlements and keys moved. */
619
+ end(): void {
620
+ const jobs = JSON.stringify(this.jobsOf());
621
+ if (jobs !== this.jobsText) { this.jobsText = jobs; this.jobsTick = ++this.table.seq; }
622
+ this.ends.set(this.round, this.table.seq);
623
+ }
624
+
625
+ /** The sequence now: what `since` is set to for a system whose steps just ran. */
626
+ get seq(): number { return this.table.seq; }
627
+
628
+ /**
629
+ * What changed since round `since` ended, or null when that is too long
630
+ * ago or never, so the whole is sent instead: per entity the components
631
+ * written and the ones taken away, the resources written, and the jobs,
632
+ * settlements and remembered answers each only when it moved. Since the
633
+ * round that just ended, it is empty.
634
+ */
635
+ delta(since: number): Delta | null {
636
+ const from = this.ends.get(since);
637
+ if (from === undefined) return null;
638
+ const { patches, gone, added } = this.table.changes(from);
639
+ const entities: Delta["entities"] = {};
640
+ const made: Record<string, string> = {};
641
+ for (const [e, patch] of patches) { entities[String(e)] = patch; const c = this.made.get(e); if (c !== undefined) made[String(e)] = c; }
642
+ for (const e of gone) entities[String(e)] = null;
643
+ const resources: Record<string, unknown> = {};
644
+ for (const [r, v] of this.values) if ((this.rtick.get(r) ?? 0) > from) resources[r.name] = v;
645
+ const d: Delta = { kind: "delta", round: this.round, since, entities, resources, next: this.next, made, now: this.now, rng: this.rng };
646
+ if (added.size) d.added = Object.fromEntries([...added].map(([e, names]) => [String(e), names]));
647
+ if (this.jobsTick > from) d.jobs = this.jobsOf();
648
+ if (this.settledTick > from) d.settled = Object.fromEntries(this.settledLast);
649
+ if (this.keysTick > from) d.keys = Object.fromEntries(this.keys);
650
+ return d;
651
+ }
652
+
653
+ private jobsOf(): Snapshot["jobs"] {
654
+ return [...this.jobs.values()].map((j) => ({ effect: j.effect.name, entity: j.entity, args: j.args, status: j.status, attempt: j.attempt, since: j.since, error: j.error, ...(j.effect.shape === "source" ? { shape: "source" as const, phase: j.phase, events: j.events, last: j.last } : {}) }));
655
+ }
656
+
657
+ // ---- idempotency keys ----------------------------------------------------
658
+
659
+ /** The answer remembered under `key`, if any. */
660
+ recall(key: string): Remembered | undefined { return this.keys.get(key); }
661
+
662
+ /** Remembers `value` under `key` at the round's time; the oldest go past KEY_LIMIT. */
663
+ remember(key: string, value: unknown): void {
664
+ this.keys.delete(key);
665
+ this.keys.set(key, { value, at: this.now });
666
+ this.keysTick = ++this.table.seq;
667
+ for (const k of this.keys.keys()) { if (this.keys.size <= KEY_LIMIT) break; this.keys.delete(k); }
668
+ }
669
+
670
+ /** The jobs that were running when the state was kept, of effects not safe to repeat: the runtime lands each as failed. */
671
+ interrupted(): Job[] { return [...this.jobs.values()].filter((j) => j.status === "interrupted"); }
672
+
673
+ snapshot(): Snapshot {
674
+ const entities: Snapshot["entities"] = {};
675
+ for (const e of this.table.all()) entities[String(e)] = this.table.partsOf(e);
676
+ const resources: Record<string, unknown> = {};
677
+ for (const [r, v] of this.values) resources[r.name] = v;
678
+ return {
679
+ round: this.round, resources, entities,
680
+ jobs: this.jobsOf(),
681
+ next: this.next,
682
+ settled: Object.fromEntries(this.settledLast),
683
+ keys: Object.fromEntries(this.keys),
684
+ made: Object.fromEntries([...this.made].map(([e, c]) => [String(e), c])),
685
+ now: this.now,
686
+ rng: this.rng,
687
+ };
688
+ }
689
+
690
+ /**
691
+ * A state from a snapshot, fitted to the model: what the model no longer
692
+ * declares, or a value its schema no longer admits, is dropped and told
693
+ * through `dropped`, never refused, so the state never holds the code
694
+ * back. An entity left with nothing stays only while a kept value still
695
+ * points at it. A value dropped shows to `removed(c)` in the first round,
696
+ * as a write would. Jobs come back waiting, to run again, keeping their
697
+ * attempt and last error; a job that was running, of an effect not safe
698
+ * to repeat, comes back interrupted instead.
699
+ */
700
+ static restore(snap: Snapshot, model: Model, dropped: (what: string) => void = () => {}): State {
701
+ const w = new State(model);
702
+ w.round = snap.round ?? 0;
703
+ for (const [name, v] of Object.entries(snap.resources ?? {})) {
704
+ const r = w.byName.get(name);
705
+ if (!r || r.kind !== "resource") { dropped(`resource ${name}: no longer declared`); continue; }
706
+ try { w.set(r as never, w.check(r, v) as never); } catch (err) { dropped(`resource ${name}: ${err instanceof Error ? err.message : String(err)}; its initial value stands`); }
707
+ }
708
+ const ids = Object.keys(snap.entities ?? {}).map(Number).filter((e) => Number.isInteger(e) && e >= 1).sort((a, b) => a - b);
709
+ const kept = new Map<Entity, [Component, unknown][]>();
710
+ for (const e of ids) {
711
+ const parts: [Component, unknown][] = [];
712
+ for (const [name, v] of Object.entries(snap.entities[String(e)]!)) {
713
+ const c = w.byName.get(name);
714
+ if (!c || c.kind !== "component") { dropped(`entity ${e}: ${name} is no longer declared`); continue; }
715
+ try { parts.push([c, w.check(c, v)]); } catch (err) { dropped(`entity ${e}: ${err instanceof Error ? err.message : String(err)}`); w.table.forget(e, c.name, v); }
716
+ }
717
+ kept.set(e, parts);
718
+ }
719
+ // What a kept value points at: those entities stay even when nothing is left on them.
720
+ const referenced = new Set<Entity>();
721
+ const note = (json: Json, v: unknown) => { mapEntities(json, v, (_, id) => { if (typeof id === "number") referenced.add(id); return id; }); };
722
+ for (const parts of kept.values()) for (const [c, v] of parts) note(c.schema.json, v);
723
+ for (const [r, v] of w.values) note(r.schema.json, v);
724
+ for (const [e, parts] of kept) {
725
+ if (parts.length === 0 && !referenced.has(e)) { if (Object.keys(snap.entities[String(e)]!).length) dropped(`entity ${e}: nothing left on it and nothing points at it`); continue; }
726
+ w.table.spawn(e);
727
+ for (const [c, v] of parts) w.put(e, c, v);
728
+ }
729
+ // The counter as kept; a snapshot from before it was kept falls back to past the highest id.
730
+ w.next = Math.max(...ids.map((e) => e + 1), Number.isInteger(snap.next) ? snap.next! : 1, 1);
731
+ for (const j of snap.jobs ?? []) {
732
+ const eff = w.byName.get(j.effect);
733
+ if (!eff || eff.kind !== "effect") { dropped(`job of ${j.effect}: no longer declared`); continue; }
734
+ if (j.entity !== null && !w.table.exists(j.entity)) continue;
735
+ let args: unknown;
736
+ try { args = eff.args.check(j.args, eff.name); } catch (err) { dropped(`job of ${j.effect}: ${err instanceof Error ? err.message : String(err)}`); continue; }
737
+ const interrupted = j.status === "interrupted" || (j.status === "running" && !eff.repeat);
738
+ w.jobs.set(jobKey(eff, j.entity), { effect: eff, entity: j.entity, args, status: interrupted ? "interrupted" : "waiting", attempt: j.attempt ?? 0, since: j.since ?? 0, error: j.error ?? null, instance: ++w.instances, phase: null, events: j.events ?? 0, last: j.last ?? 0 });
739
+ }
740
+ for (const [k, s] of Object.entries(snap.settled ?? {})) {
741
+ if (w.byName.get(s.effect)?.kind !== "effect") { dropped(`settlement of ${s.effect}: no longer declared`); continue; }
742
+ // "cell:<effect>" is how a state kept before systems were named so keyed a system's own job.
743
+ if (k.startsWith("system:") || k.startsWith("cell:")) w.settledLast.set(settledKey(s.effect, null), s);
744
+ else if (w.table.exists(Number(k))) w.settledLast.set(k, s);
745
+ }
746
+ for (const [k, r] of Object.entries(snap.keys ?? {}).sort((a, b) => a[1].at - b[1].at)) w.keys.set(k, r);
747
+ for (const [id, c] of Object.entries(snap.made ?? {})) if (w.table.exists(Number(id)) && w.byName.size) w.made.set(Number(id), c);
748
+ if (typeof snap.rng === "number") w.rng = snap.rng >>> 0 || 1;
749
+ return w;
750
+ }
751
+
752
+ private exists(e: Entity): void {
753
+ if (!this.table.exists(e)) throw new StateError("entity", `no entity ${e}`);
754
+ }
755
+ }
756
+
757
+ const isSel = (x: unknown): x is Select => typeof x === "object" && x !== null && (x as { kind?: unknown }).kind === "select";
758
+
759
+ export type { Schema, Json };