@typeonce/ritmo-devtools 0.6.0 → 0.8.0

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 (72) hide show
  1. package/README.md +105 -21
  2. package/dist/Determinism.d.ts +70 -0
  3. package/dist/Determinism.d.ts.map +1 -0
  4. package/dist/Determinism.js +137 -0
  5. package/dist/Determinism.js.map +1 -0
  6. package/dist/Format.d.ts +8 -1
  7. package/dist/Format.d.ts.map +1 -1
  8. package/dist/Format.js +47 -5
  9. package/dist/Format.js.map +1 -1
  10. package/dist/Invariant.d.ts +34 -12
  11. package/dist/Invariant.d.ts.map +1 -1
  12. package/dist/Invariant.js +6 -19
  13. package/dist/Invariant.js.map +1 -1
  14. package/dist/Lint.d.ts +60 -0
  15. package/dist/Lint.d.ts.map +1 -0
  16. package/dist/Lint.js +15 -0
  17. package/dist/Lint.js.map +1 -0
  18. package/dist/Monitor.d.ts +18 -4
  19. package/dist/Monitor.d.ts.map +1 -1
  20. package/dist/Monitor.js +32 -21
  21. package/dist/Monitor.js.map +1 -1
  22. package/dist/Replay.d.ts +271 -0
  23. package/dist/Replay.d.ts.map +1 -0
  24. package/dist/Replay.js +335 -0
  25. package/dist/Replay.js.map +1 -0
  26. package/dist/ReplayNodeSink.d.ts +11 -0
  27. package/dist/ReplayNodeSink.d.ts.map +1 -0
  28. package/dist/ReplayNodeSink.js +29 -0
  29. package/dist/ReplayNodeSink.js.map +1 -0
  30. package/dist/ReplayVitePlugin.d.ts +41 -0
  31. package/dist/ReplayVitePlugin.d.ts.map +1 -0
  32. package/dist/ReplayVitePlugin.js +41 -0
  33. package/dist/ReplayVitePlugin.js.map +1 -0
  34. package/dist/ReplayViteSink.d.ts +30 -0
  35. package/dist/ReplayViteSink.d.ts.map +1 -0
  36. package/dist/ReplayViteSink.js +9 -0
  37. package/dist/ReplayViteSink.js.map +1 -0
  38. package/dist/Session.d.ts +66 -14
  39. package/dist/Session.d.ts.map +1 -1
  40. package/dist/Session.js +93 -7
  41. package/dist/Session.js.map +1 -1
  42. package/dist/cli.d.ts +3 -0
  43. package/dist/cli.d.ts.map +1 -0
  44. package/dist/cli.js +57 -0
  45. package/dist/cli.js.map +1 -0
  46. package/dist/index.d.ts +3 -0
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +3 -0
  49. package/dist/index.js.map +1 -1
  50. package/dist/internal/monitor.d.ts +4 -2
  51. package/dist/internal/monitor.d.ts.map +1 -1
  52. package/dist/internal/monitor.js +9 -3
  53. package/dist/internal/monitor.js.map +1 -1
  54. package/dist/internal/version.d.ts +7 -0
  55. package/dist/internal/version.d.ts.map +1 -0
  56. package/dist/internal/version.js +7 -0
  57. package/dist/internal/version.js.map +1 -0
  58. package/package.json +5 -2
  59. package/src/Determinism.ts +190 -0
  60. package/src/Format.ts +50 -4
  61. package/src/Invariant.ts +38 -12
  62. package/src/Lint.ts +72 -0
  63. package/src/Monitor.ts +49 -26
  64. package/src/Replay.ts +583 -0
  65. package/src/ReplayNodeSink.ts +38 -0
  66. package/src/ReplayVitePlugin.ts +74 -0
  67. package/src/ReplayViteSink.ts +33 -0
  68. package/src/Session.ts +154 -24
  69. package/src/cli.ts +55 -0
  70. package/src/index.ts +3 -0
  71. package/src/internal/monitor.ts +12 -5
  72. package/src/internal/version.ts +6 -0
package/src/Monitor.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  *
15
15
  * The monitor observes the runtime only while it is open, so a closed monitor
16
16
  * costs nothing. While open it reduces trace events to counts and shows its
17
- * own cost per tick. It needs a runtime made with `debug: true`.
17
+ * own cost per tick. It needs a runtime made with `debug: { hostClock }`, and the page passed as `host`.
18
18
  *
19
19
  * @module Monitor
20
20
  * @docGroup devtools
@@ -22,6 +22,7 @@
22
22
  * @example
23
23
  * ```ts
24
24
  * const monitor = Monitor.attach(runtime, {
25
+ * host: window,
25
26
  * schedules: { fixedUpdate: step, frame },
26
27
  * pick: (event) => entityUnderPointer(event),
27
28
  * locate: (entity) => screenPositionOf(entity)
@@ -30,7 +31,7 @@
30
31
  * ```
31
32
  */
32
33
  import type * as Debug from "@typeonce/ritmo/Debug"
33
- import type { ExecutableScheduleDefinition } from "@typeonce/ritmo/Schedule"
34
+ import type { AnyScheduleOrder, ExecutableScheduleDefinition } from "@typeonce/ritmo/Schedule"
34
35
  import type { Schema } from "@typeonce/ritmo/Schema"
35
36
 
36
37
  import * as Format from "./Format.ts"
@@ -57,8 +58,14 @@ export interface Options<S extends Schema.Any, Root> {
57
58
  * panel and make up the system map. Name the schedule objects actually
58
59
  * passed to `tick`: nested schedules are flattened into them.
59
60
  */
60
- readonly schedules: Readonly<Record<string, ExecutableScheduleDefinition<S, any, Root, any, any>>>
61
- /** Where the panel is added. Defaults to `document.body`. */
61
+ readonly schedules: Readonly<Record<string, ExecutableScheduleDefinition<S, any, Root, any, any, AnyScheduleOrder>>>
62
+ /**
63
+ * The page the panel lives in, passed explicitly (`window`): its
64
+ * `document`, timers, animation frames, keyboard events, and
65
+ * `performance` clock. The monitor reads no globals.
66
+ */
67
+ readonly host: Window
68
+ /** Where the panel is added. Defaults to `host.document.body`. */
62
69
  readonly mount?: HTMLElement
63
70
  /** `KeyboardEvent.code` that toggles the panel. Defaults to `Backslash`. */
64
71
  readonly key?: string
@@ -70,6 +77,11 @@ export interface Options<S extends Schema.Any, Root> {
70
77
  readonly locate?: (entity: number) => { readonly x: number; readonly y: number } | undefined
71
78
  /** Milliseconds between panel refreshes. Defaults to 250. */
72
79
  readonly refreshMs?: number
80
+ /**
81
+ * A replay recorder (`Replay.recorder`): adds a "Save replay" button that
82
+ * saves the last seconds of play with the reason "Save replay button".
83
+ */
84
+ readonly replay?: { readonly save: (reason: string) => unknown }
73
85
  }
74
86
 
75
87
  export interface Monitor {
@@ -154,7 +166,7 @@ const accessOf = (system: Debug.SystemDescription): Access => {
154
166
  const unique = (values: ReadonlyArray<string>) => [...new Set(values)]
155
167
  const writes = unique(system.queries.flatMap((query) => query.writes))
156
168
  return {
157
- reads: unique(system.queries.flatMap((query) => [...query.reads, ...query.optional, ...query.added, ...query.changed])).filter((name) => !writes.includes(name)),
169
+ reads: unique(system.queries.flatMap((query) => [...query.reads, ...query.optional, ...query.added, ...query.changed, ...query.anyOf.flatMap((members) => [...members.added, ...members.changed])])).filter((name) => !writes.includes(name)),
158
170
  writes,
159
171
  resourceReads: system.resources.reads.filter((name) => !system.resources.writes.includes(name)),
160
172
  resourceWrites: system.resources.writes,
@@ -243,18 +255,19 @@ const STYLE = `
243
255
  export const attach = <S extends Schema.Any, Root>(runtime: Target<S, Root>, options: Options<S, Root>): Monitor => {
244
256
  const debug = runtime.debug
245
257
  debug.nameSchedules(options.schedules)
246
- const mount = options.mount ?? document.body
258
+ const host = options.host
259
+ const mount = options.mount ?? host.document.body
247
260
  const key = options.key ?? "Backslash"
248
261
  const refreshMs = options.refreshMs ?? 250
249
262
 
250
- if (document.getElementById("btm-style") === null) {
251
- const style = document.createElement("style")
263
+ if (host.document.getElementById("btm-style") === null) {
264
+ const style = host.document.createElement("style")
252
265
  style.id = "btm-style"
253
266
  style.textContent = STYLE
254
- document.head.append(style)
267
+ host.document.head.append(style)
255
268
  }
256
269
 
257
- const root = document.createElement("div")
270
+ const root = host.document.createElement("div")
258
271
  root.className = "btm"
259
272
  root.innerHTML = `
260
273
  <button class="btm-toggle" type="button" title="Toggle the Ritmo monitor">◉ monitor</button>
@@ -264,6 +277,7 @@ export const attach = <S extends Schema.Any, Root>(runtime: Target<S, Root>, opt
264
277
  <button class="btm-tab" data-tab="overview" type="button">Overview</button>
265
278
  <button class="btm-tab" data-tab="systems" type="button">Systems</button>
266
279
  <button class="btm-tab" data-tab="entity" type="button">Entity</button>
280
+ ${options.replay === undefined ? "" : `<button class="btm-save-replay" type="button" title="Save the last seconds of play as a replay">Save replay</button>`}
267
281
  <button class="btm-close" type="button" title="Close">×</button>
268
282
  </div>
269
283
  <div class="btm-status"></div>
@@ -294,7 +308,7 @@ export const attach = <S extends Schema.Any, Root>(runtime: Target<S, Root>, opt
294
308
  let tab: Tab = "overview"
295
309
  let collector: Collector | undefined
296
310
  let stopObserving: (() => void) | undefined
297
- let timer: ReturnType<typeof setInterval> | undefined
311
+ let timer: number | undefined
298
312
  let openedAt = 0
299
313
  let description: Debug.Description | undefined
300
314
  /** How many systems read each component, to name what systems spawn. */
@@ -515,20 +529,20 @@ export const attach = <S extends Schema.Any, Root>(runtime: Target<S, Root>, opt
515
529
 
516
530
  const render = () => {
517
531
  if (collector === undefined) return
518
- const started = performance.now()
532
+ const started = host.performance.now()
519
533
  const history = collector.history
520
534
  const latest = history.slice(-RECENT)
521
535
  const overhead = latest.reduce((total, sample) => total + sample.overheadMs, 0)
522
536
  const ticks = latest.reduce((total, sample) => total + [...sample.schedules.values()].reduce((sum, window) => sum + window.ticks, 0), 0)
523
- status.textContent = `observing for ${Math.round((performance.now() - openedAt) / 1000)}s · frame ${debug.frame()} · panel ${ticks === 0 ? "–" : number(overhead / ticks, 3)} ms/tick + ${number(renderMs, 1)} ms/refresh`
537
+ status.textContent = `observing for ${Math.round((host.performance.now() - openedAt) / 1000)}s · frame ${debug.frame()} · panel ${ticks === 0 ? "–" : number(overhead / ticks, 3)} ms/tick + ${number(renderMs, 1)} ms/refresh`
524
538
  status.title = "The panel's own work. Tracing (on only while the panel is open) also slows every system run a little, and the times shown include it: compare them with each other, not with a closed-monitor run."
525
539
  body.innerHTML = tab === "overview" ? renderOverview(history) : tab === "systems" ? renderSystems(history) : renderEntity()
526
- renderMs = performance.now() - started
540
+ renderMs = host.performance.now() - started
527
541
  }
528
542
 
529
543
  const refresh = () => {
530
544
  if (collector === undefined) return
531
- collector.sample(performance.now())
545
+ collector.sample(host.performance.now())
532
546
  render()
533
547
  }
534
548
 
@@ -539,7 +553,7 @@ export const attach = <S extends Schema.Any, Root>(runtime: Target<S, Root>, opt
539
553
  ring.style.left = `${at.x}px`
540
554
  ring.style.top = `${at.y}px`
541
555
  }
542
- if (!panel.hidden) requestAnimationFrame(placeRing)
556
+ if (!panel.hidden) host.requestAnimationFrame(placeRing)
543
557
  }
544
558
 
545
559
  const setTab = (next: Tab) => {
@@ -555,14 +569,14 @@ export const attach = <S extends Schema.Any, Root>(runtime: Target<S, Root>, opt
555
569
  readers = new Map(description.access.components.map((entry) => [entry.name, entry.readers.length]))
556
570
  prefix = commonPrefix([...description.systems.map((system) => system.name), ...description.components.map((component) => component.name)])
557
571
  browse.replaceChildren(new Option("browse by component…", ""), ...description.components.map((component) => new Option(short(component.name), component.name)))
558
- collector = new Collector(() => debug.population(), performance.now())
572
+ collector = new Collector(() => debug.population(), () => host.performance.now())
559
573
  collector.select(selected)
560
574
  stopObserving = debug.observe(collector.observe)
561
- openedAt = performance.now()
575
+ openedAt = host.performance.now()
562
576
  panel.hidden = false
563
- timer = setInterval(refresh, refreshMs)
577
+ timer = host.setInterval(refresh, refreshMs)
564
578
  setTab(tab)
565
- requestAnimationFrame(placeRing)
579
+ host.requestAnimationFrame(placeRing)
566
580
  }
567
581
 
568
582
  const close = () => {
@@ -571,7 +585,7 @@ export const attach = <S extends Schema.Any, Root>(runtime: Target<S, Root>, opt
571
585
  ring.hidden = true
572
586
  stopObserving?.()
573
587
  stopObserving = undefined
574
- if (timer !== undefined) clearInterval(timer)
588
+ if (timer !== undefined) host.clearInterval(timer)
575
589
  timer = undefined
576
590
  collector = undefined
577
591
  setPicking(false)
@@ -603,9 +617,9 @@ export const attach = <S extends Schema.Any, Root>(runtime: Target<S, Root>, opt
603
617
  picking = on
604
618
  pickButton.classList.toggle("is-on", on)
605
619
  pickButton.textContent = on ? "Click an entity… (Esc)" : "Pick on screen"
606
- document.documentElement.classList.toggle("btm-picking", on)
607
- if (on) window.addEventListener("pointerdown", onPick, { capture: true })
608
- else window.removeEventListener("pointerdown", onPick, { capture: true })
620
+ host.document.documentElement.classList.toggle("btm-picking", on)
621
+ if (on) host.addEventListener("pointerdown", onPick, { capture: true })
622
+ else host.removeEventListener("pointerdown", onPick, { capture: true })
609
623
  }
610
624
 
611
625
  // ── Events ─────────────────────────────────────────────────────────────
@@ -620,9 +634,18 @@ export const attach = <S extends Schema.Any, Root>(runtime: Target<S, Root>, opt
620
634
  setPicking(false)
621
635
  }
622
636
  }
623
- window.addEventListener("keydown", onKey)
637
+ host.addEventListener("keydown", onKey)
624
638
  toggleButton.addEventListener("click", () => (panel.hidden ? open() : close()))
625
639
  query<HTMLButtonElement>(".btm-close").addEventListener("click", close)
640
+ const replay = options.replay
641
+ if (replay !== undefined) {
642
+ const button = query<HTMLButtonElement>(".btm-save-replay")
643
+ button.addEventListener("click", () => {
644
+ replay.save("Save replay button")
645
+ button.textContent = "Saved"
646
+ host.setTimeout(() => { button.textContent = "Save replay" }, 1500)
647
+ })
648
+ }
626
649
  for (const button of root.querySelectorAll<HTMLButtonElement>(".btm-tab")) {
627
650
  button.addEventListener("click", () => setTab(button.dataset["tab"] as Tab))
628
651
  }
@@ -681,7 +704,7 @@ export const attach = <S extends Schema.Any, Root>(runtime: Target<S, Root>, opt
681
704
  select,
682
705
  detach: () => {
683
706
  close()
684
- window.removeEventListener("keydown", onKey)
707
+ host.removeEventListener("keydown", onKey)
685
708
  root.remove()
686
709
  }
687
710
  }