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
package/README.md CHANGED
@@ -101,6 +101,8 @@ Useful when `step` is expensive (large grids, n-body simulations, fluid solvers)
101
101
  | `automatick/react/performance` | `<PerformanceOverlay>` |
102
102
  | `automatick/react/context` | `<SimulationContext>` |
103
103
 
104
+ All entry points use named exports. If you'd rather have a single namespace object, use `import * as Automatick from 'automatick'` and call `Automatick.createEngine(...)` — same for any subpath.
105
+
104
106
  ## License
105
107
 
106
108
  MIT
@@ -1,3 +1,7 @@
1
+ import {
2
+ isInitFn
3
+ } from "./chunk-IKR53C2U.js";
4
+
1
5
  // src/engine.ts
2
6
  var PERF_BUFFER_SIZE = 120;
3
7
  var SimulationEngine = class {
@@ -16,15 +20,40 @@ var SimulationEngine = class {
16
20
  listeners = /* @__PURE__ */ new Set();
17
21
  historyListeners = /* @__PURE__ */ new Set();
18
22
  perfBuffer = [];
23
+ rafId = null;
24
+ rafCancel = null;
19
25
  constructor(config) {
20
- this.initFn = config.init;
26
+ const init = config.init;
27
+ if (isInitFn(init)) {
28
+ this.initFn = init;
29
+ } else {
30
+ const seed = structuredClone(init);
31
+ this.initFn = () => structuredClone(seed);
32
+ }
21
33
  this.stepFn = config.step;
22
34
  this.shouldStopFn = config.shouldStop;
23
35
  this.maxTime = config.maxTime;
24
36
  this.delayMs = config.delayMs ?? 0;
25
37
  this.ticksPerFrame = config.ticksPerFrame ?? 1;
26
- this.params = { ...config.initialParams };
38
+ this.params = config.initialParams ? { ...config.initialParams } : {};
27
39
  this.data = this.initFn(this.params);
40
+ if (config.render) {
41
+ this.listeners.add(config.render);
42
+ config.render(this.getSnapshot());
43
+ }
44
+ const autoFrame = config.autoFrame ?? true;
45
+ if (autoFrame) {
46
+ const raf = globalThis.requestAnimationFrame;
47
+ const caf = globalThis.cancelAnimationFrame;
48
+ if (typeof raf === "function" && typeof caf === "function") {
49
+ const loop = (now) => {
50
+ this.handleAnimationFrame(now);
51
+ this.rafId = raf(loop);
52
+ };
53
+ this.rafCancel = caf;
54
+ this.rafId = raf(loop);
55
+ }
56
+ }
28
57
  }
29
58
  getSnapshot() {
30
59
  return {
@@ -141,6 +170,11 @@ var SimulationEngine = class {
141
170
  }
142
171
  }
143
172
  destroy() {
173
+ if (this.rafId !== null && this.rafCancel) {
174
+ this.rafCancel(this.rafId);
175
+ }
176
+ this.rafId = null;
177
+ this.rafCancel = null;
144
178
  this.listeners.clear();
145
179
  this.historyListeners.clear();
146
180
  }
@@ -160,7 +194,9 @@ var SimulationEngine = class {
160
194
  this.data = this.stepFn({
161
195
  data: this.data,
162
196
  params: this.params,
163
- tick: this.tick
197
+ tick: this.tick,
198
+ status: this.status,
199
+ stepDurationMs: this.lastStepMs
164
200
  });
165
201
  const t1 = performance.now();
166
202
  this.lastStepMs = t1 - t0;
@@ -0,0 +1,12 @@
1
+ // src/sim.ts
2
+ function isInitFn(init) {
3
+ return typeof init === "function";
4
+ }
5
+ function defineSim(sim) {
6
+ return sim;
7
+ }
8
+
9
+ export {
10
+ isInitFn,
11
+ defineSim
12
+ };
@@ -0,0 +1,89 @@
1
+ // src/worker/serialize.ts
2
+ function deserializeWorkerMessage(raw) {
3
+ return raw;
4
+ }
5
+ function serializeMainMessage(msg) {
6
+ return msg;
7
+ }
8
+
9
+ // src/worker/workerRunner.ts
10
+ var PERF_BUFFER_SIZE = 120;
11
+ function createWorkerRunner(worker, config) {
12
+ const listeners = /* @__PURE__ */ new Set();
13
+ let currentSnapshot = {
14
+ data: void 0,
15
+ params: config.initialParams,
16
+ tick: 0,
17
+ status: "idle",
18
+ stepDurationMs: 0
19
+ };
20
+ const perfBuffer = [];
21
+ function send(msg) {
22
+ worker.postMessage(serializeMainMessage(msg));
23
+ }
24
+ function emit() {
25
+ for (const l of listeners) {
26
+ l(currentSnapshot);
27
+ }
28
+ }
29
+ function pushPerf(snapshot) {
30
+ if (snapshot.tick <= 0) return;
31
+ const last = perfBuffer[perfBuffer.length - 1];
32
+ if (last && last.tick === snapshot.tick) return;
33
+ if (perfBuffer.length >= PERF_BUFFER_SIZE) perfBuffer.shift();
34
+ perfBuffer.push({ tick: snapshot.tick, stepMs: snapshot.stepDurationMs });
35
+ }
36
+ worker.onmessage = (event) => {
37
+ const msg = deserializeWorkerMessage(event.data);
38
+ switch (msg.kind) {
39
+ case "snapshot":
40
+ currentSnapshot = msg.snapshot;
41
+ pushPerf(msg.snapshot);
42
+ emit();
43
+ break;
44
+ case "error":
45
+ currentSnapshot = { ...currentSnapshot, status: "stopped" };
46
+ emit();
47
+ break;
48
+ }
49
+ };
50
+ worker.onerror = () => {
51
+ currentSnapshot = { ...currentSnapshot, status: "stopped" };
52
+ emit();
53
+ };
54
+ return {
55
+ getSnapshot: () => currentSnapshot,
56
+ subscribe(listener) {
57
+ listeners.add(listener);
58
+ return () => {
59
+ listeners.delete(listener);
60
+ };
61
+ },
62
+ play: () => send({ kind: "play" }),
63
+ pause: () => send({ kind: "pause" }),
64
+ stop: () => send({ kind: "stop" }),
65
+ seek: (tick) => send({ kind: "seek", tick }),
66
+ advance: (count = 1) => send({ kind: "advance", count }),
67
+ setParams: (patch) => send({ kind: "setParams", patch }),
68
+ resetWith: (patch) => send({ kind: "resetWith", patch }),
69
+ setConfig: (patch) => send({ kind: "setConfig", patch }),
70
+ recordDrawTime(tick, ms) {
71
+ for (let i = perfBuffer.length - 1; i >= 0; i--) {
72
+ if (perfBuffer[i].tick === tick) {
73
+ perfBuffer[i].drawMs = ms;
74
+ return;
75
+ }
76
+ }
77
+ },
78
+ getPerformance: () => perfBuffer,
79
+ destroy() {
80
+ listeners.clear();
81
+ send({ kind: "destroy" });
82
+ worker.terminate();
83
+ }
84
+ };
85
+ }
86
+
87
+ export {
88
+ createWorkerRunner
89
+ };
@@ -0,0 +1,132 @@
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
+ // Merge sim.defaultParams under the patch sent from main \u2014 the main
52
+ // thread sees the sim module via a URL, so it can't apply defaults.
53
+ const initialParams = { ...(sim.defaultParams || {}), ...(msg.params || {}) };
54
+ engine = engineMod.createEngine({
55
+ init: sim.init,
56
+ step: sim.step,
57
+ shouldStop: sim.shouldStop,
58
+ initialParams,
59
+ maxTime: msg.config.maxTime,
60
+ // Worker host owns its own setTimeout-driven loop; rAF wouldn't exist here anyway.
61
+ autoFrame: false,
62
+ });
63
+ emitSnapshot();
64
+ break;
65
+ }
66
+ case 'play':
67
+ if (!engine) return;
68
+ engine.play(); emitSnapshot(); stopLoop(); loopTimer = setTimeout(tickLoop, 0);
69
+ break;
70
+ case 'pause':
71
+ if (!engine) return;
72
+ stopLoop(); engine.pause(); emitSnapshot();
73
+ break;
74
+ case 'stop':
75
+ if (!engine) return;
76
+ stopLoop(); engine.stop(); emitSnapshot();
77
+ break;
78
+ case 'seek':
79
+ if (!engine) return;
80
+ stopLoop(); engine.seek(msg.tick); emitSnapshot();
81
+ break;
82
+ case 'advance':
83
+ if (!engine) return;
84
+ engine.advance(msg.count); emitSnapshot();
85
+ break;
86
+ case 'setParams':
87
+ if (!engine) return;
88
+ engine.setParams(msg.patch); emitSnapshot();
89
+ break;
90
+ case 'resetWith':
91
+ if (!engine) return;
92
+ stopLoop(); engine.resetWith(msg.patch); emitSnapshot();
93
+ break;
94
+ case 'setConfig':
95
+ if (msg.patch.delayMs !== undefined) delayMs = msg.patch.delayMs;
96
+ if (msg.patch.ticksPerFrame !== undefined) ticksPerFrame = msg.patch.ticksPerFrame;
97
+ if (msg.patch.snapshotIntervalMs !== undefined) snapshotIntervalMs = msg.patch.snapshotIntervalMs;
98
+ break;
99
+ case 'destroy':
100
+ stopLoop();
101
+ if (engine) { engine.destroy(); engine = null; }
102
+ self.close();
103
+ break;
104
+ }
105
+ } catch (err) {
106
+ postMessage({ kind: 'error', error: { message: err.message, stack: err.stack } });
107
+ }
108
+ };
109
+ `;
110
+ function createSimWorker(options) {
111
+ const blob = new Blob([WORKER_SCRIPT], { type: "text/javascript" });
112
+ const blobUrl = URL.createObjectURL(blob);
113
+ const worker = new Worker(blobUrl, { type: "module" });
114
+ worker.addEventListener("message", function cleanup(event) {
115
+ if (event.data?.kind === "snapshot" || event.data?.kind === "error") {
116
+ URL.revokeObjectURL(blobUrl);
117
+ worker.removeEventListener("message", cleanup);
118
+ }
119
+ });
120
+ worker.postMessage({
121
+ kind: "init",
122
+ moduleUrl: options.moduleUrl,
123
+ engineUrl: options.engineUrl,
124
+ params: options.initialParams,
125
+ config: options.config
126
+ });
127
+ return worker;
128
+ }
129
+
130
+ export {
131
+ createSimWorker
132
+ };
package/dist/engine.cjs CHANGED
@@ -24,6 +24,13 @@ __export(engine_exports, {
24
24
  createEngine: () => createEngine
25
25
  });
26
26
  module.exports = __toCommonJS(engine_exports);
27
+
28
+ // src/sim.ts
29
+ function isInitFn(init) {
30
+ return typeof init === "function";
31
+ }
32
+
33
+ // src/engine.ts
27
34
  var PERF_BUFFER_SIZE = 120;
28
35
  var SimulationEngine = class {
29
36
  data;
@@ -41,15 +48,40 @@ var SimulationEngine = class {
41
48
  listeners = /* @__PURE__ */ new Set();
42
49
  historyListeners = /* @__PURE__ */ new Set();
43
50
  perfBuffer = [];
51
+ rafId = null;
52
+ rafCancel = null;
44
53
  constructor(config) {
45
- this.initFn = config.init;
54
+ const init = config.init;
55
+ if (isInitFn(init)) {
56
+ this.initFn = init;
57
+ } else {
58
+ const seed = structuredClone(init);
59
+ this.initFn = () => structuredClone(seed);
60
+ }
46
61
  this.stepFn = config.step;
47
62
  this.shouldStopFn = config.shouldStop;
48
63
  this.maxTime = config.maxTime;
49
64
  this.delayMs = config.delayMs ?? 0;
50
65
  this.ticksPerFrame = config.ticksPerFrame ?? 1;
51
- this.params = { ...config.initialParams };
66
+ this.params = config.initialParams ? { ...config.initialParams } : {};
52
67
  this.data = this.initFn(this.params);
68
+ if (config.render) {
69
+ this.listeners.add(config.render);
70
+ config.render(this.getSnapshot());
71
+ }
72
+ const autoFrame = config.autoFrame ?? true;
73
+ if (autoFrame) {
74
+ const raf = globalThis.requestAnimationFrame;
75
+ const caf = globalThis.cancelAnimationFrame;
76
+ if (typeof raf === "function" && typeof caf === "function") {
77
+ const loop = (now) => {
78
+ this.handleAnimationFrame(now);
79
+ this.rafId = raf(loop);
80
+ };
81
+ this.rafCancel = caf;
82
+ this.rafId = raf(loop);
83
+ }
84
+ }
53
85
  }
54
86
  getSnapshot() {
55
87
  return {
@@ -166,6 +198,11 @@ var SimulationEngine = class {
166
198
  }
167
199
  }
168
200
  destroy() {
201
+ if (this.rafId !== null && this.rafCancel) {
202
+ this.rafCancel(this.rafId);
203
+ }
204
+ this.rafId = null;
205
+ this.rafCancel = null;
169
206
  this.listeners.clear();
170
207
  this.historyListeners.clear();
171
208
  }
@@ -185,7 +222,9 @@ var SimulationEngine = class {
185
222
  this.data = this.stepFn({
186
223
  data: this.data,
187
224
  params: this.params,
188
- tick: this.tick
225
+ tick: this.tick,
226
+ status: this.status,
227
+ stepDurationMs: this.lastStepMs
189
228
  });
190
229
  const t1 = performance.now();
191
230
  this.lastStepMs = t1 - t0;
package/dist/engine.d.cts CHANGED
@@ -1,28 +1,50 @@
1
- import { StepArgs } from './sim.cjs';
1
+ import { SimInit } from './sim.cjs';
2
+ import { State, SimulationStatus } from './state.cjs';
2
3
 
3
- type SimulationStatus = 'idle' | 'playing' | 'paused' | 'stopped';
4
4
  type TickPerformance = {
5
5
  tick: number;
6
6
  stepMs: number;
7
7
  drawMs?: number;
8
8
  };
9
- type EngineConfig<Data, Params> = {
10
- init: (params: Params) => Data;
11
- step: (args: StepArgs<Data, Params>) => Data;
9
+ type EngineConfig<Data, Params = Record<string, never>> = {
10
+ /**
11
+ * Initial simulation state — value or `(params) => Data`. See `SimInit`.
12
+ * When a value is passed, the engine `structuredClone`s it on each (re)init.
13
+ */
14
+ init: SimInit<Data, Params>;
15
+ step: (state: State<Data, Params>) => Data;
12
16
  shouldStop?: (data: Data, params: Params) => boolean;
13
- initialParams: Params;
17
+ /**
18
+ * Initial param values. Optional — when omitted, the engine seeds an empty
19
+ * params object and `Params` defaults to `Record<string, never>`.
20
+ */
21
+ initialParams?: Params;
14
22
  maxTime?: number;
15
23
  delayMs?: number;
16
24
  ticksPerFrame?: number;
25
+ /**
26
+ * Optional render callback — sugar for the vanilla path. When provided, the
27
+ * engine calls it once with the initial state (right after init) and on
28
+ * every state emit thereafter, equivalent to `engine.subscribe(render)`
29
+ * followed by an initial paint. `subscribe` remains the lower-level
30
+ * primitive; React adapter and worker callers wire their own subscribers.
31
+ */
32
+ render?: (snapshot: State<Data, Params>) => void;
33
+ /**
34
+ * Drive `handleAnimationFrame` from an internal `requestAnimationFrame` loop.
35
+ * Defaults to `true` so vanilla callers don't have to write the loop. The
36
+ * React adapter and worker host pass `false` because they either own the
37
+ * frame loop themselves or run in an environment that has none. The loop is
38
+ * created at construction and torn down by `destroy()`. If
39
+ * `globalThis.requestAnimationFrame` is missing (server, worker), the option
40
+ * is silently a no-op.
41
+ *
42
+ * Note: when `true`, the rAF closure pins this engine in memory — vanilla
43
+ * consumers must call `destroy()` to let it be garbage-collected.
44
+ */
45
+ autoFrame?: boolean;
17
46
  };
18
- type EngineSnapshot<Data, Params> = {
19
- data: Data;
20
- params: Params;
21
- tick: number;
22
- status: SimulationStatus;
23
- stepDurationMs: number;
24
- };
25
- declare class SimulationEngine<Data, Params> {
47
+ declare class SimulationEngine<Data, Params = Record<string, never>> {
26
48
  private data;
27
49
  private params;
28
50
  private tick;
@@ -38,14 +60,16 @@ declare class SimulationEngine<Data, Params> {
38
60
  private readonly listeners;
39
61
  private readonly historyListeners;
40
62
  private readonly perfBuffer;
63
+ private rafId;
64
+ private rafCancel;
41
65
  constructor(config: EngineConfig<Data, Params>);
42
- getSnapshot(): EngineSnapshot<Data, Params>;
66
+ getSnapshot(): State<Data, Params>;
43
67
  getStatus(): SimulationStatus;
44
68
  getPerformance(): readonly TickPerformance[];
45
69
  setDelayMs(ms: number): void;
46
70
  setTicksPerFrame(n: number): void;
47
71
  recordDrawTime(tick: number, ms: number): void;
48
- subscribe(listener: (snapshot: EngineSnapshot<Data, Params>) => void): () => void;
72
+ subscribe(listener: (snapshot: State<Data, Params>) => void): () => void;
49
73
  subscribeHistory(listener: (entry: {
50
74
  tick: number;
51
75
  data: Data;
@@ -67,6 +91,6 @@ declare class SimulationEngine<Data, Params> {
67
91
  private emit;
68
92
  private emitHistory;
69
93
  }
70
- declare function createEngine<Data, Params>(config: EngineConfig<Data, Params>): SimulationEngine<Data, Params>;
94
+ declare function createEngine<Data, Params = Record<string, never>>(config: EngineConfig<Data, Params>): SimulationEngine<Data, Params>;
71
95
 
72
- export { type EngineConfig, type EngineSnapshot, SimulationEngine, type SimulationStatus, type TickPerformance, createEngine };
96
+ export { type EngineConfig, SimulationEngine, SimulationStatus, State, type TickPerformance, createEngine };
package/dist/engine.d.ts CHANGED
@@ -1,28 +1,50 @@
1
- import { StepArgs } from './sim.js';
1
+ import { SimInit } from './sim.js';
2
+ import { State, SimulationStatus } from './state.js';
2
3
 
3
- type SimulationStatus = 'idle' | 'playing' | 'paused' | 'stopped';
4
4
  type TickPerformance = {
5
5
  tick: number;
6
6
  stepMs: number;
7
7
  drawMs?: number;
8
8
  };
9
- type EngineConfig<Data, Params> = {
10
- init: (params: Params) => Data;
11
- step: (args: StepArgs<Data, Params>) => Data;
9
+ type EngineConfig<Data, Params = Record<string, never>> = {
10
+ /**
11
+ * Initial simulation state — value or `(params) => Data`. See `SimInit`.
12
+ * When a value is passed, the engine `structuredClone`s it on each (re)init.
13
+ */
14
+ init: SimInit<Data, Params>;
15
+ step: (state: State<Data, Params>) => Data;
12
16
  shouldStop?: (data: Data, params: Params) => boolean;
13
- initialParams: Params;
17
+ /**
18
+ * Initial param values. Optional — when omitted, the engine seeds an empty
19
+ * params object and `Params` defaults to `Record<string, never>`.
20
+ */
21
+ initialParams?: Params;
14
22
  maxTime?: number;
15
23
  delayMs?: number;
16
24
  ticksPerFrame?: number;
25
+ /**
26
+ * Optional render callback — sugar for the vanilla path. When provided, the
27
+ * engine calls it once with the initial state (right after init) and on
28
+ * every state emit thereafter, equivalent to `engine.subscribe(render)`
29
+ * followed by an initial paint. `subscribe` remains the lower-level
30
+ * primitive; React adapter and worker callers wire their own subscribers.
31
+ */
32
+ render?: (snapshot: State<Data, Params>) => void;
33
+ /**
34
+ * Drive `handleAnimationFrame` from an internal `requestAnimationFrame` loop.
35
+ * Defaults to `true` so vanilla callers don't have to write the loop. The
36
+ * React adapter and worker host pass `false` because they either own the
37
+ * frame loop themselves or run in an environment that has none. The loop is
38
+ * created at construction and torn down by `destroy()`. If
39
+ * `globalThis.requestAnimationFrame` is missing (server, worker), the option
40
+ * is silently a no-op.
41
+ *
42
+ * Note: when `true`, the rAF closure pins this engine in memory — vanilla
43
+ * consumers must call `destroy()` to let it be garbage-collected.
44
+ */
45
+ autoFrame?: boolean;
17
46
  };
18
- type EngineSnapshot<Data, Params> = {
19
- data: Data;
20
- params: Params;
21
- tick: number;
22
- status: SimulationStatus;
23
- stepDurationMs: number;
24
- };
25
- declare class SimulationEngine<Data, Params> {
47
+ declare class SimulationEngine<Data, Params = Record<string, never>> {
26
48
  private data;
27
49
  private params;
28
50
  private tick;
@@ -38,14 +60,16 @@ declare class SimulationEngine<Data, Params> {
38
60
  private readonly listeners;
39
61
  private readonly historyListeners;
40
62
  private readonly perfBuffer;
63
+ private rafId;
64
+ private rafCancel;
41
65
  constructor(config: EngineConfig<Data, Params>);
42
- getSnapshot(): EngineSnapshot<Data, Params>;
66
+ getSnapshot(): State<Data, Params>;
43
67
  getStatus(): SimulationStatus;
44
68
  getPerformance(): readonly TickPerformance[];
45
69
  setDelayMs(ms: number): void;
46
70
  setTicksPerFrame(n: number): void;
47
71
  recordDrawTime(tick: number, ms: number): void;
48
- subscribe(listener: (snapshot: EngineSnapshot<Data, Params>) => void): () => void;
72
+ subscribe(listener: (snapshot: State<Data, Params>) => void): () => void;
49
73
  subscribeHistory(listener: (entry: {
50
74
  tick: number;
51
75
  data: Data;
@@ -67,6 +91,6 @@ declare class SimulationEngine<Data, Params> {
67
91
  private emit;
68
92
  private emitHistory;
69
93
  }
70
- declare function createEngine<Data, Params>(config: EngineConfig<Data, Params>): SimulationEngine<Data, Params>;
94
+ declare function createEngine<Data, Params = Record<string, never>>(config: EngineConfig<Data, Params>): SimulationEngine<Data, Params>;
71
95
 
72
- export { type EngineConfig, type EngineSnapshot, SimulationEngine, type SimulationStatus, type TickPerformance, createEngine };
96
+ export { type EngineConfig, SimulationEngine, SimulationStatus, State, type TickPerformance, createEngine };
package/dist/engine.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  SimulationEngine,
3
3
  createEngine
4
- } from "./chunk-YNLOTPDY.js";
4
+ } from "./chunk-A375T3UD.js";
5
+ import "./chunk-IKR53C2U.js";
5
6
  export {
6
7
  SimulationEngine,
7
8
  createEngine
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
- import { EngineSnapshot, TickPerformance } from '../engine.cjs';
2
+ import { TickPerformance } from '../engine.cjs';
3
+ import { State } from '../state.cjs';
3
4
  import '../sim.cjs';
4
5
 
5
6
  /**
@@ -8,8 +9,8 @@ import '../sim.cjs';
8
9
  * and to record draw timing back to the engine's performance buffer.
9
10
  */
10
11
  type EngineContextValue = {
11
- subscribe: (listener: (snapshot: EngineSnapshot<unknown, unknown>) => void) => () => void;
12
- getSnapshot: () => EngineSnapshot<unknown, unknown>;
12
+ subscribe: (listener: (snapshot: State<unknown, unknown>) => void) => () => void;
13
+ getSnapshot: () => State<unknown, unknown>;
13
14
  recordDrawTime: (tick: number, ms: number) => void;
14
15
  getPerformance: () => readonly TickPerformance[];
15
16
  };
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
- import { EngineSnapshot, TickPerformance } from '../engine.js';
2
+ import { TickPerformance } from '../engine.js';
3
+ import { State } from '../state.js';
3
4
  import '../sim.js';
4
5
 
5
6
  /**
@@ -8,8 +9,8 @@ import '../sim.js';
8
9
  * and to record draw timing back to the engine's performance buffer.
9
10
  */
10
11
  type EngineContextValue = {
11
- subscribe: (listener: (snapshot: EngineSnapshot<unknown, unknown>) => void) => () => void;
12
- getSnapshot: () => EngineSnapshot<unknown, unknown>;
12
+ subscribe: (listener: (snapshot: State<unknown, unknown>) => void) => () => void;
13
+ getSnapshot: () => State<unknown, unknown>;
13
14
  recordDrawTime: (tick: number, ms: number) => void;
14
15
  getPerformance: () => readonly TickPerformance[];
15
16
  };