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,420 @@
1
+ // The storage under the state and under a replica: entities as a bitmask of
2
+ // the components each has, and one store per component holding its values
3
+ // in entity order. A query walks the smallest store of its required terms
4
+ // and tests masks; `not(…)` is a bit test. Every value and every entity's
5
+ // component set carries the sequence number of its last change, which is
6
+ // what change terms (`added`, `changed`, `removed`) and deltas read. Pure
7
+ // data structure, browser-safe: components are known by name here.
8
+ import type { Entity } from "./state.ts";
9
+
10
+ export type Mask = Uint32Array;
11
+ const BITS = 32;
12
+
13
+ /** A term's shape as the table sees it: a component by name, a not, or a change term. */
14
+ export type Named = { kind: "component"; name: string } | { kind: "not"; components: readonly { name: string }[] } | { kind: "changed"; how: "added" | "changed" | "removed"; component: { name: string } };
15
+
16
+ /** A query declared once: its terms, the predicate over the row, the order by one component's value. Its matched set is kept live by the table it runs in. */
17
+ export interface Select<R = Record<string, unknown>> { readonly kind: "select"; readonly terms: readonly Named[]; readonly pred: ((row: R) => boolean) | null; readonly order: { component: { name: string }; cmp: (a: unknown, b: unknown) => number } | null }
18
+
19
+ /**
20
+ * A select kept live: its plan, its matched ids in id order, and a version
21
+ * that moves when the set changes, and, for a select with a predicate or an
22
+ * order, when a value of one of its components is written on a member. A
23
+ * watcher compares versions; the answer is made once per version.
24
+ */
25
+ interface Live { plan: Plan; ids: number[]; version: number; sensitive: boolean; keyed: Store | null; cache: { version: number; ids: readonly number[] } | null }
26
+
27
+ /** A query resolved against this table: masks, the stores to walk, and the names to put in a row. */
28
+ interface Plan {
29
+ required: Mask; excluded: Mask;
30
+ comps: { name: string; store: Store }[];
31
+ changes: { how: "added" | "changed" | "removed"; name: string; store: Store }[];
32
+ }
33
+
34
+ /** One component's values, in entity order, with the sequence of each one's last write and of its arrival. */
35
+ class Store {
36
+ ids: number[] = [];
37
+ vals: unknown[] = [];
38
+ ticks: number[] = [];
39
+ arrived: number[] = [];
40
+ /** Entities that lost it, each with the value they had and when. */
41
+ removed = new Map<number, { seq: number; value: unknown }>();
42
+ /** The key field's values to entities, when the component has one. */
43
+ keys: Map<unknown, number> | null = null;
44
+ constructor(readonly name: string, readonly id: number, readonly keyOf: ((value: unknown) => unknown) | null) { if (keyOf) this.keys = new Map(); }
45
+
46
+ /** The position of `e`, or the bitwise complement of where it would go. */
47
+ at(e: number): number {
48
+ let lo = 0, hi = this.ids.length - 1;
49
+ while (lo <= hi) { const mid = (lo + hi) >>> 1; const v = this.ids[mid]!; if (v === e) return mid; if (v < e) lo = mid + 1; else hi = mid - 1; }
50
+ return ~lo;
51
+ }
52
+ }
53
+
54
+ const grow = <T extends Uint32Array | Uint8Array | Float64Array>(a: T, n: number): T => { const b = new (a.constructor as new (n: number) => T)(n); b.set(a as never); return b; };
55
+
56
+ export class Table {
57
+ private stores = new Map<string, Store>();
58
+ private byId: Store[] = [];
59
+ private words = 1;
60
+ private capacity = 64;
61
+ private masks: Mask = new Uint32Array(this.capacity * this.words);
62
+ private alive = new Uint8Array(this.capacity);
63
+ /** When each entity's component set last changed. */
64
+ private etick = new Float64Array(this.capacity);
65
+ /** When anything on each entity was last written: a part put, taken away, the entity spawned or gone. */
66
+ private wtick = new Float64Array(this.capacity);
67
+ /** Entities despawned, and when. */
68
+ readonly gone = new Map<number, number>();
69
+ /** The sequence of the last change; every write takes the next. */
70
+ seq = 0;
71
+ private count = 0;
72
+ private highest = 0;
73
+ private live = new Map<Select, Live>();
74
+
75
+ constructor(private keyOf: (name: string) => ((value: unknown) => unknown) | null = () => null) {}
76
+
77
+ /** The table knows the component, assigning its id. */
78
+ register(name: string): Store {
79
+ let s = this.stores.get(name);
80
+ if (s) return s;
81
+ const id = this.byId.length;
82
+ s = new Store(name, id, this.keyOf(name));
83
+ this.stores.set(name, s);
84
+ this.byId.push(s);
85
+ const words = Math.ceil(this.byId.length / BITS);
86
+ if (words > this.words) {
87
+ const masks = new Uint32Array(this.capacity * words);
88
+ for (let e = 0; e < this.capacity; e++) for (let k = 0; k < this.words; k++) masks[e * words + k] = this.masks[e * this.words + k]!;
89
+ this.masks = masks; this.words = words;
90
+ for (const l of this.live.values()) l.plan = this.plan(this.termsOf(l.plan));
91
+ }
92
+ return s;
93
+ }
94
+
95
+ has(name: string): boolean { return this.stores.has(name); }
96
+ names(): string[] { return this.byId.map((s) => s.name); }
97
+
98
+ private room(e: number): void {
99
+ if (e < this.capacity) return;
100
+ let cap = this.capacity;
101
+ while (cap <= e) cap *= 2;
102
+ this.masks = grow(this.masks, cap * this.words);
103
+ this.alive = grow(this.alive, cap);
104
+ this.etick = grow(this.etick, cap);
105
+ this.wtick = grow(this.wtick, cap);
106
+ this.capacity = cap;
107
+ }
108
+
109
+ // ---- entities ------------------------------------------------------------
110
+
111
+ exists(e: number): boolean { return e < this.capacity && this.alive[e] === 1; }
112
+ size(): number { return this.count; }
113
+ /** Every entity, in id order. */
114
+ all(): number[] { const out: number[] = []; for (let e = 1; e <= this.highest; e++) if (this.alive[e] === 1) out.push(e); return out; }
115
+
116
+ spawn(e: number): void {
117
+ this.room(e);
118
+ if (this.alive[e] === 1) throw new Error(`entity ${e} exists`);
119
+ this.alive[e] = 1;
120
+ this.count++;
121
+ if (e > this.highest) this.highest = e;
122
+ this.etick[e] = ++this.seq;
123
+ this.wtick[e] = this.seq;
124
+ this.gone.delete(e);
125
+ }
126
+
127
+ /** Removes the entity and every value on it; what it carried, for an undo. */
128
+ despawn(e: number): Record<string, unknown> {
129
+ const parts = this.partsOf(e);
130
+ for (const name of Object.keys(parts)) this.remove(e, name);
131
+ this.alive[e] = 0;
132
+ this.count--;
133
+ this.gone.set(e, ++this.seq);
134
+ this.etick[e] = this.seq;
135
+ this.wtick[e] = this.seq;
136
+ return parts;
137
+ }
138
+
139
+ /** When the entity's component set last changed. */
140
+ tickOf(e: number): number { return this.etick[e] ?? 0; }
141
+ /** When anything on the entity was last written. */
142
+ writeTick(e: number): number { return this.wtick[e] ?? 0; }
143
+ /** When the entity's value of `name` was last written, or -1 without one. */
144
+ valueTick(e: number, name: string): number { const s = this.stores.get(name); if (!s) return -1; const i = s.at(e); return i >= 0 ? s.ticks[i]! : -1; }
145
+
146
+ // ---- values --------------------------------------------------------------
147
+
148
+ get(e: number, name: string): unknown {
149
+ const s = this.stores.get(name);
150
+ if (!s) return undefined;
151
+ const i = s.at(e);
152
+ return i >= 0 ? s.vals[i] : undefined;
153
+ }
154
+
155
+ hasPart(e: number, name: string): boolean {
156
+ const s = this.stores.get(name);
157
+ return !!s && this.bit(e, s.id);
158
+ }
159
+
160
+ private bit(e: number, id: number): boolean { return ((this.masks[e * this.words + (id >>> 5)]! >>> (id & 31)) & 1) === 1; }
161
+
162
+ /** Attaches or replaces; the value before, if any, and whether it was there. */
163
+ put(e: number, name: string, value: unknown): { had: boolean; was: unknown; tick: number; arrived: number } {
164
+ const s = this.register(name);
165
+ const i = s.at(e);
166
+ const seq = ++this.seq;
167
+ if (i >= 0) {
168
+ const was = s.vals[i], tick = s.ticks[i]!, arrived = s.arrived[i]!;
169
+ if (s.keys) { s.keys.delete(s.keyOf!(was)); s.keys.set(s.keyOf!(value), e); }
170
+ s.vals[i] = value; s.ticks[i] = seq;
171
+ this.wtick[e] = seq;
172
+ if (this.live.size) this.touched(e, s);
173
+ return { had: true, was, tick, arrived };
174
+ }
175
+ const at = ~i;
176
+ s.ids.splice(at, 0, e); s.vals.splice(at, 0, value); s.ticks.splice(at, 0, seq); s.arrived.splice(at, 0, seq);
177
+ s.removed.delete(e);
178
+ if (s.keys) s.keys.set(s.keyOf!(value), e);
179
+ this.setBit(e, s.id, true, seq);
180
+ return { had: false, was: undefined, tick: 0, arrived: 0 };
181
+ }
182
+
183
+ /** Puts back a value with the ticks it had: an undo. */
184
+ restore(e: number, name: string, value: unknown, tick: number, arrived: number): void {
185
+ const s = this.register(name);
186
+ const i = s.at(e);
187
+ if (i >= 0) { s.vals[i] = value; s.ticks[i] = tick; if (s.keys) s.keys.set(s.keyOf!(value), e); return; }
188
+ this.put(e, name, value);
189
+ const j = s.at(e);
190
+ s.ticks[j] = tick; s.arrived[j] = arrived;
191
+ }
192
+
193
+ /** Detaches; the value it had, or undefined when it was not there. */
194
+ remove(e: number, name: string): { had: boolean; was: unknown; tick: number; arrived: number } {
195
+ const s = this.stores.get(name);
196
+ if (!s) return { had: false, was: undefined, tick: 0, arrived: 0 };
197
+ const i = s.at(e);
198
+ if (i < 0) return { had: false, was: undefined, tick: 0, arrived: 0 };
199
+ const was = s.vals[i], tick = s.ticks[i]!, arrived = s.arrived[i]!;
200
+ s.ids.splice(i, 1); s.vals.splice(i, 1); s.ticks.splice(i, 1); s.arrived.splice(i, 1);
201
+ if (s.keys) s.keys.delete(s.keyOf!(was));
202
+ const seq = ++this.seq;
203
+ s.removed.set(e, { seq, value: was });
204
+ this.setBit(e, s.id, false, seq);
205
+ return { had: true, was, tick, arrived };
206
+ }
207
+
208
+ /** Records that `e` lost `name` with `value` without ever holding it here: a kept value dropped on load, so change terms see it go. */
209
+ forget(e: number, name: string, value: unknown): void {
210
+ this.register(name).removed.set(e, { seq: ++this.seq, value });
211
+ }
212
+
213
+ private setBit(e: number, id: number, on: boolean, seq: number): void {
214
+ const w = e * this.words + (id >>> 5), b = 1 << (id & 31);
215
+ const before = this.masks[w]!;
216
+ this.masks[w] = on ? before | b : before & ~b;
217
+ this.etick[e] = seq;
218
+ this.wtick[e] = seq;
219
+ if (this.live.size) this.relive(e);
220
+ }
221
+
222
+ /** A value of `s` was written on `e`: every sensitive live select `e` is in moves its version. */
223
+ private touched(e: number, s: Store): void {
224
+ for (const l of this.live.values()) {
225
+ if (!l.sensitive || ((l.plan.required[s.id >>> 5]! >>> (s.id & 31)) & 1) !== 1) continue;
226
+ if (this.matches(e, l.plan)) l.version++;
227
+ }
228
+ }
229
+
230
+ /** The entity's values by component name, in id order of the components. */
231
+ partsOf(e: number): Record<string, unknown> {
232
+ const o: Record<string, unknown> = {};
233
+ for (const s of this.byId) if (this.bit(e, s.id)) o[s.name] = s.vals[s.at(e)];
234
+ return o;
235
+ }
236
+
237
+ /** Makes the entity carry exactly these parts: a replica applying a delta. */
238
+ setParts(e: number, parts: Record<string, unknown>): void {
239
+ if (!this.exists(e)) this.spawn(e);
240
+ for (const s of this.byId) if (this.bit(e, s.id) && !(s.name in parts)) this.remove(e, s.name);
241
+ for (const [name, v] of Object.entries(parts)) this.put(e, name, v);
242
+ }
243
+
244
+ /** The entity whose `name` has the key `key`. */
245
+ find(name: string, key: unknown): number | undefined {
246
+ const s = this.stores.get(name);
247
+ return s?.keys?.get(key);
248
+ }
249
+
250
+ /** How many entities have the component. */
251
+ countOf(name: string): number { return this.stores.get(name)?.ids.length ?? 0; }
252
+ /** The entities that have the component, in id order: a copy. */
253
+ idsOf(name: string): number[] { return [...(this.stores.get(name)?.ids ?? [])]; }
254
+
255
+ // ---- queries -------------------------------------------------------------
256
+
257
+ private termsOf(p: Plan): Named[] {
258
+ const out: Named[] = p.comps.map((c) => ({ kind: "component", name: c.name }));
259
+ const nots: { name: string }[] = [];
260
+ for (const s of this.byId) if (((p.excluded[s.id >>> 5]! >>> (s.id & 31)) & 1) === 1) nots.push({ name: s.name });
261
+ if (nots.length) out.push({ kind: "not", components: nots });
262
+ for (const c of p.changes) out.push({ kind: "changed", how: c.how, component: { name: c.name } });
263
+ return out;
264
+ }
265
+
266
+ private plan(terms: readonly Named[]): Plan {
267
+ const required = new Uint32Array(this.words), excluded = new Uint32Array(this.words);
268
+ const comps: Plan["comps"] = [], changes: Plan["changes"] = [];
269
+ const on = (m: Mask, id: number) => { m[id >>> 5] = (m[id >>> 5]! | (1 << (id & 31))) >>> 0; };
270
+ for (const t of terms) {
271
+ if (t.kind === "component") { const s = this.register(t.name); on(required, s.id); comps.push({ name: s.name, store: s }); }
272
+ else if (t.kind === "not") for (const c of t.components) on(excluded, this.register(c.name).id);
273
+ else {
274
+ const s = this.register(t.component.name);
275
+ if (t.how !== "removed") { on(required, s.id); comps.push({ name: s.name, store: s }); }
276
+ changes.push({ how: t.how, name: s.name, store: s });
277
+ }
278
+ }
279
+ return { required, excluded, comps, changes };
280
+ }
281
+
282
+ private matches(e: number, p: Plan): boolean {
283
+ const base = e * this.words;
284
+ for (let k = 0; k < this.words; k++) {
285
+ const m = this.masks[base + k]!;
286
+ if (((m & p.required[k]!) >>> 0) !== p.required[k]! || (m & p.excluded[k]!) !== 0) return false;
287
+ }
288
+ return true;
289
+ }
290
+
291
+ /** Visits every entity matching the terms, in id order, with its values in term order; `since` is what change terms are measured from. */
292
+ each(terms: readonly Named[] | Select, since: number, fn: (e: number, values: unknown[]) => void): void {
293
+ const sel = isSelect(terms) ? terms : null;
294
+ const live = sel ? this.live.get(sel) : undefined;
295
+ const p = live ? live.plan : this.plan(sel ? sel.terms : (terms as readonly Named[]));
296
+ const changed = p.changes.filter((c) => c.how !== "removed");
297
+ const removed = p.changes.find((c) => c.how === "removed");
298
+ const values = new Array<unknown>(p.comps.length);
299
+ const row = sel?.pred ? Object.fromEntries(p.comps.map((c) => [c.name, undefined])) as Record<string, unknown> : null;
300
+ const visit = (e: number) => {
301
+ if (removed) {
302
+ const r = removed.store.removed.get(e);
303
+ if (!r || r.seq <= since) return;
304
+ if (!this.exists(e)) { if (p.comps.length) return; }
305
+ }
306
+ if (p.comps.length || this.exists(e)) { if (!this.exists(e) || !this.matches(e, p)) return; }
307
+ for (let i = 0; i < p.comps.length; i++) {
308
+ const s = p.comps[i]!.store, at = s.at(e);
309
+ values[i] = s.vals[at];
310
+ for (const c of changed) if (c.store === s && (c.how === "added" ? s.arrived[at]! : s.ticks[at]!) <= since) return;
311
+ }
312
+ if (row) { row.entity = e; for (let i = 0; i < p.comps.length; i++) row[p.comps[i]!.name] = values[i]; if (!sel!.pred!(row)) return; }
313
+ fn(e, values);
314
+ };
315
+ if (removed) { for (const e of [...removed.store.removed.keys()].sort((a, b) => a - b)) visit(e); return; }
316
+ if (live) { for (const e of [...live.ids]) visit(e); return; }
317
+ if (p.comps.length === 0) { for (let e = 1; e <= this.highest; e++) if (this.alive[e] === 1) visit(e); return; }
318
+ let smallest = p.comps[0]!.store;
319
+ for (const c of p.comps) if (c.store.ids.length < smallest.ids.length) smallest = c.store;
320
+ for (const e of [...smallest.ids]) visit(e);
321
+ }
322
+
323
+ /** The names each row's values go under, in term order. */
324
+ columns(terms: readonly Named[] | Select): string[] {
325
+ const ts = isSelect(terms) ? terms.terms : terms;
326
+ const out: string[] = [];
327
+ for (const t of ts) { if (t.kind === "component") out.push(t.name); else if (t.kind === "changed" && t.how !== "removed") out.push(t.component.name); }
328
+ return out;
329
+ }
330
+
331
+ /** The rows matching the terms: the entity, then each value under its component's name. A removed term's value is the last one it had. */
332
+ query(terms: readonly Named[] | Select, since: number): Record<string, unknown>[] {
333
+ const names = this.columns(terms);
334
+ const ts = isSelect(terms) ? terms.terms : terms;
335
+ const rem = ts.find((t): t is Extract<Named, { kind: "changed" }> => t.kind === "changed" && t.how === "removed");
336
+ const out: Record<string, unknown>[] = [];
337
+ this.each(terms, since, (e, values) => {
338
+ const row: Record<string, unknown> = { entity: e };
339
+ for (let i = 0; i < names.length; i++) row[names[i]!] = values[i];
340
+ if (rem) row[rem.component.name] = this.stores.get(rem.component.name)!.removed.get(e)!.value;
341
+ out.push(row);
342
+ });
343
+ return out;
344
+ }
345
+
346
+ /** Keeps the select's matched set live from now on: built once, then kept by every mask change. A select with change terms is not kept. */
347
+ keep(sel: Select): void {
348
+ if (this.live.has(sel) || sel.terms.some((t) => t.kind === "changed")) return;
349
+ const plan = this.plan(sel.terms);
350
+ const ids: number[] = [];
351
+ for (let e = 1; e <= this.highest; e++) if (this.alive[e] === 1 && this.matches(e, plan)) ids.push(e);
352
+ this.live.set(sel, { plan, ids, version: 1, sensitive: sel.pred !== null || sel.order !== null, keyed: sel.order ? this.register(sel.order.component.name) : null, cache: null });
353
+ }
354
+
355
+ /** The version of a kept select: another number whenever its answer may differ. 0 for one not kept. */
356
+ version(sel: Select): number { return this.live.get(sel)?.version ?? 0; }
357
+
358
+ /** The entities a kept select answers, its predicate applied, in its order: the same array until the version moves. */
359
+ kept(sel: Select): readonly number[] {
360
+ const l = this.live.get(sel);
361
+ if (!l) throw new Error("kept: the select is not kept; keep it first");
362
+ if (l.cache && l.cache.version === l.version) return l.cache.ids;
363
+ let ids: number[];
364
+ if (sel.pred) { ids = []; this.each(sel, 0, (e) => { ids.push(e); }); } else ids = [...l.ids];
365
+ if (sel.order && l.keyed) { const s = l.keyed, cmp = sel.order.cmp; ids.sort((a, b) => cmp(s.vals[s.at(a)], s.vals[s.at(b)]) || a - b); }
366
+ l.cache = { version: l.version, ids: Object.freeze(ids) };
367
+ return l.cache.ids;
368
+ }
369
+
370
+ private relive(e: number): void {
371
+ for (const l of this.live.values()) {
372
+ const inSet = this.matches(e, l.plan) && this.alive[e] === 1;
373
+ let lo = 0, hi = l.ids.length - 1, at = -1;
374
+ while (lo <= hi) { const mid = (lo + hi) >>> 1; const v = l.ids[mid]!; if (v === e) { at = mid; break; } if (v < e) lo = mid + 1; else hi = mid - 1; }
375
+ if (inSet && at < 0) { l.ids.splice(lo, 0, e); l.version++; }
376
+ else if (!inSet && at >= 0) { l.ids.splice(at, 1); l.version++; }
377
+ }
378
+ }
379
+
380
+ // ---- change ------------------------------------------------------------------
381
+
382
+ /**
383
+ * What changed after `since`, by entity: every value written, every
384
+ * component taken away (null), and the entities gone. Costs a walk of
385
+ * every store, not of every entity times every store.
386
+ */
387
+ changes(since: number): { patches: Map<number, Record<string, unknown>>; gone: number[]; added: Map<number, string[]> } {
388
+ const patches = new Map<number, Record<string, unknown>>();
389
+ const added = new Map<number, string[]>();
390
+ const of = (e: number) => { let p = patches.get(e); if (!p) { p = {}; patches.set(e, p); } return p; };
391
+ for (const s of this.byId) {
392
+ for (let i = 0; i < s.ids.length; i++) if (s.ticks[i]! > since) { const e = s.ids[i]!; of(e)[s.name] = s.vals[i]; if (s.arrived[i]! > since) { const a = added.get(e); if (a) a.push(s.name); else added.set(e, [s.name]); } }
393
+ for (const [e, r] of s.removed) if (r.seq > since && this.alive[e] === 1) of(e)[s.name] = null;
394
+ }
395
+ const gone: number[] = [];
396
+ for (const [e, at] of this.gone) if (at > since && this.alive[e] !== 1) { gone.push(e); patches.delete(e); added.delete(e); }
397
+ return { patches, gone: gone.sort((a, b) => a - b), added };
398
+ }
399
+
400
+ /** A delta's patch applied to the entity: each value put, each null taken away; a replica's. */
401
+ patch(e: number, parts: Record<string, unknown>): void {
402
+ if (!this.exists(e)) this.spawn(e);
403
+ for (const [name, v] of Object.entries(parts)) { if (v === null) this.remove(e, name); else this.put(e, name, v); }
404
+ }
405
+
406
+ /** Every value written after `since`, by walking the stores: the entity, the component's name, the value. */
407
+ changedParts(since: number): [number, string, unknown][] {
408
+ const out: [number, string, unknown][] = [];
409
+ for (const s of this.byId) for (let i = 0; i < s.ids.length; i++) if (s.ticks[i]! > since) out.push([s.ids[i]!, s.name, s.vals[i]]);
410
+ return out;
411
+ }
412
+
413
+ /** Forgets removals and despawns older than `seq`: nothing measures from before it any more. */
414
+ prune(seq: number): void {
415
+ for (const [e, at] of this.gone) if (at <= seq) this.gone.delete(e);
416
+ for (const s of this.byId) for (const [e, r] of s.removed) if (r.seq <= seq) s.removed.delete(e);
417
+ }
418
+ }
419
+
420
+ export const isSelect = (x: unknown): x is Select => typeof x === "object" && x !== null && (x as { kind?: unknown }).kind === "select";
@@ -0,0 +1,97 @@
1
+ // State in, state out: a baba or a system driven by hand, on a virtual clock.
2
+ // `fire` runs the action now, as its system; `tick` advances the clock to the
3
+ // time given, running every round due on the way (a retry, a scheduled
4
+ // effect, the baba's tick) and one round at that time; jobs wait until
5
+ // `done` or `failed` answers them, unless a port is given. The same driver
6
+ // serves the headless runner and a live simulation: both only advance.
7
+ import { Runtime, type Port } from "./runtime.ts";
8
+ import type { Baba, System } from "./baba.ts";
9
+ import type { State, Effect, Entity, Args, Result, Job, Snapshot } from "./state.ts";
10
+ import { virtualScheduler } from "./scheduler.ts";
11
+ import type { ActOf as ContractActs, ReadOf as ContractReads } from "./baba.ts";
12
+ import type { Adapter } from "../spec/adapters.ts";
13
+
14
+ type IsAny<T> = 0 extends 1 & T ? true : false;
15
+ /** An untyped testbed (a scenario in a file of its own) fires and reads anything. */
16
+ type ActOf<V> = IsAny<V> extends true ? Record<string, { args: any; result: any }> : ContractActs<V>;
17
+ type ReadOf<V> = IsAny<V> extends true ? Record<string, { args: any; result: any }> : ContractReads<V>;
18
+ type ArgsOf<D> = D extends { args: infer A } ? ({} extends A ? [args?: A] : [args: A]) : [];
19
+ type ResultOf<D> = D extends { result: infer R } ? R : never;
20
+
21
+ export interface Testbed<V> {
22
+ readonly w: State<V>;
23
+ /** Runs the action now, as its system, over the state as it is; what it returned. */
24
+ fire<K extends keyof ActOf<V> & string>(name: K, ...args: ArgsOf<ActOf<V>[K]>): ResultOf<ActOf<V>[K]>;
25
+ /** The clock to `now` (the last time plus one by default), every round due on the way, then one round at `now`. */
26
+ tick(at?: { now: number }): void;
27
+ /** The clock to `to`: every round due on the way, and nothing more. What a live driver calls as real time passes. */
28
+ advance(to: number): void;
29
+ /** The time, as the clock has it. */
30
+ readonly now: number;
31
+ /** The earliest moment a round is due on its own, or null. */
32
+ readonly due: number | null;
33
+ /** The jobs of `effect` waiting for an answer. */
34
+ pending<E extends Effect>(effect: E): { entity: Entity | null; args: Args<E> }[];
35
+ /** Answers the jobs of `effect` whose args match `where` (every one by default), at the next tick. */
36
+ done<E extends Effect>(effect: E, result: Result<E>, where?: Partial<Args<E>> | { entity: Entity | null }): void;
37
+ failed<E extends Effect>(effect: E, error: string, where?: Partial<Args<E>> | { entity: Entity | null }): void;
38
+ /** A source's listing, as its `list` would answer: lands at the next tick for the jobs matching `where`. */
39
+ list<E extends Effect>(effect: E, items: unknown[], where?: Partial<Args<E>> | { entity: Entity | null }): void;
40
+ /** A source's events, as its `watch` would yield them. */
41
+ watch<E extends Effect>(effect: E, events: unknown[], where?: Partial<Args<E>> | { entity: Entity | null }): void;
42
+ read<K extends keyof ReadOf<V> & string>(name: K, ...args: ArgsOf<ReadOf<V>[K]>): ResultOf<ReadOf<V>[K]>;
43
+ readonly round: number;
44
+ readonly jobs: Job[];
45
+ /** Each adapter's state, by effect: the outside as the state's models hold it. */
46
+ readonly outside: ReadonlyMap<Effect, Map<string, unknown>>;
47
+ }
48
+
49
+ export function testbed<V extends Baba | System>(v: V, at: { now?: number; seed?: number; snapshot?: Snapshot; port?: Port; adapters?: (effect: Effect) => Adapter | undefined; log?: (line: string) => void } = {}): Testbed<V> {
50
+ const clock = virtualScheduler(at.now ?? 0);
51
+ const rt = new Runtime(v, { auto: false, scheduler: clock, seed: at.seed ?? 1, ...(at.snapshot ? { snapshot: at.snapshot } : {}), ...(at.port ? { port: at.port } : {}), ...(at.adapters ? { adapters: at.adapters } : {}), ...(at.log ? { log: at.log } : {}) });
52
+ rt.w.checking = true;
53
+ rt.w.begin(clock.now(), 0);
54
+ const matching = (effect: Effect, where?: Record<string, unknown>) => [...rt.w.jobs.values()].filter((j) => j.effect === effect).filter((j) => {
55
+ if (!where) return true;
56
+ if ("entity" in where && Object.keys(where).length === 1) return j.entity === where.entity;
57
+ return Object.entries(where).every(([k, val]) => JSON.stringify((j.args as Record<string, unknown>)[k]) === JSON.stringify(val));
58
+ });
59
+ return {
60
+ w: rt.w as State<V>,
61
+ fire: ((name: string, args?: unknown) => {
62
+ const d = rt.baba.actions[name];
63
+ if (!d) throw new Error(`no action ${name}`);
64
+ return rt.act(name, d.args.check(args ?? {}));
65
+ }) as never,
66
+ // The clock to `to`, the round, then whatever the round's jobs answered at once: an adapter's reply with no delay lands before the hand looks.
67
+ tick(t) { const to = t?.now ?? clock.now() + 1; clock.advance(to); rt.step(to); clock.flush(); },
68
+ advance(to) { clock.advance(to); },
69
+ get now() { return clock.now(); },
70
+ get due() { return rt.due(); },
71
+ pending: (effect) => rt.pending(effect).map((j) => ({ entity: j.entity, args: j.args as never })),
72
+ done(effect, result, where) {
73
+ const jobs = matching(effect, where as Record<string, unknown> | undefined);
74
+ if (jobs.length === 0) throw new Error(`no job of ${effect.name} is waiting${where ? " for those args" : ""}`);
75
+ for (const j of jobs) rt.answer(effect, j.entity, j.instance, true, result);
76
+ },
77
+ failed(effect, error, where) {
78
+ const jobs = matching(effect, where as Record<string, unknown> | undefined);
79
+ if (jobs.length === 0) throw new Error(`no job of ${effect.name} is waiting${where ? " for those args" : ""}`);
80
+ for (const j of jobs) rt.answer(effect, j.entity, j.instance, false, error);
81
+ },
82
+ list(effect, items, where) {
83
+ const jobs = matching(effect, where as Record<string, unknown> | undefined);
84
+ if (jobs.length === 0) throw new Error(`no job of ${effect.name} is running${where ? " for those args" : ""}`);
85
+ for (const j of jobs) rt.deliver(effect, j.entity, j.instance, { listed: items });
86
+ },
87
+ watch(effect, events, where) {
88
+ const jobs = matching(effect, where as Record<string, unknown> | undefined);
89
+ if (jobs.length === 0) throw new Error(`no job of ${effect.name} is running${where ? " for those args" : ""}`);
90
+ for (const j of jobs) rt.deliver(effect, j.entity, j.instance, { events });
91
+ },
92
+ read: ((name: string, args?: unknown) => rt.read(name, args ?? {})) as never,
93
+ get round() { return rt.round; },
94
+ get jobs() { return [...rt.w.jobs.values()]; },
95
+ get outside() { return rt.outside; },
96
+ };
97
+ }