automatick 0.0.1 → 0.0.3

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 (44) hide show
  1. package/README.md +2 -0
  2. package/dist/{chunk-YNLOTPDY.js → chunk-A375T3UD.js} +39 -3
  3. package/dist/chunk-IKR53C2U.js +12 -0
  4. package/dist/chunk-LMHH7YPE.js +89 -0
  5. package/dist/chunk-VPS3ZXWI.js +132 -0
  6. package/dist/engine.cjs +42 -3
  7. package/dist/engine.d.cts +42 -18
  8. package/dist/engine.d.ts +42 -18
  9. package/dist/engine.js +2 -1
  10. package/dist/react/EngineContext.d.cts +4 -3
  11. package/dist/react/EngineContext.d.ts +4 -3
  12. package/dist/react/Simulation.cjs +341 -102
  13. package/dist/react/Simulation.d.cts +36 -16
  14. package/dist/react/Simulation.d.ts +36 -16
  15. package/dist/react/Simulation.js +93 -100
  16. package/dist/react/SimulationContext.d.cts +1 -2
  17. package/dist/react/SimulationContext.d.ts +1 -2
  18. package/dist/react/hooks.d.cts +1 -1
  19. package/dist/react/hooks.d.ts +1 -1
  20. package/dist/react/useSimulationCanvas.cjs +2 -10
  21. package/dist/react/useSimulationCanvas.d.cts +4 -7
  22. package/dist/react/useSimulationCanvas.d.ts +4 -7
  23. package/dist/react/useSimulationCanvas.js +2 -10
  24. package/dist/sim.cjs +7 -2
  25. package/dist/sim.d.cts +34 -14
  26. package/dist/sim.d.ts +34 -14
  27. package/dist/sim.js +6 -5
  28. package/dist/standalone/engine.js +242 -0
  29. package/dist/standalone/sim.js +11 -0
  30. package/dist/state.cjs +18 -0
  31. package/dist/state.d.cts +16 -0
  32. package/dist/state.d.ts +16 -0
  33. package/dist/state.js +0 -0
  34. package/dist/worker/createSimWorker.cjs +12 -3
  35. package/dist/worker/createSimWorker.d.cts +1 -2
  36. package/dist/worker/createSimWorker.d.ts +1 -2
  37. package/dist/worker/createSimWorker.js +3 -119
  38. package/dist/worker/protocol.d.cts +5 -5
  39. package/dist/worker/protocol.d.ts +5 -5
  40. package/dist/worker/workerRunner.cjs +21 -6
  41. package/dist/worker/workerRunner.d.cts +11 -3
  42. package/dist/worker/workerRunner.d.ts +11 -3
  43. package/dist/worker/workerRunner.js +3 -70
  44. package/package.json +2 -2
@@ -0,0 +1,242 @@
1
+ // src/sim.ts
2
+ function isInitFn(init) {
3
+ return typeof init === "function";
4
+ }
5
+
6
+ // src/engine.ts
7
+ var PERF_BUFFER_SIZE = 120;
8
+ var SimulationEngine = class {
9
+ data;
10
+ params;
11
+ tick = 0;
12
+ status = "idle";
13
+ lastUpdateMs = null;
14
+ lastStepMs = 0;
15
+ initFn;
16
+ stepFn;
17
+ shouldStopFn;
18
+ maxTime;
19
+ delayMs;
20
+ ticksPerFrame;
21
+ listeners = /* @__PURE__ */ new Set();
22
+ historyListeners = /* @__PURE__ */ new Set();
23
+ perfBuffer = [];
24
+ rafId = null;
25
+ rafCancel = null;
26
+ constructor(config) {
27
+ const init = config.init;
28
+ if (isInitFn(init)) {
29
+ this.initFn = init;
30
+ } else {
31
+ const seed = structuredClone(init);
32
+ this.initFn = () => structuredClone(seed);
33
+ }
34
+ this.stepFn = config.step;
35
+ this.shouldStopFn = config.shouldStop;
36
+ this.maxTime = config.maxTime;
37
+ this.delayMs = config.delayMs ?? 0;
38
+ this.ticksPerFrame = config.ticksPerFrame ?? 1;
39
+ this.params = config.initialParams ? { ...config.initialParams } : {};
40
+ this.data = this.initFn(this.params);
41
+ if (config.render) {
42
+ this.listeners.add(config.render);
43
+ config.render(this.getSnapshot());
44
+ }
45
+ const autoFrame = config.autoFrame ?? true;
46
+ if (autoFrame) {
47
+ const raf = globalThis.requestAnimationFrame;
48
+ const caf = globalThis.cancelAnimationFrame;
49
+ if (typeof raf === "function" && typeof caf === "function") {
50
+ const loop = (now) => {
51
+ this.handleAnimationFrame(now);
52
+ this.rafId = raf(loop);
53
+ };
54
+ this.rafCancel = caf;
55
+ this.rafId = raf(loop);
56
+ }
57
+ }
58
+ }
59
+ getSnapshot() {
60
+ return {
61
+ data: this.data,
62
+ params: this.params,
63
+ tick: this.tick,
64
+ status: this.status,
65
+ stepDurationMs: this.lastStepMs
66
+ };
67
+ }
68
+ getStatus() {
69
+ return this.status;
70
+ }
71
+ getPerformance() {
72
+ return this.perfBuffer;
73
+ }
74
+ setDelayMs(ms) {
75
+ this.delayMs = ms;
76
+ }
77
+ setTicksPerFrame(n) {
78
+ this.ticksPerFrame = n;
79
+ }
80
+ recordDrawTime(tick, ms) {
81
+ for (let i = this.perfBuffer.length - 1; i >= 0; i--) {
82
+ if (this.perfBuffer[i].tick === tick) {
83
+ this.perfBuffer[i].drawMs = ms;
84
+ return;
85
+ }
86
+ }
87
+ }
88
+ subscribe(listener) {
89
+ this.listeners.add(listener);
90
+ return () => {
91
+ this.listeners.delete(listener);
92
+ };
93
+ }
94
+ subscribeHistory(listener) {
95
+ this.historyListeners.add(listener);
96
+ return () => {
97
+ this.historyListeners.delete(listener);
98
+ };
99
+ }
100
+ play() {
101
+ if (this.status === "stopped") return;
102
+ if (this.status === "playing") return;
103
+ this.status = "playing";
104
+ this.lastUpdateMs = null;
105
+ this.emit();
106
+ }
107
+ pause() {
108
+ if (this.status !== "playing") return;
109
+ this.status = "paused";
110
+ this.emit();
111
+ }
112
+ stop() {
113
+ if (this.status === "idle" || this.status === "stopped") return;
114
+ this.status = "stopped";
115
+ this.emit();
116
+ }
117
+ seek(targetTick) {
118
+ if (this.status === "stopped") return;
119
+ if (targetTick <= this.tick) {
120
+ if (this.status !== "paused") {
121
+ this.status = "paused";
122
+ this.emit();
123
+ }
124
+ return;
125
+ }
126
+ this.status = "paused";
127
+ this.lastUpdateMs = null;
128
+ this.advanceTicks(targetTick - this.tick);
129
+ if (this.status === "paused") {
130
+ this.emit();
131
+ }
132
+ }
133
+ advance(count = 1) {
134
+ if (this.status === "stopped") return;
135
+ if (this.status === "playing" || this.status === "idle") {
136
+ this.status = "paused";
137
+ }
138
+ this.lastUpdateMs = null;
139
+ this.advanceTicks(count);
140
+ if (this.status === "paused") {
141
+ this.emit();
142
+ }
143
+ }
144
+ setParams(patch) {
145
+ this.params = { ...this.params, ...patch };
146
+ this.emit();
147
+ }
148
+ resetWith(patch) {
149
+ if (patch) {
150
+ this.params = { ...this.params, ...patch };
151
+ }
152
+ this.data = this.initFn(this.params);
153
+ this.tick = 0;
154
+ this.status = "idle";
155
+ this.lastUpdateMs = null;
156
+ this.lastStepMs = 0;
157
+ this.perfBuffer.length = 0;
158
+ this.emit();
159
+ }
160
+ handleAnimationFrame(nowMs) {
161
+ if (this.status !== "playing") return;
162
+ if (this.lastUpdateMs === null) {
163
+ this.lastUpdateMs = nowMs;
164
+ return;
165
+ }
166
+ if (this.delayMs > 0 && nowMs - this.lastUpdateMs < this.delayMs) return;
167
+ this.lastUpdateMs = nowMs;
168
+ this.advanceTicks(this.ticksPerFrame);
169
+ if (this.status === "playing") {
170
+ this.emit();
171
+ }
172
+ }
173
+ destroy() {
174
+ if (this.rafId !== null && this.rafCancel) {
175
+ this.rafCancel(this.rafId);
176
+ }
177
+ this.rafId = null;
178
+ this.rafCancel = null;
179
+ this.listeners.clear();
180
+ this.historyListeners.clear();
181
+ }
182
+ /**
183
+ * Run step up to `count` ticks. Returns true if all ticks completed
184
+ * without termination, false if stopped early.
185
+ */
186
+ advanceTicks(count) {
187
+ for (let i = 0; i < count; i++) {
188
+ if (this.maxTime !== void 0 && this.tick >= this.maxTime) {
189
+ this.status = "stopped";
190
+ this.emit();
191
+ return false;
192
+ }
193
+ this.tick += 1;
194
+ const t0 = performance.now();
195
+ this.data = this.stepFn({
196
+ data: this.data,
197
+ params: this.params,
198
+ tick: this.tick,
199
+ status: this.status,
200
+ stepDurationMs: this.lastStepMs
201
+ });
202
+ const t1 = performance.now();
203
+ this.lastStepMs = t1 - t0;
204
+ if (this.perfBuffer.length >= PERF_BUFFER_SIZE) {
205
+ this.perfBuffer.shift();
206
+ }
207
+ this.perfBuffer.push({ tick: this.tick, stepMs: this.lastStepMs });
208
+ this.emitHistory();
209
+ if (this.shouldStopFn?.(this.data, this.params)) {
210
+ this.status = "stopped";
211
+ this.emit();
212
+ return false;
213
+ }
214
+ if (this.maxTime !== void 0 && this.tick >= this.maxTime) {
215
+ this.status = "stopped";
216
+ this.emit();
217
+ return false;
218
+ }
219
+ }
220
+ return true;
221
+ }
222
+ emit() {
223
+ const snap = this.getSnapshot();
224
+ for (const l of this.listeners) {
225
+ l(snap);
226
+ }
227
+ }
228
+ emitHistory() {
229
+ if (this.historyListeners.size === 0) return;
230
+ const entry = { tick: this.tick, data: this.data };
231
+ for (const l of this.historyListeners) {
232
+ l(entry);
233
+ }
234
+ }
235
+ };
236
+ function createEngine(config) {
237
+ return new SimulationEngine(config);
238
+ }
239
+ export {
240
+ SimulationEngine,
241
+ createEngine
242
+ };
@@ -0,0 +1,11 @@
1
+ // src/sim.ts
2
+ function isInitFn(init) {
3
+ return typeof init === "function";
4
+ }
5
+ function defineSim(sim) {
6
+ return sim;
7
+ }
8
+ export {
9
+ defineSim,
10
+ isInitFn
11
+ };
package/dist/state.cjs ADDED
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __copyProps = (to, from, except, desc) => {
7
+ if (from && typeof from === "object" || typeof from === "function") {
8
+ for (let key of __getOwnPropNames(from))
9
+ if (!__hasOwnProp.call(to, key) && key !== except)
10
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
11
+ }
12
+ return to;
13
+ };
14
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
15
+
16
+ // src/state.ts
17
+ var state_exports = {};
18
+ module.exports = __toCommonJS(state_exports);
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The unified engine state shape — passed to `step`, returned by `getSnapshot`,
3
+ * and delivered to the `render` callback. All three are operations on the
4
+ * engine's inner state, so they share one type.
5
+ */
6
+ type State<Data, Params> = {
7
+ data: Data;
8
+ params: Params;
9
+ tick: number;
10
+ status: SimulationStatus;
11
+ /** Duration of the previous step in ms. `0` before the first step has run. */
12
+ stepDurationMs: number;
13
+ };
14
+ type SimulationStatus = 'idle' | 'playing' | 'paused' | 'stopped';
15
+
16
+ export type { SimulationStatus, State };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The unified engine state shape — passed to `step`, returned by `getSnapshot`,
3
+ * and delivered to the `render` callback. All three are operations on the
4
+ * engine's inner state, so they share one type.
5
+ */
6
+ type State<Data, Params> = {
7
+ data: Data;
8
+ params: Params;
9
+ tick: number;
10
+ status: SimulationStatus;
11
+ /** Duration of the previous step in ms. `0` before the first step has run. */
12
+ stepDurationMs: number;
13
+ };
14
+ type SimulationStatus = 'idle' | 'playing' | 'paused' | 'stopped';
15
+
16
+ export type { SimulationStatus, State };
package/dist/state.js ADDED
File without changes
@@ -72,14 +72,18 @@ self.onmessage = async (event) => {
72
72
  import(msg.engineUrl),
73
73
  ]);
74
74
  const sim = simMod.default;
75
+ // Merge sim.defaultParams under the patch sent from main \u2014 the main
76
+ // thread sees the sim module via a URL, so it can't apply defaults.
77
+ const initialParams = { ...(sim.defaultParams || {}), ...(msg.params || {}) };
75
78
  engine = engineMod.createEngine({
76
79
  init: sim.init,
77
80
  step: sim.step,
78
81
  shouldStop: sim.shouldStop,
79
- initialParams: msg.params,
82
+ initialParams,
80
83
  maxTime: msg.config.maxTime,
84
+ // Worker host owns its own setTimeout-driven loop; rAF wouldn't exist here anyway.
85
+ autoFrame: false,
81
86
  });
82
- postMessage({ kind: 'ready' });
83
87
  emitSnapshot();
84
88
  break;
85
89
  }
@@ -111,6 +115,11 @@ self.onmessage = async (event) => {
111
115
  if (!engine) return;
112
116
  stopLoop(); engine.resetWith(msg.patch); emitSnapshot();
113
117
  break;
118
+ case 'setConfig':
119
+ if (msg.patch.delayMs !== undefined) delayMs = msg.patch.delayMs;
120
+ if (msg.patch.ticksPerFrame !== undefined) ticksPerFrame = msg.patch.ticksPerFrame;
121
+ if (msg.patch.snapshotIntervalMs !== undefined) snapshotIntervalMs = msg.patch.snapshotIntervalMs;
122
+ break;
114
123
  case 'destroy':
115
124
  stopLoop();
116
125
  if (engine) { engine.destroy(); engine = null; }
@@ -127,7 +136,7 @@ function createSimWorker(options) {
127
136
  const blobUrl = URL.createObjectURL(blob);
128
137
  const worker = new Worker(blobUrl, { type: "module" });
129
138
  worker.addEventListener("message", function cleanup(event) {
130
- if (event.data?.kind === "ready" || event.data?.kind === "error") {
139
+ if (event.data?.kind === "snapshot" || event.data?.kind === "error") {
131
140
  URL.revokeObjectURL(blobUrl);
132
141
  worker.removeEventListener("message", cleanup);
133
142
  }
@@ -1,6 +1,5 @@
1
1
  import { WorkerConfig } from './protocol.cjs';
2
- import '../engine.cjs';
3
- import '../sim.cjs';
2
+ import '../state.cjs';
4
3
 
5
4
  /**
6
5
  * Creates a Web Worker that loads a sim module and runs the engine.
@@ -1,6 +1,5 @@
1
1
  import { WorkerConfig } from './protocol.js';
2
- import '../engine.js';
3
- import '../sim.js';
2
+ import '../state.js';
4
3
 
5
4
  /**
6
5
  * Creates a Web Worker that loads a sim module and runs the engine.
@@ -1,122 +1,6 @@
1
- // src/worker/createSimWorker.ts
2
- var WORKER_SCRIPT = `
3
- let engine = null;
4
- let loopTimer = null;
5
- let snapshotIntervalMs = 16;
6
- let lastSnapshotMs = 0;
7
- let ticksPerFrame = 1;
8
- let delayMs = 0;
9
-
10
- function emitSnapshot() {
11
- if (!engine) return;
12
- postMessage({ kind: 'snapshot', snapshot: engine.getSnapshot() });
13
- lastSnapshotMs = performance.now();
14
- }
15
-
16
- function tickLoop() {
17
- if (!engine || engine.getStatus() !== 'playing') return;
18
-
19
- for (let i = 0; i < ticksPerFrame; i++) {
20
- engine.advance(1);
21
- const s = engine.getStatus();
22
- if (s === 'stopped') { emitSnapshot(); return; }
23
- if (s !== 'paused') break;
24
- }
25
-
26
- // advance() transitions to paused; resume playing for the next batch
27
- if (engine.getStatus() === 'paused') engine.play();
28
-
29
- if (performance.now() - lastSnapshotMs >= snapshotIntervalMs) emitSnapshot();
30
- loopTimer = setTimeout(tickLoop, delayMs);
31
- }
32
-
33
- function stopLoop() {
34
- if (loopTimer !== null) { clearTimeout(loopTimer); loopTimer = null; }
35
- }
36
-
37
- self.onmessage = async (event) => {
38
- const msg = event.data;
39
- try {
40
- switch (msg.kind) {
41
- case 'init': {
42
- delayMs = msg.config.delayMs || 0;
43
- ticksPerFrame = msg.config.ticksPerFrame || 1;
44
- snapshotIntervalMs = msg.config.snapshotIntervalMs || 16;
45
-
46
- const [simMod, engineMod] = await Promise.all([
47
- import(msg.moduleUrl),
48
- import(msg.engineUrl),
49
- ]);
50
- const sim = simMod.default;
51
- engine = engineMod.createEngine({
52
- init: sim.init,
53
- step: sim.step,
54
- shouldStop: sim.shouldStop,
55
- initialParams: msg.params,
56
- maxTime: msg.config.maxTime,
57
- });
58
- postMessage({ kind: 'ready' });
59
- emitSnapshot();
60
- break;
61
- }
62
- case 'play':
63
- if (!engine) return;
64
- engine.play(); emitSnapshot(); stopLoop(); loopTimer = setTimeout(tickLoop, 0);
65
- break;
66
- case 'pause':
67
- if (!engine) return;
68
- stopLoop(); engine.pause(); emitSnapshot();
69
- break;
70
- case 'stop':
71
- if (!engine) return;
72
- stopLoop(); engine.stop(); emitSnapshot();
73
- break;
74
- case 'seek':
75
- if (!engine) return;
76
- stopLoop(); engine.seek(msg.tick); emitSnapshot();
77
- break;
78
- case 'advance':
79
- if (!engine) return;
80
- engine.advance(msg.count); emitSnapshot();
81
- break;
82
- case 'setParams':
83
- if (!engine) return;
84
- engine.setParams(msg.patch); emitSnapshot();
85
- break;
86
- case 'resetWith':
87
- if (!engine) return;
88
- stopLoop(); engine.resetWith(msg.patch); emitSnapshot();
89
- break;
90
- case 'destroy':
91
- stopLoop();
92
- if (engine) { engine.destroy(); engine = null; }
93
- self.close();
94
- break;
95
- }
96
- } catch (err) {
97
- postMessage({ kind: 'error', error: { message: err.message, stack: err.stack } });
98
- }
99
- };
100
- `;
101
- function createSimWorker(options) {
102
- const blob = new Blob([WORKER_SCRIPT], { type: "text/javascript" });
103
- const blobUrl = URL.createObjectURL(blob);
104
- const worker = new Worker(blobUrl, { type: "module" });
105
- worker.addEventListener("message", function cleanup(event) {
106
- if (event.data?.kind === "ready" || event.data?.kind === "error") {
107
- URL.revokeObjectURL(blobUrl);
108
- worker.removeEventListener("message", cleanup);
109
- }
110
- });
111
- worker.postMessage({
112
- kind: "init",
113
- moduleUrl: options.moduleUrl,
114
- engineUrl: options.engineUrl,
115
- params: options.initialParams,
116
- config: options.config
117
- });
118
- return worker;
119
- }
1
+ import {
2
+ createSimWorker
3
+ } from "../chunk-VPS3ZXWI.js";
120
4
  export {
121
5
  createSimWorker
122
6
  };
@@ -1,5 +1,4 @@
1
- import { EngineSnapshot } from '../engine.cjs';
2
- import '../sim.cjs';
1
+ import { State } from '../state.cjs';
3
2
 
4
3
  /**
5
4
  * Wire protocol types for main ↔ worker communication.
@@ -32,21 +31,22 @@ type MainToWorkerMessage<Params> = {
32
31
  } | {
33
32
  kind: 'resetWith';
34
33
  patch?: Partial<Params>;
34
+ } | {
35
+ kind: 'setConfig';
36
+ patch: Partial<WorkerConfig>;
35
37
  } | {
36
38
  kind: 'destroy';
37
39
  };
38
40
  /** Messages sent from the worker to the main thread. */
39
41
  type WorkerToMainMessage<Data, Params> = {
40
42
  kind: 'snapshot';
41
- snapshot: EngineSnapshot<Data, Params>;
43
+ snapshot: State<Data, Params>;
42
44
  } | {
43
45
  kind: 'error';
44
46
  error: {
45
47
  message: string;
46
48
  stack?: string;
47
49
  };
48
- } | {
49
- kind: 'ready';
50
50
  };
51
51
  /** Worker-specific configuration passed at init time. */
52
52
  type WorkerConfig = {
@@ -1,5 +1,4 @@
1
- import { EngineSnapshot } from '../engine.js';
2
- import '../sim.js';
1
+ import { State } from '../state.js';
3
2
 
4
3
  /**
5
4
  * Wire protocol types for main ↔ worker communication.
@@ -32,21 +31,22 @@ type MainToWorkerMessage<Params> = {
32
31
  } | {
33
32
  kind: 'resetWith';
34
33
  patch?: Partial<Params>;
34
+ } | {
35
+ kind: 'setConfig';
36
+ patch: Partial<WorkerConfig>;
35
37
  } | {
36
38
  kind: 'destroy';
37
39
  };
38
40
  /** Messages sent from the worker to the main thread. */
39
41
  type WorkerToMainMessage<Data, Params> = {
40
42
  kind: 'snapshot';
41
- snapshot: EngineSnapshot<Data, Params>;
43
+ snapshot: State<Data, Params>;
42
44
  } | {
43
45
  kind: 'error';
44
46
  error: {
45
47
  message: string;
46
48
  stack?: string;
47
49
  };
48
- } | {
49
- kind: 'ready';
50
50
  };
51
51
  /** Worker-specific configuration passed at init time. */
52
52
  type WorkerConfig = {
@@ -33,6 +33,7 @@ function serializeMainMessage(msg) {
33
33
  }
34
34
 
35
35
  // src/worker/workerRunner.ts
36
+ var PERF_BUFFER_SIZE = 120;
36
37
  function createWorkerRunner(worker, config) {
37
38
  const listeners = /* @__PURE__ */ new Set();
38
39
  let currentSnapshot = {
@@ -42,7 +43,7 @@ function createWorkerRunner(worker, config) {
42
43
  status: "idle",
43
44
  stepDurationMs: 0
44
45
  };
45
- let errorMessage = null;
46
+ const perfBuffer = [];
46
47
  function send(msg) {
47
48
  worker.postMessage(serializeMainMessage(msg));
48
49
  }
@@ -51,24 +52,28 @@ function createWorkerRunner(worker, config) {
51
52
  l(currentSnapshot);
52
53
  }
53
54
  }
55
+ function pushPerf(snapshot) {
56
+ if (snapshot.tick <= 0) return;
57
+ const last = perfBuffer[perfBuffer.length - 1];
58
+ if (last && last.tick === snapshot.tick) return;
59
+ if (perfBuffer.length >= PERF_BUFFER_SIZE) perfBuffer.shift();
60
+ perfBuffer.push({ tick: snapshot.tick, stepMs: snapshot.stepDurationMs });
61
+ }
54
62
  worker.onmessage = (event) => {
55
63
  const msg = deserializeWorkerMessage(event.data);
56
64
  switch (msg.kind) {
57
65
  case "snapshot":
58
66
  currentSnapshot = msg.snapshot;
67
+ pushPerf(msg.snapshot);
59
68
  emit();
60
69
  break;
61
70
  case "error":
62
- errorMessage = msg.error.message;
63
71
  currentSnapshot = { ...currentSnapshot, status: "stopped" };
64
72
  emit();
65
73
  break;
66
- case "ready":
67
- break;
68
74
  }
69
75
  };
70
- worker.onerror = (event) => {
71
- errorMessage = event.message ?? "Unknown worker error";
76
+ worker.onerror = () => {
72
77
  currentSnapshot = { ...currentSnapshot, status: "stopped" };
73
78
  emit();
74
79
  };
@@ -87,6 +92,16 @@ function createWorkerRunner(worker, config) {
87
92
  advance: (count = 1) => send({ kind: "advance", count }),
88
93
  setParams: (patch) => send({ kind: "setParams", patch }),
89
94
  resetWith: (patch) => send({ kind: "resetWith", patch }),
95
+ setConfig: (patch) => send({ kind: "setConfig", patch }),
96
+ recordDrawTime(tick, ms) {
97
+ for (let i = perfBuffer.length - 1; i >= 0; i--) {
98
+ if (perfBuffer[i].tick === tick) {
99
+ perfBuffer[i].drawMs = ms;
100
+ return;
101
+ }
102
+ }
103
+ },
104
+ getPerformance: () => perfBuffer,
90
105
  destroy() {
91
106
  listeners.clear();
92
107
  send({ kind: "destroy" });
@@ -1,4 +1,5 @@
1
- import { EngineSnapshot } from '../engine.cjs';
1
+ import { TickPerformance } from '../engine.cjs';
2
+ import { State } from '../state.cjs';
2
3
  import { WorkerConfig } from './protocol.cjs';
3
4
  import '../sim.cjs';
4
5
 
@@ -14,8 +15,8 @@ type WorkerRunnerConfig<Params> = {
14
15
  config: WorkerConfig;
15
16
  };
16
17
  type WorkerRunner<Data, Params> = {
17
- getSnapshot: () => EngineSnapshot<Data, Params>;
18
- subscribe: (listener: (snapshot: EngineSnapshot<Data, Params>) => void) => () => void;
18
+ getSnapshot: () => State<Data, Params>;
19
+ subscribe: (listener: (snapshot: State<Data, Params>) => void) => () => void;
19
20
  play: () => void;
20
21
  pause: () => void;
21
22
  stop: () => void;
@@ -23,6 +24,13 @@ type WorkerRunner<Data, Params> = {
23
24
  advance: (count?: number) => void;
24
25
  setParams: (patch: Partial<Params>) => void;
25
26
  resetWith: (patch?: Partial<Params>) => void;
27
+ setConfig: (patch: Partial<WorkerConfig>) => void;
28
+ /**
29
+ * Record a draw time for a tick. Draws happen on the main thread, so
30
+ * timings are tracked here — they never cross the postMessage boundary.
31
+ */
32
+ recordDrawTime: (tick: number, ms: number) => void;
33
+ getPerformance: () => readonly TickPerformance[];
26
34
  destroy: () => void;
27
35
  };
28
36
  declare function createWorkerRunner<Data, Params>(worker: Worker, config: WorkerRunnerConfig<Params>): WorkerRunner<Data, Params>;
@@ -1,4 +1,5 @@
1
- import { EngineSnapshot } from '../engine.js';
1
+ import { TickPerformance } from '../engine.js';
2
+ import { State } from '../state.js';
2
3
  import { WorkerConfig } from './protocol.js';
3
4
  import '../sim.js';
4
5
 
@@ -14,8 +15,8 @@ type WorkerRunnerConfig<Params> = {
14
15
  config: WorkerConfig;
15
16
  };
16
17
  type WorkerRunner<Data, Params> = {
17
- getSnapshot: () => EngineSnapshot<Data, Params>;
18
- subscribe: (listener: (snapshot: EngineSnapshot<Data, Params>) => void) => () => void;
18
+ getSnapshot: () => State<Data, Params>;
19
+ subscribe: (listener: (snapshot: State<Data, Params>) => void) => () => void;
19
20
  play: () => void;
20
21
  pause: () => void;
21
22
  stop: () => void;
@@ -23,6 +24,13 @@ type WorkerRunner<Data, Params> = {
23
24
  advance: (count?: number) => void;
24
25
  setParams: (patch: Partial<Params>) => void;
25
26
  resetWith: (patch?: Partial<Params>) => void;
27
+ setConfig: (patch: Partial<WorkerConfig>) => void;
28
+ /**
29
+ * Record a draw time for a tick. Draws happen on the main thread, so
30
+ * timings are tracked here — they never cross the postMessage boundary.
31
+ */
32
+ recordDrawTime: (tick: number, ms: number) => void;
33
+ getPerformance: () => readonly TickPerformance[];
26
34
  destroy: () => void;
27
35
  };
28
36
  declare function createWorkerRunner<Data, Params>(worker: Worker, config: WorkerRunnerConfig<Params>): WorkerRunner<Data, Params>;