automatick 0.0.2 → 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.
@@ -73,7 +73,7 @@ var SimulationEngine = class {
73
73
  this.maxTime = config.maxTime;
74
74
  this.delayMs = config.delayMs ?? 0;
75
75
  this.ticksPerFrame = config.ticksPerFrame ?? 1;
76
- this.params = { ...config.initialParams };
76
+ this.params = config.initialParams ? { ...config.initialParams } : {};
77
77
  this.data = this.initFn(this.params);
78
78
  if (config.render) {
79
79
  this.listeners.add(config.render);
@@ -274,6 +274,221 @@ function createEngine(config) {
274
274
  return new SimulationEngine(config);
275
275
  }
276
276
 
277
+ // src/worker/createSimWorker.ts
278
+ var WORKER_SCRIPT = `
279
+ let engine = null;
280
+ let loopTimer = null;
281
+ let snapshotIntervalMs = 16;
282
+ let lastSnapshotMs = 0;
283
+ let ticksPerFrame = 1;
284
+ let delayMs = 0;
285
+
286
+ function emitSnapshot() {
287
+ if (!engine) return;
288
+ postMessage({ kind: 'snapshot', snapshot: engine.getSnapshot() });
289
+ lastSnapshotMs = performance.now();
290
+ }
291
+
292
+ function tickLoop() {
293
+ if (!engine || engine.getStatus() !== 'playing') return;
294
+
295
+ for (let i = 0; i < ticksPerFrame; i++) {
296
+ engine.advance(1);
297
+ const s = engine.getStatus();
298
+ if (s === 'stopped') { emitSnapshot(); return; }
299
+ if (s !== 'paused') break;
300
+ }
301
+
302
+ // advance() transitions to paused; resume playing for the next batch
303
+ if (engine.getStatus() === 'paused') engine.play();
304
+
305
+ if (performance.now() - lastSnapshotMs >= snapshotIntervalMs) emitSnapshot();
306
+ loopTimer = setTimeout(tickLoop, delayMs);
307
+ }
308
+
309
+ function stopLoop() {
310
+ if (loopTimer !== null) { clearTimeout(loopTimer); loopTimer = null; }
311
+ }
312
+
313
+ self.onmessage = async (event) => {
314
+ const msg = event.data;
315
+ try {
316
+ switch (msg.kind) {
317
+ case 'init': {
318
+ delayMs = msg.config.delayMs || 0;
319
+ ticksPerFrame = msg.config.ticksPerFrame || 1;
320
+ snapshotIntervalMs = msg.config.snapshotIntervalMs || 16;
321
+
322
+ const [simMod, engineMod] = await Promise.all([
323
+ import(msg.moduleUrl),
324
+ import(msg.engineUrl),
325
+ ]);
326
+ const sim = simMod.default;
327
+ // Merge sim.defaultParams under the patch sent from main \u2014 the main
328
+ // thread sees the sim module via a URL, so it can't apply defaults.
329
+ const initialParams = { ...(sim.defaultParams || {}), ...(msg.params || {}) };
330
+ engine = engineMod.createEngine({
331
+ init: sim.init,
332
+ step: sim.step,
333
+ shouldStop: sim.shouldStop,
334
+ initialParams,
335
+ maxTime: msg.config.maxTime,
336
+ // Worker host owns its own setTimeout-driven loop; rAF wouldn't exist here anyway.
337
+ autoFrame: false,
338
+ });
339
+ emitSnapshot();
340
+ break;
341
+ }
342
+ case 'play':
343
+ if (!engine) return;
344
+ engine.play(); emitSnapshot(); stopLoop(); loopTimer = setTimeout(tickLoop, 0);
345
+ break;
346
+ case 'pause':
347
+ if (!engine) return;
348
+ stopLoop(); engine.pause(); emitSnapshot();
349
+ break;
350
+ case 'stop':
351
+ if (!engine) return;
352
+ stopLoop(); engine.stop(); emitSnapshot();
353
+ break;
354
+ case 'seek':
355
+ if (!engine) return;
356
+ stopLoop(); engine.seek(msg.tick); emitSnapshot();
357
+ break;
358
+ case 'advance':
359
+ if (!engine) return;
360
+ engine.advance(msg.count); emitSnapshot();
361
+ break;
362
+ case 'setParams':
363
+ if (!engine) return;
364
+ engine.setParams(msg.patch); emitSnapshot();
365
+ break;
366
+ case 'resetWith':
367
+ if (!engine) return;
368
+ stopLoop(); engine.resetWith(msg.patch); emitSnapshot();
369
+ break;
370
+ case 'setConfig':
371
+ if (msg.patch.delayMs !== undefined) delayMs = msg.patch.delayMs;
372
+ if (msg.patch.ticksPerFrame !== undefined) ticksPerFrame = msg.patch.ticksPerFrame;
373
+ if (msg.patch.snapshotIntervalMs !== undefined) snapshotIntervalMs = msg.patch.snapshotIntervalMs;
374
+ break;
375
+ case 'destroy':
376
+ stopLoop();
377
+ if (engine) { engine.destroy(); engine = null; }
378
+ self.close();
379
+ break;
380
+ }
381
+ } catch (err) {
382
+ postMessage({ kind: 'error', error: { message: err.message, stack: err.stack } });
383
+ }
384
+ };
385
+ `;
386
+ function createSimWorker(options) {
387
+ const blob = new Blob([WORKER_SCRIPT], { type: "text/javascript" });
388
+ const blobUrl = URL.createObjectURL(blob);
389
+ const worker = new Worker(blobUrl, { type: "module" });
390
+ worker.addEventListener("message", function cleanup(event) {
391
+ if (event.data?.kind === "snapshot" || event.data?.kind === "error") {
392
+ URL.revokeObjectURL(blobUrl);
393
+ worker.removeEventListener("message", cleanup);
394
+ }
395
+ });
396
+ worker.postMessage({
397
+ kind: "init",
398
+ moduleUrl: options.moduleUrl,
399
+ engineUrl: options.engineUrl,
400
+ params: options.initialParams,
401
+ config: options.config
402
+ });
403
+ return worker;
404
+ }
405
+
406
+ // src/worker/serialize.ts
407
+ function deserializeWorkerMessage(raw) {
408
+ return raw;
409
+ }
410
+ function serializeMainMessage(msg) {
411
+ return msg;
412
+ }
413
+
414
+ // src/worker/workerRunner.ts
415
+ var PERF_BUFFER_SIZE2 = 120;
416
+ function createWorkerRunner(worker, config) {
417
+ const listeners = /* @__PURE__ */ new Set();
418
+ let currentSnapshot = {
419
+ data: void 0,
420
+ params: config.initialParams,
421
+ tick: 0,
422
+ status: "idle",
423
+ stepDurationMs: 0
424
+ };
425
+ const perfBuffer = [];
426
+ function send(msg) {
427
+ worker.postMessage(serializeMainMessage(msg));
428
+ }
429
+ function emit() {
430
+ for (const l of listeners) {
431
+ l(currentSnapshot);
432
+ }
433
+ }
434
+ function pushPerf(snapshot) {
435
+ if (snapshot.tick <= 0) return;
436
+ const last = perfBuffer[perfBuffer.length - 1];
437
+ if (last && last.tick === snapshot.tick) return;
438
+ if (perfBuffer.length >= PERF_BUFFER_SIZE2) perfBuffer.shift();
439
+ perfBuffer.push({ tick: snapshot.tick, stepMs: snapshot.stepDurationMs });
440
+ }
441
+ worker.onmessage = (event) => {
442
+ const msg = deserializeWorkerMessage(event.data);
443
+ switch (msg.kind) {
444
+ case "snapshot":
445
+ currentSnapshot = msg.snapshot;
446
+ pushPerf(msg.snapshot);
447
+ emit();
448
+ break;
449
+ case "error":
450
+ currentSnapshot = { ...currentSnapshot, status: "stopped" };
451
+ emit();
452
+ break;
453
+ }
454
+ };
455
+ worker.onerror = () => {
456
+ currentSnapshot = { ...currentSnapshot, status: "stopped" };
457
+ emit();
458
+ };
459
+ return {
460
+ getSnapshot: () => currentSnapshot,
461
+ subscribe(listener) {
462
+ listeners.add(listener);
463
+ return () => {
464
+ listeners.delete(listener);
465
+ };
466
+ },
467
+ play: () => send({ kind: "play" }),
468
+ pause: () => send({ kind: "pause" }),
469
+ stop: () => send({ kind: "stop" }),
470
+ seek: (tick) => send({ kind: "seek", tick }),
471
+ advance: (count = 1) => send({ kind: "advance", count }),
472
+ setParams: (patch) => send({ kind: "setParams", patch }),
473
+ resetWith: (patch) => send({ kind: "resetWith", patch }),
474
+ setConfig: (patch) => send({ kind: "setConfig", patch }),
475
+ recordDrawTime(tick, ms) {
476
+ for (let i = perfBuffer.length - 1; i >= 0; i--) {
477
+ if (perfBuffer[i].tick === tick) {
478
+ perfBuffer[i].drawMs = ms;
479
+ return;
480
+ }
481
+ }
482
+ },
483
+ getPerformance: () => perfBuffer,
484
+ destroy() {
485
+ listeners.clear();
486
+ send({ kind: "destroy" });
487
+ worker.terminate();
488
+ }
489
+ };
490
+ }
491
+
277
492
  // src/react/SimulationContext.tsx
278
493
  var import_react = __toESM(require("react"), 1);
279
494
  var SimulationContext = import_react.default.createContext(null);
@@ -296,15 +511,31 @@ function useStableCallback(fn) {
296
511
 
297
512
  // src/react/Simulation.tsx
298
513
  var import_jsx_runtime = require("react/jsx-runtime");
514
+ var import_meta = {};
299
515
  function LocalSimulation(props) {
300
- const { sim, params: paramsProp, children, autoplay } = props;
516
+ const {
517
+ init,
518
+ step,
519
+ shouldStop,
520
+ defaultParams,
521
+ params: paramsProp,
522
+ children,
523
+ autoplay
524
+ } = props;
301
525
  const engineRef = import_react4.default.useRef(null);
302
526
  if (!engineRef.current) {
303
- const initialParams = paramsProp ? { ...sim.defaultParams, ...paramsProp } : sim.defaultParams;
527
+ let initialParams;
528
+ if (defaultParams && paramsProp) {
529
+ initialParams = { ...defaultParams, ...paramsProp };
530
+ } else if (defaultParams) {
531
+ initialParams = defaultParams;
532
+ } else if (paramsProp) {
533
+ initialParams = paramsProp;
534
+ }
304
535
  engineRef.current = createEngine({
305
- init: sim.init,
306
- step: sim.step,
307
- shouldStop: sim.shouldStop,
536
+ init,
537
+ step,
538
+ shouldStop,
308
539
  initialParams,
309
540
  maxTime: props.maxTime,
310
541
  delayMs: props.delayMs,
@@ -346,119 +577,70 @@ function LocalSimulation(props) {
346
577
  }, [engine]);
347
578
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SimulationProvider, { snapshot, backend: engine, children });
348
579
  }
580
+ function engineUrl() {
581
+ return new URL("../standalone/engine.js", import_meta.url).href;
582
+ }
349
583
  function WorkerSimulation(props) {
350
584
  const { children, autoplay } = props;
351
- const [backend, setBackend] = import_react4.default.useState(
585
+ const [runner, setRunner] = import_react4.default.useState(
352
586
  null
353
587
  );
354
588
  const [snapshot, setSnapshot] = import_react4.default.useState(
355
589
  null
356
590
  );
357
- const propsRef = import_react4.default.useRef(props);
358
- propsRef.current = props;
359
591
  import_react4.default.useEffect(() => {
360
- let cancelled = false;
361
- let runner = null;
362
- (async () => {
363
- const mod = await propsRef.current.worker();
364
- const simModule = mod.default;
365
- if (cancelled) return;
366
- const p = propsRef.current;
367
- const initialParams = p.params ? { ...simModule.defaultParams, ...p.params } : simModule.defaultParams;
368
- const engine = createEngine({
369
- init: simModule.init,
370
- step: simModule.step,
371
- shouldStop: simModule.shouldStop,
372
- initialParams,
373
- maxTime: p.maxTime,
374
- autoFrame: false
375
- });
376
- if (cancelled) {
377
- engine.destroy();
378
- return;
379
- }
380
- const delayMs = p.delayMs ?? 0;
381
- const ticksPerFrame = p.ticksPerFrame ?? 1;
382
- let loopTimer = null;
383
- function stopLoop() {
384
- if (loopTimer !== null) {
385
- clearTimeout(loopTimer);
386
- loopTimer = null;
387
- }
388
- }
389
- function tickLoop() {
390
- if (engine.getStatus() !== "playing") return;
391
- for (let i = 0; i < ticksPerFrame; i++) {
392
- engine.advance(1);
393
- const s = engine.getStatus();
394
- if (s === "stopped") return;
395
- if (s !== "paused") break;
396
- }
397
- if (engine.getStatus() === "paused") {
398
- engine.play();
399
- }
400
- loopTimer = setTimeout(tickLoop, delayMs);
592
+ const moduleUrl = props.worker.toString();
593
+ const initialParams = props.params ?? {};
594
+ const worker = createSimWorker({
595
+ moduleUrl,
596
+ engineUrl: engineUrl(),
597
+ initialParams,
598
+ config: {
599
+ maxTime: props.maxTime,
600
+ delayMs: props.delayMs,
601
+ ticksPerFrame: props.ticksPerFrame,
602
+ snapshotIntervalMs: props.snapshotIntervalMs
401
603
  }
402
- runner = {
403
- getSnapshot: () => engine.getSnapshot(),
404
- subscribe: (listener) => engine.subscribe(listener),
405
- play: () => {
406
- engine.play();
407
- stopLoop();
408
- loopTimer = setTimeout(tickLoop, 0);
409
- },
410
- pause: () => {
411
- stopLoop();
412
- engine.pause();
413
- },
414
- stop: () => {
415
- stopLoop();
416
- engine.stop();
417
- },
418
- seek: (tick) => {
419
- stopLoop();
420
- engine.seek(tick);
421
- },
422
- advance: (count) => engine.advance(count),
423
- setParams: (patch) => engine.setParams(patch),
424
- resetWith: (patch) => {
425
- stopLoop();
426
- engine.resetWith(patch);
427
- },
428
- destroy: () => {
429
- stopLoop();
430
- engine.destroy();
431
- },
432
- recordDrawTime: (tick, ms) => engine.recordDrawTime(tick, ms),
433
- getPerformance: () => engine.getPerformance()
434
- };
435
- if (!cancelled) {
436
- setBackend(runner);
437
- setSnapshot(engine.getSnapshot());
604
+ });
605
+ const r = createWorkerRunner(worker, {
606
+ initialParams,
607
+ config: {
608
+ maxTime: props.maxTime,
609
+ delayMs: props.delayMs,
610
+ ticksPerFrame: props.ticksPerFrame,
611
+ snapshotIntervalMs: props.snapshotIntervalMs
438
612
  }
439
- })();
613
+ });
614
+ const unsub = r.subscribe((next) => setSnapshot(next));
615
+ setRunner(r);
440
616
  return () => {
441
- cancelled = true;
442
- runner?.destroy();
617
+ unsub();
618
+ r.destroy();
443
619
  };
444
620
  }, []);
445
621
  import_react4.default.useEffect(() => {
446
- if (!backend) return;
447
- return backend.subscribe((next) => setSnapshot(next));
448
- }, [backend]);
449
- import_react4.default.useEffect(() => {
450
- if (autoplay && backend) backend.play();
451
- }, [backend, autoplay]);
622
+ if (autoplay && runner) runner.play();
623
+ }, [runner, autoplay]);
452
624
  const isFirstRender = import_react4.default.useRef(true);
453
625
  import_react4.default.useEffect(() => {
454
626
  if (isFirstRender.current) {
455
627
  isFirstRender.current = false;
456
628
  return;
457
629
  }
458
- if (props.params && backend) backend.setParams(props.params);
459
- }, [backend, props.params]);
460
- if (!snapshot || !backend) return null;
461
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SimulationProvider, { snapshot, backend, children });
630
+ if (props.params && runner) runner.setParams(props.params);
631
+ }, [runner, props.params]);
632
+ import_react4.default.useEffect(() => {
633
+ if (!runner) return;
634
+ if (props.delayMs === void 0 && props.ticksPerFrame === void 0 && props.snapshotIntervalMs === void 0)
635
+ return;
636
+ runner.setConfig({
637
+ delayMs: props.delayMs,
638
+ ticksPerFrame: props.ticksPerFrame,
639
+ snapshotIntervalMs: props.snapshotIntervalMs
640
+ });
641
+ }, [runner, props.delayMs, props.ticksPerFrame, props.snapshotIntervalMs]);
642
+ if (!snapshot || !runner || snapshot.data === void 0) return null;
643
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SimulationProvider, { snapshot, backend: runner, children });
462
644
  }
463
645
  function SimulationProvider({
464
646
  snapshot,
@@ -517,6 +699,19 @@ function Simulation(props) {
517
699
  if ("worker" in props && props.worker != null) {
518
700
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(WorkerSimulation, { ...props });
519
701
  }
702
+ if ("sim" in props && props.sim != null) {
703
+ const { sim, ...rest } = props;
704
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
705
+ LocalSimulation,
706
+ {
707
+ ...rest,
708
+ init: sim.init,
709
+ step: sim.step,
710
+ shouldStop: sim.shouldStop,
711
+ defaultParams: sim.defaultParams
712
+ }
713
+ );
714
+ }
520
715
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LocalSimulation, { ...props });
521
716
  }
522
717
  // Annotate the CommonJS export names for ESM import in node:
@@ -1,11 +1,10 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import React from 'react';
3
- import { SimModule } from '../sim.cjs';
4
- import '../state.cjs';
3
+ import { SimModule, SimInit } from '../sim.cjs';
4
+ import { State } from '../state.cjs';
5
5
 
6
- type SimulationPropsLocal<Data, Params> = {
7
- sim: SimModule<Data, Params>;
8
- worker?: never;
6
+ /** Common simulation-level props shared by all three modes. */
7
+ type SimulationPropsCommon<Params> = {
9
8
  params?: Partial<Params>;
10
9
  maxTime?: number;
11
10
  delayMs?: number;
@@ -13,20 +12,40 @@ type SimulationPropsLocal<Data, Params> = {
13
12
  autoplay?: boolean;
14
13
  children?: React.ReactNode;
15
14
  };
16
- type SimulationPropsWorker<Data, Params> = {
15
+ type SimulationPropsLocal<Data, Params> = SimulationPropsCommon<Params> & {
16
+ sim: SimModule<Data, Params>;
17
+ worker?: never;
18
+ init?: never;
19
+ step?: never;
20
+ shouldStop?: never;
21
+ defaultParams?: never;
22
+ };
23
+ type SimulationPropsWorker<_Data, Params> = SimulationPropsCommon<Params> & {
17
24
  sim?: never;
18
- worker: () => Promise<{
19
- default: SimModule<Data, Params>;
20
- }>;
21
- params?: Partial<Params>;
22
- maxTime?: number;
23
- delayMs?: number;
24
- ticksPerFrame?: number;
25
+ /**
26
+ * URL of the sim module the worker should `import()` inside its own context.
27
+ * Vite idiom: `new URL('./sim.ts', import.meta.url)`. Plain strings are
28
+ * resolved the same way.
29
+ *
30
+ * Data/Params can't be inferred from a URL, so worker-mode call sites
31
+ * specify them via the `<Simulation<Data, Params>>` generic parameters.
32
+ */
33
+ worker: URL | string;
34
+ init?: never;
35
+ step?: never;
36
+ shouldStop?: never;
37
+ defaultParams?: never;
25
38
  snapshotIntervalMs?: number;
26
- autoplay?: boolean;
27
- children?: React.ReactNode;
28
39
  };
29
- type SimulationProps<Data, Params> = SimulationPropsLocal<Data, Params> | SimulationPropsWorker<Data, Params>;
30
- declare function Simulation<Data, Params>(props: SimulationProps<Data, Params>): react_jsx_runtime.JSX.Element;
40
+ type SimulationPropsInline<Data, Params> = SimulationPropsCommon<Params> & {
41
+ sim?: never;
42
+ worker?: never;
43
+ init: SimInit<Data, Params>;
44
+ step: (state: State<Data, Params>) => Data;
45
+ shouldStop?: (data: Data, params: Params) => boolean;
46
+ defaultParams?: Params;
47
+ };
48
+ type SimulationProps<Data, Params = Record<string, never>> = SimulationPropsLocal<Data, Params> | SimulationPropsWorker<Data, Params> | SimulationPropsInline<Data, Params>;
49
+ declare function Simulation<Data, Params = Record<string, never>>(props: SimulationProps<Data, Params>): react_jsx_runtime.JSX.Element;
31
50
 
32
51
  export { Simulation, type SimulationProps };
@@ -1,11 +1,10 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import React from 'react';
3
- import { SimModule } from '../sim.js';
4
- import '../state.js';
3
+ import { SimModule, SimInit } from '../sim.js';
4
+ import { State } from '../state.js';
5
5
 
6
- type SimulationPropsLocal<Data, Params> = {
7
- sim: SimModule<Data, Params>;
8
- worker?: never;
6
+ /** Common simulation-level props shared by all three modes. */
7
+ type SimulationPropsCommon<Params> = {
9
8
  params?: Partial<Params>;
10
9
  maxTime?: number;
11
10
  delayMs?: number;
@@ -13,20 +12,40 @@ type SimulationPropsLocal<Data, Params> = {
13
12
  autoplay?: boolean;
14
13
  children?: React.ReactNode;
15
14
  };
16
- type SimulationPropsWorker<Data, Params> = {
15
+ type SimulationPropsLocal<Data, Params> = SimulationPropsCommon<Params> & {
16
+ sim: SimModule<Data, Params>;
17
+ worker?: never;
18
+ init?: never;
19
+ step?: never;
20
+ shouldStop?: never;
21
+ defaultParams?: never;
22
+ };
23
+ type SimulationPropsWorker<_Data, Params> = SimulationPropsCommon<Params> & {
17
24
  sim?: never;
18
- worker: () => Promise<{
19
- default: SimModule<Data, Params>;
20
- }>;
21
- params?: Partial<Params>;
22
- maxTime?: number;
23
- delayMs?: number;
24
- ticksPerFrame?: number;
25
+ /**
26
+ * URL of the sim module the worker should `import()` inside its own context.
27
+ * Vite idiom: `new URL('./sim.ts', import.meta.url)`. Plain strings are
28
+ * resolved the same way.
29
+ *
30
+ * Data/Params can't be inferred from a URL, so worker-mode call sites
31
+ * specify them via the `<Simulation<Data, Params>>` generic parameters.
32
+ */
33
+ worker: URL | string;
34
+ init?: never;
35
+ step?: never;
36
+ shouldStop?: never;
37
+ defaultParams?: never;
25
38
  snapshotIntervalMs?: number;
26
- autoplay?: boolean;
27
- children?: React.ReactNode;
28
39
  };
29
- type SimulationProps<Data, Params> = SimulationPropsLocal<Data, Params> | SimulationPropsWorker<Data, Params>;
30
- declare function Simulation<Data, Params>(props: SimulationProps<Data, Params>): react_jsx_runtime.JSX.Element;
40
+ type SimulationPropsInline<Data, Params> = SimulationPropsCommon<Params> & {
41
+ sim?: never;
42
+ worker?: never;
43
+ init: SimInit<Data, Params>;
44
+ step: (state: State<Data, Params>) => Data;
45
+ shouldStop?: (data: Data, params: Params) => boolean;
46
+ defaultParams?: Params;
47
+ };
48
+ type SimulationProps<Data, Params = Record<string, never>> = SimulationPropsLocal<Data, Params> | SimulationPropsWorker<Data, Params> | SimulationPropsInline<Data, Params>;
49
+ declare function Simulation<Data, Params = Record<string, never>>(props: SimulationProps<Data, Params>): react_jsx_runtime.JSX.Element;
31
50
 
32
51
  export { Simulation, type SimulationProps };