@milehimikey/em 1.7.2 → 1.8.1

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 (119) hide show
  1. package/.claude/skills/event-modeling/SKILL.md +92 -65
  2. package/.claude/skills/event-modeling/reference/conform.md +45 -20
  3. package/.claude/skills/event-modeling/reference/em-dsl.md +132 -55
  4. package/.claude/skills/event-modeling/reference/extract.md +5 -4
  5. package/.claude/skills/event-modeling/reference/implement.md +34 -22
  6. package/.claude/skills/event-modeling/templates/model-readme.md +8 -5
  7. package/.claude/skills/event-modeling/templates/slice.md +19 -5
  8. package/README.md +6 -4
  9. package/dist/catalog/docJoin.d.ts +28 -3
  10. package/dist/catalog/docJoin.js +82 -13
  11. package/dist/catalog/docJoin.js.map +1 -1
  12. package/dist/catalog/docModelConsistencyValidate.d.ts +8 -0
  13. package/dist/catalog/docModelConsistencyValidate.js +335 -0
  14. package/dist/catalog/docModelConsistencyValidate.js.map +1 -0
  15. package/dist/catalog/noteBindingValidate.d.ts +8 -0
  16. package/dist/catalog/noteBindingValidate.js +133 -0
  17. package/dist/catalog/noteBindingValidate.js.map +1 -0
  18. package/dist/catalog/pages.js +3 -3
  19. package/dist/catalog/pages.js.map +1 -1
  20. package/dist/catalog/sliceDoc.d.ts +14 -0
  21. package/dist/catalog/sliceDoc.js +22 -0
  22. package/dist/catalog/sliceDoc.js.map +1 -1
  23. package/dist/catalog/sliceReadyValidate.d.ts +18 -0
  24. package/dist/catalog/sliceReadyValidate.js +25 -1
  25. package/dist/catalog/sliceReadyValidate.js.map +1 -1
  26. package/dist/cli/agentsMd.d.ts +15 -0
  27. package/dist/cli/agentsMd.js +72 -0
  28. package/dist/cli/agentsMd.js.map +1 -0
  29. package/dist/cli/conformScope.d.ts +86 -0
  30. package/dist/cli/conformScope.js +156 -0
  31. package/dist/cli/conformScope.js.map +1 -0
  32. package/dist/cli/contract.d.ts +7 -0
  33. package/dist/cli/contract.js +27 -0
  34. package/dist/cli/contract.js.map +1 -0
  35. package/dist/cli/coverage.d.ts +71 -0
  36. package/dist/cli/coverage.js +177 -0
  37. package/dist/cli/coverage.js.map +1 -0
  38. package/dist/cli/markImplemented.d.ts +34 -0
  39. package/dist/cli/markImplemented.js +180 -0
  40. package/dist/cli/markImplemented.js.map +1 -0
  41. package/dist/cli/migrateReactionShape.d.ts +62 -0
  42. package/dist/cli/migrateReactionShape.js +283 -0
  43. package/dist/cli/migrateReactionShape.js.map +1 -0
  44. package/dist/cli/sliceIndex.d.ts +49 -0
  45. package/dist/cli/sliceIndex.js +115 -0
  46. package/dist/cli/sliceIndex.js.map +1 -0
  47. package/dist/cli/sliceNew.d.ts +18 -0
  48. package/dist/cli/sliceNew.js +53 -0
  49. package/dist/cli/sliceNew.js.map +1 -0
  50. package/dist/cli/stateFile.d.ts +74 -0
  51. package/dist/cli/stateFile.js +191 -0
  52. package/dist/cli/stateFile.js.map +1 -0
  53. package/dist/cli.js +535 -43
  54. package/dist/cli.js.map +1 -1
  55. package/dist/emit/coverageJson.d.ts +8 -0
  56. package/dist/emit/coverageJson.js +30 -0
  57. package/dist/emit/coverageJson.js.map +1 -0
  58. package/dist/emit/diffJson.d.ts +9 -1
  59. package/dist/emit/diffJson.js +44 -1
  60. package/dist/emit/diffJson.js.map +1 -1
  61. package/dist/emit/dot.js +41 -10
  62. package/dist/emit/dot.js.map +1 -1
  63. package/dist/emit/json.d.ts +77 -4
  64. package/dist/emit/json.js +78 -4
  65. package/dist/emit/json.js.map +1 -1
  66. package/dist/emit/validateJson.d.ts +52 -0
  67. package/dist/emit/validateJson.js +120 -0
  68. package/dist/emit/validateJson.js.map +1 -0
  69. package/dist/mcp/main.d.ts +2 -0
  70. package/dist/mcp/main.js +20 -0
  71. package/dist/mcp/main.js.map +1 -0
  72. package/dist/mcp/server.d.ts +11 -0
  73. package/dist/mcp/server.js +260 -0
  74. package/dist/mcp/server.js.map +1 -0
  75. package/dist/model/model.d.ts +30 -1
  76. package/dist/model/model.js +26 -0
  77. package/dist/model/model.js.map +1 -1
  78. package/dist/model/rules.d.ts +114 -0
  79. package/dist/model/rules.js +116 -4
  80. package/dist/model/rules.js.map +1 -1
  81. package/dist/model/validate.js +92 -8
  82. package/dist/model/validate.js.map +1 -1
  83. package/dist/parser/ast.d.ts +47 -0
  84. package/dist/parser/lexer.d.ts +16 -0
  85. package/dist/parser/lexer.js +44 -2
  86. package/dist/parser/lexer.js.map +1 -1
  87. package/dist/parser/parser.js +354 -20
  88. package/dist/parser/parser.js.map +1 -1
  89. package/dist/render/drawNotes.d.ts +11 -1
  90. package/dist/render/drawNotes.js +28 -3
  91. package/dist/render/drawNotes.js.map +1 -1
  92. package/dist/render/render.js +116 -23
  93. package/dist/render/render.js.map +1 -1
  94. package/dist/render/serve.d.ts +4 -0
  95. package/dist/render/serve.js +27 -239
  96. package/dist/render/serve.js.map +1 -1
  97. package/dist/render/sliceOverlay.d.ts +7 -2
  98. package/dist/render/sliceOverlay.js +19 -7
  99. package/dist/render/sliceOverlay.js.map +1 -1
  100. package/dist/render/sliceStatus.d.ts +7 -3
  101. package/dist/render/sliceStatus.js +48 -12
  102. package/dist/render/sliceStatus.js.map +1 -1
  103. package/dist/render/viewerHtml.d.ts +1 -0
  104. package/dist/render/viewerHtml.js +5 -0
  105. package/dist/render/viewerHtml.js.map +1 -0
  106. package/dist/render/watch.d.ts +10 -0
  107. package/dist/render/watch.js +29 -0
  108. package/dist/render/watch.js.map +1 -1
  109. package/dist/templates.d.ts +20 -1
  110. package/dist/templates.js +172 -3
  111. package/dist/templates.js.map +1 -1
  112. package/dist/util/markers.d.ts +13 -0
  113. package/dist/util/markers.js +30 -0
  114. package/dist/util/markers.js.map +1 -0
  115. package/dist/util/walkDir.d.ts +10 -1
  116. package/dist/util/walkDir.js +11 -7
  117. package/dist/util/walkDir.js.map +1 -1
  118. package/package.json +12 -3
  119. package/.claude/skills/event-modeling/templates/live.html +0 -117
@@ -0,0 +1,30 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Shared marker-delimited region patcher: `<!-- GENERATED:<name>:start -->...
3
+ // <!-- GENERATED:<name>:end -->`. Originally scripts/generate-skill-docs.ts's own local helper
4
+ // (MIL-92, docs-generation only); extracted here so a real runtime command — `em slice index`
5
+ // (MIL-98) — can reuse the exact same marker convention instead of re-deriving its own regex.
6
+ // Pure string in, string (or null) out — no fs access, so both a dev script and a CLI command
7
+ // can layer their own file-reading and error reporting on top.
8
+ /** Captures the exact start/end marker lines separately from whatever sits between them, so
9
+ * replacement never depends on how much whitespace happened to separate them before — matters
10
+ * for a freshly-added, still-empty marker pair (start line immediately followed by end line,
11
+ * nothing in between to non-greedily match a leading "\n" against). */
12
+ export function markerRegex(name) {
13
+ const start = `<!-- GENERATED:${name}:start[^\\n]*-->`;
14
+ const end = `<!-- GENERATED:${name}:end -->`;
15
+ return new RegExp(`(${start})([\\s\\S]*?)(${end})`);
16
+ }
17
+ /**
18
+ * Replace the body between the named marker pair in `content` with `newBody`, preserving the
19
+ * marker lines themselves verbatim. Returns `null` (never throws) when the marker pair isn't
20
+ * found in `content` — callers decide how to report that: a dev-tooling throw
21
+ * (scripts/generate-skill-docs.ts), or a user-facing CLI error suggesting how to add the
22
+ * markers (`em slice index`, src/cli/sliceIndex.ts).
23
+ */
24
+ export function applyMarker(content, markerName, newBody) {
25
+ const re = markerRegex(markerName);
26
+ if (!re.test(content))
27
+ return null;
28
+ return content.replace(re, (_m, open, _old, close) => `${open}\n${newBody}\n${close}`);
29
+ }
30
+ //# sourceMappingURL=markers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markers.js","sourceRoot":"","sources":["../../src/util/markers.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,8EAA8E;AAC9E,+FAA+F;AAC/F,8FAA8F;AAC9F,8FAA8F;AAC9F,8FAA8F;AAC9F,+DAA+D;AAE/D;;;wEAGwE;AACxE,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,MAAM,KAAK,GAAG,kBAAkB,IAAI,kBAAkB,CAAC;IACvD,MAAM,GAAG,GAAG,kBAAkB,IAAI,UAAU,CAAC;IAC7C,OAAO,IAAI,MAAM,CAAC,IAAI,KAAK,iBAAiB,GAAG,GAAG,CAAC,CAAC;AACtD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,OAAe,EAAE,UAAkB,EAAE,OAAe;IAC9E,MAAM,EAAE,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC;IACnC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,IAAI,KAAK,OAAO,KAAK,KAAK,EAAE,CAAC,CAAC;AACzF,CAAC"}
@@ -1,4 +1,13 @@
1
+ export interface WalkDirOptions {
2
+ /** When given, called with each directory entry's bare name before descending into it; a
3
+ * `true` return prunes the whole subtree (never read via `readdirSync`, never descended) —
4
+ * cheaper and safer than walking everything and filtering the flat result list afterward
5
+ * (skips permission errors / huge trees under a pruned dir too, e.g. `node_modules`). Omitted
6
+ * entirely by every existing caller (skillSync.ts/skillCheck.ts), which still walk the whole
7
+ * tree with no behavior change. */
8
+ skipDir?: (name: string) => boolean;
9
+ }
1
10
  /** Every regular file under `dir`, recursively, as paths relative to `dir` — sorted, POSIX
2
11
  * forward slashes regardless of platform (so hashes/change-reports are stable and testable
3
12
  * across OSes). Symlinks and other non-file/non-directory entries are skipped. */
4
- export declare function walkDir(dir: string): string[];
13
+ export declare function walkDir(dir: string, opts?: WalkDirOptions): string[];
@@ -2,7 +2,7 @@
2
2
  // Recursive file-tree listing shared by `em skill sync`/`em skill check` (src/cli/skillSync.ts,
3
3
  // src/cli/skillCheck.ts) to enumerate every file in the bundled skill directory — not just the
4
4
  // `.md` subset test/skillExamples.test.ts's skillFiles() walks for doctesting, which deliberately
5
- // skips non-`.md` files like templates/live.html. Sync/check must cover the whole tree.
5
+ // skips any non-`.md` file the skill may bundle. Sync/check must cover the whole tree.
6
6
  //
7
7
  // Manual walk rather than `readdirSync(dir, { recursive: true })` — that overload is Node
8
8
  // 20.1+ only; package.json declares `"engines": { "node": ">=18" }`.
@@ -11,15 +11,19 @@ import { join, relative, sep } from "node:path";
11
11
  /** Every regular file under `dir`, recursively, as paths relative to `dir` — sorted, POSIX
12
12
  * forward slashes regardless of platform (so hashes/change-reports are stable and testable
13
13
  * across OSes). Symlinks and other non-file/non-directory entries are skipped. */
14
- export function walkDir(dir) {
14
+ export function walkDir(dir, opts) {
15
15
  const out = [];
16
+ const skipDir = opts?.skipDir;
16
17
  function recurse(current) {
17
18
  for (const entry of readdirSync(current, { withFileTypes: true })) {
18
- const full = join(current, entry.name);
19
- if (entry.isDirectory())
20
- recurse(full);
21
- else if (entry.isFile())
22
- out.push(relative(dir, full).split(sep).join("/"));
19
+ if (entry.isDirectory()) {
20
+ if (skipDir?.(entry.name))
21
+ continue;
22
+ recurse(join(current, entry.name));
23
+ }
24
+ else if (entry.isFile()) {
25
+ out.push(relative(dir, join(current, entry.name)).split(sep).join("/"));
26
+ }
23
27
  }
24
28
  }
25
29
  recurse(dir);
@@ -1 +1 @@
1
- {"version":3,"file":"walkDir.js","sourceRoot":"","sources":["../../src/util/walkDir.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,gGAAgG;AAChG,+FAA+F;AAC/F,kGAAkG;AAClG,wFAAwF;AACxF,EAAE;AACF,0FAA0F;AAC1F,qEAAqE;AAErE,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACtC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAEhD;;mFAEmF;AACnF,MAAM,UAAU,OAAO,CAAC,GAAW;IACjC,MAAM,GAAG,GAAa,EAAE,CAAC;IAEzB,SAAS,OAAO,CAAC,OAAe;QAC9B,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,OAAO,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;YAClE,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YACvC,IAAI,KAAK,CAAC,WAAW,EAAE;gBAAE,OAAO,CAAC,IAAI,CAAC,CAAC;iBAClC,IAAI,KAAK,CAAC,MAAM,EAAE;gBAAE,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QAC9E,CAAC;IACH,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,CAAC;IACb,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC;AACpB,CAAC"}
1
+ {"version":3,"file":"walkDir.js","sourceRoot":"","sources":["../../src/util/walkDir.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,gGAAgG;AAChG,+FAA+F;AAC/F,kGAAkG;AAClG,uFAAuF;AACvF,EAAE;AACF,0FAA0F;AAC1F,qEAAqE;AAErE,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACtC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAYhD;;mFAEmF;AACnF,MAAM,UAAU,OAAO,CAAC,GAAW,EAAE,IAAqB;IACxD,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,MAAM,OAAO,GAAG,IAAI,EAAE,OAAO,CAAC;IAE9B,SAAS,OAAO,CAAC,OAAe;QAC9B,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,OAAO,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;YAClE,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;gBACxB,IAAI,OAAO,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC;oBAAE,SAAS;gBACpC,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;YACrC,CAAC;iBAAM,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;gBAC1B,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;YAC1E,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,CAAC;IACb,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC;AACpB,CAAC"}
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@milehimikey/em",
3
- "version": "1.7.2",
3
+ "version": "1.8.1",
4
4
  "description": "AI-friendly Event Modeling — a slice-first text DSL rendered to a strict, self-contained Graphviz grid (SVG/PNG)",
5
5
  "type": "module",
6
6
  "bin": {
7
- "em": "./dist/cli.js"
7
+ "em": "./dist/cli.js",
8
+ "em-mcp": "./dist/mcp/main.js"
8
9
  },
9
10
  "main": "./dist/cli.js",
10
11
  "files": [
@@ -21,6 +22,8 @@
21
22
  "test": "vitest run",
22
23
  "test:watch": "vitest",
23
24
  "docs:generate": "tsx scripts/generate-skill-docs.ts",
25
+ "viewer:generate": "tsx scripts/generate-viewer-html.ts",
26
+ "viewer:check": "tsx scripts/generate-viewer-html.ts --check",
24
27
  "docs:check": "tsx scripts/generate-skill-docs.ts --check",
25
28
  "check:skill-version": "tsx scripts/check-skill-version-stamp.ts",
26
29
  "prepublishOnly": "npm run build && npm run typecheck && npm test"
@@ -53,13 +56,19 @@
53
56
  },
54
57
  "dependencies": {
55
58
  "@hpcc-js/wasm-graphviz": "^1.22.0",
59
+ "@modelcontextprotocol/sdk": "^1.30.0",
56
60
  "@resvg/resvg-js": "^2.6.2",
57
61
  "chokidar": "^3.6.0",
58
62
  "commander": "^12.1.0",
59
- "marked": "^18.0.0"
63
+ "marked": "^18.0.0",
64
+ "pdfkit": "^0.19.1",
65
+ "svg-to-pdfkit": "^0.1.8",
66
+ "zod": "^4.4.3"
60
67
  },
61
68
  "devDependencies": {
62
69
  "@types/node": "^22.0.0",
70
+ "@types/pdfkit": "^0.17.6",
71
+ "jsdom": "^26.1.0",
63
72
  "tsx": "^4.19.0",
64
73
  "typescript": "^5.6.0",
65
74
  "vitest": "^4.1.10"
@@ -1,117 +0,0 @@
1
- <!doctype html>
2
- <!--
3
- Live viewer for an em event model. Auto-reloads the rendered SVG so a team on a shared
4
- screen sees updates from `em watch` without manual refresh.
5
-
6
- Two ways to use it:
7
-
8
- A) Push (recommended, instant, no polling):
9
- em watch MODEL.em -o MODEL.svg --serve
10
- then open the URL it prints (http://localhost:5173/?svg=MODEL.svg). You don't need
11
- this file at all in that mode — the server serves its own push-based viewer.
12
-
13
- B) This file, over file:// (no server):
14
- 1) In one terminal: em watch MODEL.em -o MODEL.svg
15
- 2) Open this file with the SVG name in the query:
16
- open "live.html?svg=MODEL.svg" (macOS)
17
- No editing required — the ?svg= param picks the model.
18
-
19
- Under file:// the browser gives no reliable "did the file change?" signal (fetch/HEAD is
20
- blocked), so this fallback polls and reloads on a gentle interval. It double-buffers the
21
- swap so a shared screen never flashes. For zero idle work and instant updates, use --serve.
22
-
23
- The SVG is embedded via <object> so note "..." links inside it stay clickable.
24
- -->
25
- <html lang="en">
26
- <head>
27
- <meta charset="utf-8" />
28
- <title>Event Model — Live</title>
29
- <style>
30
- :root { color-scheme: light dark; }
31
- body { margin: 0; font-family: system-ui, sans-serif; background: #fafafa; }
32
- header {
33
- display: flex; align-items: center; gap: .75rem;
34
- padding: .5rem .9rem; background: #1f2933; color: #fff; font-size: 14px;
35
- }
36
- header .dot { width: 9px; height: 9px; border-radius: 50%; background: #34c759; }
37
- header .stamp { margin-left: auto; opacity: .7; font-variant-numeric: tabular-nums; }
38
- #stage { padding: 1rem; }
39
- object { width: 100%; height: calc(100vh - 60px); border: 0; }
40
- </style>
41
- </head>
42
- <body>
43
- <header>
44
- <span class="dot"></span>
45
- <span>Event Model — live (auto-refresh)</span>
46
- <span class="stamp" id="stamp">—</span>
47
- </header>
48
- <div id="stage">
49
- <object id="svg" type="image/svg+xml"></object>
50
- </div>
51
- <script>
52
- // SVG filename from ?svg=<name>.svg (defaults to model.svg) — one file serves any model.
53
- const SVG_FILE = new URLSearchParams(location.search).get("svg") || "model.svg";
54
- // Poll cadence for the file:// fallback. Tune freely; --serve makes polling unnecessary.
55
- const REFRESH_INTERVAL_MS = 2000;
56
-
57
- const stage = document.getElementById("stage");
58
- const stamp = document.getElementById("stamp");
59
- let current = document.getElementById("svg");
60
- let lastModified = null;
61
-
62
- // Best-effort change check — only meaningful when served over http(s). Under file://
63
- // fetch/HEAD is blocked, so this throws and we fall back to reloading every tick.
64
- async function changed() {
65
- if (!location.protocol.startsWith("http")) return true;
66
- try {
67
- const res = await fetch(SVG_FILE, { method: "HEAD", cache: "no-store" });
68
- const tag = res.headers.get("last-modified") || res.headers.get("etag");
69
- if (tag && tag === lastModified) return false;
70
- lastModified = tag;
71
- return true;
72
- } catch {
73
- return true; // can't tell — reload to be safe
74
- }
75
- }
76
-
77
- // Double-buffer: load into a hidden <object>, swap on load, so the shared screen
78
- // never flashes white during a reload. The buffer must be attached to the document
79
- // BEFORE data is set — a detached <object> never starts fetching its resource, so
80
- // its load event would never fire and the swap would never happen.
81
- let pending = null; // the in-flight buffer <object>, if a swap is loading
82
- function swap() {
83
- if (pending) pending.remove(); // superseded by this newer swap
84
- const next = document.createElement("object");
85
- next.type = "image/svg+xml";
86
- next.style.cssText = current.style.cssText;
87
- next.style.position = "absolute"; // out of flow + invisible while loading
88
- next.style.visibility = "hidden";
89
- next.addEventListener("load", () => {
90
- if (pending !== next) return; // a newer swap already replaced this one
91
- pending = null;
92
- next.style.position = "";
93
- next.style.visibility = "";
94
- stage.removeChild(current);
95
- current = next;
96
- stamp.textContent = new Date().toLocaleTimeString();
97
- }, { once: true });
98
- stage.appendChild(next);
99
- pending = next;
100
- next.setAttribute("data", SVG_FILE + "?t=" + Date.now());
101
- }
102
-
103
- // Self-scheduling loop: schedule the next tick only after this one resolves, so a slow
104
- // reload can never overlap the next tick.
105
- async function tick() {
106
- try {
107
- if (await changed()) swap();
108
- } finally {
109
- setTimeout(tick, REFRESH_INTERVAL_MS);
110
- }
111
- }
112
-
113
- swap();
114
- setTimeout(tick, REFRESH_INTERVAL_MS);
115
- </script>
116
- </body>
117
- </html>