versioncam 0.5.3 → 0.7.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 (54) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +82 -2
  3. package/dist/cli/commands/check.js +25 -0
  4. package/dist/cli/commands/check.js.map +1 -1
  5. package/dist/cli/commands/doctor.js +54 -1
  6. package/dist/cli/commands/doctor.js.map +1 -1
  7. package/dist/cli/commands/record.d.ts +6 -0
  8. package/dist/cli/commands/record.js +24 -3
  9. package/dist/cli/commands/record.js.map +1 -1
  10. package/dist/cli/commands/stability.js +10 -3
  11. package/dist/cli/commands/stability.js.map +1 -1
  12. package/dist/cli/usage.d.ts +1 -1
  13. package/dist/cli/usage.js +2 -0
  14. package/dist/cli/usage.js.map +1 -1
  15. package/dist/config.d.ts +37 -1
  16. package/dist/config.js +115 -3
  17. package/dist/config.js.map +1 -1
  18. package/dist/core/cursor.d.ts +22 -0
  19. package/dist/core/cursor.js +6 -0
  20. package/dist/core/cursor.js.map +1 -1
  21. package/dist/core/motion-defaults.d.ts +8 -0
  22. package/dist/core/motion-defaults.js +8 -0
  23. package/dist/core/motion-defaults.js.map +1 -1
  24. package/dist/core/timeline.d.ts +24 -0
  25. package/dist/core/timeline.js.map +1 -1
  26. package/dist/driver/clip.d.ts +6 -0
  27. package/dist/driver/clip.js +33 -2
  28. package/dist/driver/clip.js.map +1 -1
  29. package/dist/driver/file-window.d.ts +66 -0
  30. package/dist/driver/file-window.js +245 -0
  31. package/dist/driver/file-window.js.map +1 -0
  32. package/dist/driver/page-hooks.d.ts +23 -1
  33. package/dist/driver/page-hooks.js +36 -1
  34. package/dist/driver/page-hooks.js.map +1 -1
  35. package/dist/driver/replay.d.ts +263 -0
  36. package/dist/driver/replay.js +558 -0
  37. package/dist/driver/replay.js.map +1 -0
  38. package/dist/driver/session.d.ts +70 -4
  39. package/dist/driver/session.js +267 -29
  40. package/dist/driver/session.js.map +1 -1
  41. package/dist/driver/settle.js +6 -0
  42. package/dist/driver/settle.js.map +1 -1
  43. package/dist/index.d.ts +4 -2
  44. package/dist/index.js.map +1 -1
  45. package/dist/page/assets/index-CnRDAiUz.js +2 -0
  46. package/dist/page/index.html +1 -1
  47. package/dist/publish/protocol.d.ts +12 -1
  48. package/dist/publish/protocol.js.map +1 -1
  49. package/dsl.md +68 -1
  50. package/package.json +1 -1
  51. package/plugin/.claude-plugin/plugin.json +1 -1
  52. package/plugin/skills/versioncam/authoring.md +6 -1
  53. package/plugin/skills/versioncam/onboarding.md +9 -3
  54. package/dist/page/assets/index-BtUbUEdN.js +0 -2
@@ -0,0 +1,245 @@
1
+ /**
2
+ * A file window drawn over the page, to drag files from.
3
+ *
4
+ * Uploading is half of what a data app does, and the browser's own file
5
+ * dialog is outside the page: nothing can film it, and a file that simply
6
+ * appears in an upload list reads as a cut. The first app this recorded
7
+ * solved it with a small window drawn in the page, listing the files, from
8
+ * which the cursor drags them onto the app's own dropzone; nothing in it was
9
+ * that app's, so it is here. It looks like a Finder window, because the
10
+ * clips it sits in show a Mac's cursor.
11
+ *
12
+ * It is staged, and a clip that uses it should say so where its clips are
13
+ * shown. What is real is the drop: letting go over the page sends the
14
+ * element under the pointer the `dragenter`, `dragover` and `drop` events a
15
+ * real drag sends, carrying `File` objects with the names, types and
16
+ * contents given, so the app takes them through its own dropzone, its own
17
+ * accept rules and its own list, as it takes a person's.
18
+ *
19
+ * Selection works as in Finder: a click selects one row, a click with Shift
20
+ * extends to it, a click with Meta or Control adds or removes it, and a
21
+ * press on a selected row drags the whole selection. Its motion runs on
22
+ * `requestAnimationFrame`, which the recorder's clock drives, so it opens
23
+ * and closes at the same pace on every run.
24
+ */
25
+ /** The extension's kind, as Finder words it, and its MIME type. */
26
+ const KINDS = {
27
+ csv: { kind: "CSV document", type: "text/csv" },
28
+ tsv: { kind: "TSV document", type: "text/tab-separated-values" },
29
+ json: { kind: "JSON document", type: "application/json" },
30
+ txt: { kind: "Plain Text", type: "text/plain" },
31
+ fasta: { kind: "FASTA document", type: "text/plain" },
32
+ fa: { kind: "FASTA document", type: "text/plain" },
33
+ png: { kind: "PNG image", type: "image/png" },
34
+ jpg: { kind: "JPEG image", type: "image/jpeg" },
35
+ pdf: { kind: "PDF document", type: "application/pdf" },
36
+ xlsx: {
37
+ kind: "Microsoft Excel workbook",
38
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
39
+ },
40
+ };
41
+ export function fileRows(files) {
42
+ if (!Array.isArray(files) || files.length === 0) {
43
+ throw new TypeError("fileWindow: list at least one file, as { name }.");
44
+ }
45
+ return files.map((file, i) => {
46
+ if (typeof file?.name !== "string" || file.name.length === 0) {
47
+ throw new TypeError(`fileWindow: file ${i} has no name.`);
48
+ }
49
+ const ext = file.name.split(".").pop()?.toLowerCase() ?? "";
50
+ const known = KINDS[ext];
51
+ const content = file.content ?? "";
52
+ const bytes = new TextEncoder().encode(content).length;
53
+ return {
54
+ name: file.name,
55
+ content,
56
+ type: file.type ?? known?.type ?? "application/octet-stream",
57
+ size: file.size ??
58
+ (bytes === 1
59
+ ? "1 byte"
60
+ : bytes < 1024
61
+ ? `${bytes} bytes`
62
+ : `${Math.round(bytes / 1024)} KB`),
63
+ kind: known?.kind ?? "Document",
64
+ };
65
+ });
66
+ }
67
+ /**
68
+ * Runs in the page: draws the window and wires its selection and its drag.
69
+ * Self-contained, as everything handed to `page.evaluate` must be.
70
+ */
71
+ export function drawFileWindow(args) {
72
+ const doc = document;
73
+ doc.getElementById("vc-file-window")?.remove();
74
+ const style = doc.createElement("style");
75
+ style.id = "vc-file-window-style";
76
+ style.textContent = `
77
+ #vc-file-window { position: fixed; z-index: 2147483000; width: 480px; border-radius: 10px;
78
+ overflow: hidden; background: #ffffff; color: #1d1d1f; opacity: 0;
79
+ box-shadow: 0 18px 50px rgba(15, 15, 40, 0.28), 0 0 0 1px rgba(0, 0, 0, 0.12);
80
+ font: 13px/1.35 -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif; }
81
+ #vc-file-window .bar { height: 34px; display: flex; align-items: center; gap: 8px; padding: 0 12px;
82
+ background: #ececec; border-bottom: 1px solid #d9d9d9; }
83
+ #vc-file-window .dot { width: 12px; height: 12px; border-radius: 50%; }
84
+ #vc-file-window .title { flex: 1; text-align: center; margin-right: 52px; font-weight: 600; color: #4a4a4a; }
85
+ #vc-file-window .head, #vc-file-window .row { display: grid; grid-template-columns: 1fr 80px 150px;
86
+ gap: 8px; align-items: center; padding: 0 14px; }
87
+ #vc-file-window .head { height: 26px; color: #8a8a8e; font-size: 11px; border-bottom: 1px solid #eeeeee; }
88
+ #vc-file-window .row { height: 30px; cursor: default; user-select: none; }
89
+ #vc-file-window .row:nth-child(odd) { background: #f6f6f8; }
90
+ #vc-file-window .row.selected { background: #2f6fde; color: #ffffff; }
91
+ #vc-file-window .name { display: flex; align-items: center; gap: 8px; white-space: nowrap;
92
+ overflow: hidden; text-overflow: ellipsis; }
93
+ #vc-file-window .muted { color: #8a8a8e; white-space: nowrap; }
94
+ #vc-file-window .row.selected .muted { color: #dbe6ff; }
95
+ #vc-file-window .foot { height: 24px; display: flex; align-items: center; justify-content: center;
96
+ color: #8a8a8e; font-size: 11px; border-top: 1px solid #eeeeee; background: #fafafa; }
97
+ #vc-file-ghost { position: fixed; z-index: 2147483001; pointer-events: none; display: flex;
98
+ align-items: center; gap: 6px; padding: 5px 9px; border-radius: 6px; background: rgba(47, 111, 222, 0.92);
99
+ color: #ffffff; font: 12px/1.2 -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
100
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.2); }
101
+ #vc-file-ghost .count { background: #ff3b30; border-radius: 9px; padding: 0 6px; font-weight: 600; }
102
+ `;
103
+ doc.getElementById("vc-file-window-style")?.remove();
104
+ doc.head.append(style);
105
+ const icon = (kind) => {
106
+ const colour = /image/i.test(kind)
107
+ ? "#f4b400"
108
+ : /PDF/.test(kind)
109
+ ? "#ea4335"
110
+ : "#34a853";
111
+ return `<svg width="14" height="17" viewBox="0 0 14 17" aria-hidden="true"><path d="M1 1h8l4 4v11H1z" fill="#fff" stroke="#9a9aa2"/><path d="M9 1v4h4" fill="none" stroke="#9a9aa2"/><rect x="3" y="9" width="8" height="5" rx="1" fill="${colour}"/></svg>`;
112
+ };
113
+ const text = (value) => {
114
+ const span = doc.createElement("span");
115
+ span.textContent = value;
116
+ return span.innerHTML;
117
+ };
118
+ const win = doc.createElement("div");
119
+ win.id = "vc-file-window";
120
+ win.style.left = `${args.left}px`;
121
+ win.style.top = `${args.top}px`;
122
+ win.innerHTML = `
123
+ <div class="bar"><span class="dot" style="background:#ff5f57"></span><span class="dot" style="background:#febc2e"></span>
124
+ <span class="dot" style="background:#28c840"></span><span class="title">${text(args.title)}</span></div>
125
+ <div class="head"><span>Name</span><span>Size</span><span>Kind</span></div>
126
+ <div class="rows">${args.rows
127
+ .map((row, i) => `<div class="row" data-vc-file="${i}"><span class="name">${icon(row.kind)}${text(row.name)}</span>` +
128
+ `<span class="muted">${text(row.size)}</span><span class="muted">${text(row.kind)}</span></div>`)
129
+ .join("")}</div>
130
+ <div class="foot">${args.rows.length} ${args.rows.length === 1 ? "item" : "items"}</div>`;
131
+ doc.body.append(win);
132
+ const fade = (from, to, ms, done) => {
133
+ const start = performance.now();
134
+ const step = (now) => {
135
+ const t = Math.min(1, (now - start) / ms);
136
+ const eased = 1 - Math.pow(1 - t, 3);
137
+ const value = from + (to - from) * eased;
138
+ win.style.opacity = String(value);
139
+ win.style.transform = `translateY(${(1 - value) * 10}px)`;
140
+ if (t < 1)
141
+ requestAnimationFrame(step);
142
+ else
143
+ done?.();
144
+ };
145
+ requestAnimationFrame(step);
146
+ };
147
+ fade(0, 1, 280);
148
+ const rowEls = [...win.querySelectorAll(".row")];
149
+ const selected = new Set();
150
+ let anchor = 0;
151
+ const paint = () => rowEls.forEach((el, i) => el.classList.toggle("selected", selected.has(i)));
152
+ const transfer = () => {
153
+ const dt = new DataTransfer();
154
+ for (const i of [...selected].sort((a, b) => a - b)) {
155
+ const row = args.rows[i];
156
+ dt.items.add(new File([row.content], row.name, { type: row.type }));
157
+ }
158
+ return dt;
159
+ };
160
+ const fire = (target, type, x, y) => target.dispatchEvent(new DragEvent(type, {
161
+ bubbles: true,
162
+ cancelable: true,
163
+ composed: true,
164
+ clientX: x,
165
+ clientY: y,
166
+ dataTransfer: transfer(),
167
+ }));
168
+ const underPointer = (x, y) => {
169
+ const el = doc.elementFromPoint(x, y);
170
+ return el && !win.contains(el) ? el : null;
171
+ };
172
+ let pressed = null;
173
+ let ghost = null;
174
+ let over = null;
175
+ for (const [i, el] of rowEls.entries()) {
176
+ el.addEventListener("mousedown", (event) => {
177
+ event.preventDefault();
178
+ if (event.shiftKey) {
179
+ selected.clear();
180
+ for (let k = Math.min(anchor, i); k <= Math.max(anchor, i); k += 1)
181
+ selected.add(k);
182
+ }
183
+ else if (event.metaKey || event.ctrlKey) {
184
+ if (selected.has(i))
185
+ selected.delete(i);
186
+ else
187
+ selected.add(i);
188
+ anchor = i;
189
+ }
190
+ else if (!selected.has(i)) {
191
+ selected.clear();
192
+ selected.add(i);
193
+ anchor = i;
194
+ }
195
+ paint();
196
+ pressed = selected.has(i) ? { x: event.clientX, y: event.clientY } : null;
197
+ });
198
+ }
199
+ doc.addEventListener("mousemove", (event) => {
200
+ if (!pressed)
201
+ return;
202
+ if (!ghost) {
203
+ // A few pixels first, as a real drag waits for, so a click with a
204
+ // slightly unsteady hand selects and does not drag.
205
+ if (Math.hypot(event.clientX - pressed.x, event.clientY - pressed.y) < 4)
206
+ return;
207
+ ghost = doc.createElement("div");
208
+ ghost.id = "vc-file-ghost";
209
+ const first = args.rows[Math.min(...selected)];
210
+ ghost.innerHTML =
211
+ `${icon(first.kind)}${text(first.name)}` +
212
+ (selected.size > 1
213
+ ? `<span class="count">${selected.size}</span>`
214
+ : "");
215
+ doc.body.append(ghost);
216
+ }
217
+ ghost.style.left = `${event.clientX + 12}px`;
218
+ ghost.style.top = `${event.clientY + 10}px`;
219
+ const target = underPointer(event.clientX, event.clientY);
220
+ if (over && target !== over)
221
+ fire(over, "dragleave", event.clientX, event.clientY);
222
+ if (target && target !== over)
223
+ fire(target, "dragenter", event.clientX, event.clientY);
224
+ if (target)
225
+ fire(target, "dragover", event.clientX, event.clientY);
226
+ over = target;
227
+ });
228
+ doc.addEventListener("mouseup", (event) => {
229
+ const dragging = ghost !== null;
230
+ pressed = null;
231
+ if (!dragging)
232
+ return;
233
+ ghost?.remove();
234
+ ghost = null;
235
+ const target = underPointer(event.clientX, event.clientY);
236
+ over = null;
237
+ if (!target)
238
+ return;
239
+ fire(target, "drop", event.clientX, event.clientY);
240
+ // The files have gone where they were dropped, and the window with them.
241
+ fade(1, 0, 220, () => win.remove());
242
+ });
243
+ window.__vcCloseFileWindow = () => fade(1, 0, 220, () => win.remove());
244
+ }
245
+ //# sourceMappingURL=file-window.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-window.js","sourceRoot":"","sources":["../../src/driver/file-window.ts"],"names":[],"mappings":"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"}
@@ -80,10 +80,28 @@ export declare const SYNC_TRANSITIONS: string;
80
80
  * deterministically, at exactly the rate the timeline says.
81
81
  *
82
82
  * `scroll-behavior: auto` is here for the same reason: a smooth-scrolling
83
- * container animates outside our control.
83
+ * container animates outside our control. It covers what the stylesheet
84
+ * asks for; what a script asks for, `{ behavior: "smooth" }`, is
85
+ * `INSTANT_SCROLL_SCRIPT`'s.
84
86
  */
85
87
  export declare function freezeCss(zeroTransitions: boolean): string;
86
88
  export declare function freezeCssScript(transitions: Transitions): string;
89
+ /**
90
+ * Makes a script's smooth scroll instant, as the freeze stylesheet makes a
91
+ * stylesheet's.
92
+ *
93
+ * `scroll-behavior` in CSS only decides a scroll that does not say how to
94
+ * scroll. An app that calls `scrollIntoView({ behavior: "smooth" })`, or
95
+ * `scrollTo` with it, says, and the browser animates that scroll on the
96
+ * compositor's real clock whatever the stylesheet holds: a chat that keeps
97
+ * its newest message in view does it on every message, and the frames while
98
+ * it moved showed the pane wherever the machine had got it to. So the five
99
+ * methods a script scrolls with take `"smooth"` as `"instant"`, and every
100
+ * other argument through untouched. The scroll still happens, at once,
101
+ * which is what a clip on authored time can show the same way twice;
102
+ * `s.scroll` is how a clip shows one move.
103
+ */
104
+ export declare const INSTANT_SCROLL_SCRIPT = "(() => {\n const instant = (options) =>\n options && typeof options === \"object\" && options.behavior === \"smooth\"\n ? { ...options, behavior: \"instant\" }\n : options;\n const take = (owner, name) => {\n const original = owner[name];\n if (typeof original !== \"function\") return;\n owner[name] = function (...args) {\n if (args.length === 1) args[0] = instant(args[0]);\n return original.apply(this, args);\n };\n };\n for (const name of [\"scroll\", \"scrollTo\", \"scrollBy\", \"scrollIntoView\"]) {\n take(Element.prototype, name);\n }\n for (const name of [\"scroll\", \"scrollTo\", \"scrollBy\"]) take(window, name);\n})();";
87
105
  /**
88
106
  * What the driver learns from the page once per frame.
89
107
  *
@@ -123,5 +141,9 @@ declare global {
123
141
  __recorderTransitions?: {
124
142
  sync(): number;
125
143
  };
144
+ /** Set by the replay's pacing script (replay.ts). */
145
+ __recorderReplay?: {
146
+ pending: number;
147
+ };
126
148
  }
127
149
  }
@@ -196,7 +196,9 @@ export const SYNC_TRANSITIONS = "(window.__recorder ? window.__recorder.finishEn
196
196
  * deterministically, at exactly the rate the timeline says.
197
197
  *
198
198
  * `scroll-behavior: auto` is here for the same reason: a smooth-scrolling
199
- * container animates outside our control.
199
+ * container animates outside our control. It covers what the stylesheet
200
+ * asks for; what a script asks for, `{ behavior: "smooth" }`, is
201
+ * `INSTANT_SCROLL_SCRIPT`'s.
200
202
  */
201
203
  export function freezeCss(zeroTransitions) {
202
204
  const transitions = zeroTransitions
@@ -223,6 +225,39 @@ export function freezeCssScript(transitions) {
223
225
  else document.addEventListener("DOMContentLoaded", inject, { once: true });
224
226
  })();`;
225
227
  }
228
+ /**
229
+ * Makes a script's smooth scroll instant, as the freeze stylesheet makes a
230
+ * stylesheet's.
231
+ *
232
+ * `scroll-behavior` in CSS only decides a scroll that does not say how to
233
+ * scroll. An app that calls `scrollIntoView({ behavior: "smooth" })`, or
234
+ * `scrollTo` with it, says, and the browser animates that scroll on the
235
+ * compositor's real clock whatever the stylesheet holds: a chat that keeps
236
+ * its newest message in view does it on every message, and the frames while
237
+ * it moved showed the pane wherever the machine had got it to. So the five
238
+ * methods a script scrolls with take `"smooth"` as `"instant"`, and every
239
+ * other argument through untouched. The scroll still happens, at once,
240
+ * which is what a clip on authored time can show the same way twice;
241
+ * `s.scroll` is how a clip shows one move.
242
+ */
243
+ export const INSTANT_SCROLL_SCRIPT = `(() => {
244
+ const instant = (options) =>
245
+ options && typeof options === "object" && options.behavior === "smooth"
246
+ ? { ...options, behavior: "instant" }
247
+ : options;
248
+ const take = (owner, name) => {
249
+ const original = owner[name];
250
+ if (typeof original !== "function") return;
251
+ owner[name] = function (...args) {
252
+ if (args.length === 1) args[0] = instant(args[0]);
253
+ return original.apply(this, args);
254
+ };
255
+ };
256
+ for (const name of ["scroll", "scrollTo", "scrollBy", "scrollIntoView"]) {
257
+ take(Element.prototype, name);
258
+ }
259
+ for (const name of ["scroll", "scrollTo", "scrollBy"]) take(window, name);
260
+ })();`;
226
261
  /**
227
262
  * What the driver learns from the page once per frame.
228
263
  *
@@ -1 +1 @@
1
- {"version":3,"file":"page-hooks.js","sourceRoot":"","sources":["../../src/driver/page-hooks.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;;;GAIG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAwFjC,CAAC;AAEP;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;MA4BjC,CAAC;AAEP;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAC3B,8DAA8D;IAC9D,0EAA0E,CAAC;AAE7E;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,UAAU,SAAS,CAAC,eAAwB;IAChD,MAAM,WAAW,GAAG,eAAe;QACjC,CAAC,CAAC,6EAA6E;QAC/E,CAAC,CAAC,EAAE,CAAC;IACP,OAAO;EACP,WAAW;;;;EAIX,CAAC;AACH,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,WAAwB;IACtD,OAAO;iBACQ,WAAW,KAAK,MAAM;uBAChB,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;;;;;;;;;MAStF,CAAC;AACP,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,OAAe,EAAE,CAAS,EAAE,CAAS,EAAU,EAAE,CAC3E,4BAA4B,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC"}
1
+ {"version":3,"file":"page-hooks.js","sourceRoot":"","sources":["../../src/driver/page-hooks.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;;;GAIG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAwFjC,CAAC;AAEP;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;MA4BjC,CAAC;AAEP;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAC3B,8DAA8D;IAC9D,0EAA0E,CAAC;AAE7E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,UAAU,SAAS,CAAC,eAAwB;IAChD,MAAM,WAAW,GAAG,eAAe;QACjC,CAAC,CAAC,6EAA6E;QAC/E,CAAC,CAAC,EAAE,CAAC;IACP,OAAO;EACP,WAAW;;;;EAIX,CAAC;AACH,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,WAAwB;IACtD,OAAO;iBACQ,WAAW,KAAK,MAAM;uBAChB,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;;;;;;;;;MAStF,CAAC;AACP,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG;;;;;;;;;;;;;;;;;MAiB/B,CAAC;AAEP;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,OAAe,EAAE,CAAS,EAAE,CAAS,EAAU,EAAE,CAC3E,4BAA4B,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC"}
@@ -0,0 +1,263 @@
1
+ import type { BrowserContext } from "playwright";
2
+ /**
3
+ * Record a clip's responses once, and serve them on every run after.
4
+ *
5
+ * Some of what a clip films is not the app's to decide: a map's tiles, a
6
+ * model's answer, a job's status, a count that grows. Each comes from a
7
+ * server, at the server's speed and in its words of the day, so two
8
+ * recordings of one clip differ wherever one of them reaches the page, and
9
+ * the clip can never be re-rendered the same. The fix every app reached for
10
+ * was a mock written by hand, which is right once and wrong the day the
11
+ * endpoint changes. A replay is the server's own answer, written down the
12
+ * first time the clip runs and kept beside the clip as an input: the
13
+ * requests the config names are answered from it, and a request it does not
14
+ * hold fails the clip, with its URL, instead of quietly going live.
15
+ *
16
+ * The file is a HAR (one per clip, `<dir>/<clip>.har`), so a browser's
17
+ * developer tools can open it. Text bodies are written into it, where a
18
+ * diff can read them; binary bodies (a tile, an image) go beside it, under
19
+ * `bodies/`, named by their hash, so clips that load the same tiles store
20
+ * them once.
21
+ */
22
+ export type ReplayMode = "auto" | "record" | "replay" | "live";
23
+ /** A glob, in Playwright's syntax, or a regular expression, over the whole URL. */
24
+ export type ReplayPattern = string | RegExp;
25
+ export type ReplayConfig = {
26
+ /** The requests to record and replay, by URL. */
27
+ record: ReplayPattern[];
28
+ /**
29
+ * `"auto"`, the default, replays a clip whose file exists and records one
30
+ * whose file does not. `"record"` always records, `"replay"` fails a clip
31
+ * without a file, and `"live"` leaves every request to the network.
32
+ */
33
+ mode?: ReplayMode;
34
+ /** Where the files go. Default `replay/` beside the config. Commit it. */
35
+ dir?: string;
36
+ /**
37
+ * Responses the page reads over time, an event stream or lines of JSON,
38
+ * delivered one event or one line every `chunkMs` of the page's clock, so
39
+ * the same piece lands on the same frame every run.
40
+ */
41
+ pace?: {
42
+ url: ReplayPattern;
43
+ chunkMs: number;
44
+ }[];
45
+ /** Query parameters left out of the file and of the match, by name. */
46
+ redact?: string[];
47
+ /**
48
+ * What a request the replay does not hold does: `"fail"`, the default,
49
+ * fails the clip and names it; `"live"` sends it to the network and warns.
50
+ */
51
+ onMissing?: "fail" | "live";
52
+ };
53
+ export type ResolvedReplay = {
54
+ record: RegExp[];
55
+ mode: ReplayMode;
56
+ dir: string;
57
+ pace: {
58
+ url: RegExp;
59
+ chunkMs: number;
60
+ }[];
61
+ redact: string[];
62
+ onMissing: "fail" | "live";
63
+ };
64
+ /** How old a replay may be before `check` says so. */
65
+ export declare const STALE_DAYS = 90;
66
+ /**
67
+ * A glob in Playwright's syntax as a regular expression, so the recorder and
68
+ * the script it puts in the page match a URL the same way: `**` crosses `/`,
69
+ * `*` does not, `{a,b}` is either, and every other character is itself, `?`
70
+ * included. Anchored at both ends, as Playwright anchors a glob.
71
+ */
72
+ export declare function globToRegExp(glob: string): RegExp;
73
+ export declare function patternOf(pattern: ReplayPattern): RegExp;
74
+ /**
75
+ * The URL a request is known by in the file: without its fragment and
76
+ * without the query parameters `redact` names, which are gone from the file
77
+ * and so cannot be part of the match.
78
+ */
79
+ export declare function urlKey(url: string, redact: readonly string[]): string;
80
+ type Header = {
81
+ name: string;
82
+ value: string;
83
+ };
84
+ type HarContent = {
85
+ size: number;
86
+ mimeType: string;
87
+ text?: string;
88
+ encoding?: "base64";
89
+ /** A binary body, under the replay's `bodies/`, by its SHA-256. */
90
+ _file?: string;
91
+ };
92
+ export type HarEntry = {
93
+ startedDateTime: string;
94
+ time: number;
95
+ request: {
96
+ method: string;
97
+ url: string;
98
+ httpVersion: string;
99
+ headers: Header[];
100
+ queryString: Header[];
101
+ cookies: [];
102
+ headersSize: -1;
103
+ bodySize: number;
104
+ postData?: {
105
+ mimeType: string;
106
+ text: string;
107
+ };
108
+ };
109
+ response: {
110
+ status: number;
111
+ statusText: string;
112
+ httpVersion: string;
113
+ headers: Header[];
114
+ cookies: [];
115
+ content: HarContent;
116
+ redirectURL: string;
117
+ headersSize: -1;
118
+ bodySize: number;
119
+ };
120
+ cache: Record<string, never>;
121
+ timings: {
122
+ send: number;
123
+ wait: number;
124
+ receive: number;
125
+ };
126
+ };
127
+ export type Har = {
128
+ log: {
129
+ version: "1.2";
130
+ creator: {
131
+ name: string;
132
+ version: string;
133
+ };
134
+ comment: string;
135
+ _versioncam: {
136
+ clip: string;
137
+ recorded: string;
138
+ recorder: string;
139
+ };
140
+ entries: HarEntry[];
141
+ };
142
+ };
143
+ export declare function sha256(bytes: Uint8Array): string;
144
+ /**
145
+ * One recorded exchange, as the file keeps it: the secrets and the redacted
146
+ * parameters gone, a text body in the entry, a binary one handed to `store`
147
+ * and named here by its hash.
148
+ */
149
+ export declare function entryOf(exchange: {
150
+ method: string;
151
+ url: string;
152
+ requestHeaders: Record<string, string>;
153
+ postData: string | null;
154
+ status: number;
155
+ statusText: string;
156
+ responseHeaders: Record<string, string>;
157
+ body: Uint8Array;
158
+ }, options: {
159
+ redact: readonly string[];
160
+ recorded: string;
161
+ store: (hash: string, bytes: Uint8Array) => void;
162
+ }): HarEntry;
163
+ /**
164
+ * The entries of a replay, by what a request is known by, each list in the
165
+ * order the requests were made, and served in that order: the third poll of
166
+ * a job's status gets the third answer the job gave, and every poll after
167
+ * the last gets the last. Among answers to one URL, one recorded for the
168
+ * same request body is taken first, for an app that posts several queries
169
+ * to one endpoint in an order that can vary.
170
+ */
171
+ export declare class ReplayBook {
172
+ private readonly byKey;
173
+ private readonly used;
174
+ private readonly redact;
175
+ constructor(entries: readonly HarEntry[], redact: readonly string[]);
176
+ answer(method: string, url: string, postData: string | null): HarEntry | null;
177
+ }
178
+ /** The file a clip's replay is in. */
179
+ export declare function replayFile(dir: string, clipId: string): string;
180
+ export declare function readReplay(file: string): Har;
181
+ /** What `doctor` and `check` say about a clip's replay. */
182
+ export declare function describeReplay(dir: string, clipId: string, now?: Date): {
183
+ file: string;
184
+ exists: false;
185
+ } | {
186
+ file: string;
187
+ exists: true;
188
+ requests: number;
189
+ recorded: string;
190
+ ageDays: number;
191
+ recorder: string;
192
+ };
193
+ /** Which mode a clip runs in, once the file's presence is known. */
194
+ export declare function effectiveMode(mode: ReplayMode, fileExists: boolean): "record" | "replay" | "live";
195
+ /**
196
+ * A clip's replay while it runs: the routes it installs, what it recorded,
197
+ * what it could not answer, and how many answers are still on their way.
198
+ */
199
+ export declare class ClipReplay {
200
+ readonly mode: "record" | "replay" | "live";
201
+ readonly file: string;
202
+ /** Requests the replay matched and did not hold, as "METHOD url". */
203
+ readonly misses: string[];
204
+ /** Requests sent live because the replay did not hold them (`onMissing: "live"`). */
205
+ readonly wentLive: string[];
206
+ private readonly recorded;
207
+ private readonly bodies;
208
+ private readonly book;
209
+ private readonly when;
210
+ private inFlight;
211
+ private waiters;
212
+ private readonly config;
213
+ private readonly clipId;
214
+ private readonly recorder;
215
+ constructor(config: ResolvedReplay, clipId: string, recorder: string, override?: ReplayMode);
216
+ /** The page-side script that paces streamed bodies, or null when nothing is paced. */
217
+ pacingScript(): string | null;
218
+ /** Install the routes. Before the page is opened, so its first request is caught. */
219
+ install(context: BrowserContext): Promise<void>;
220
+ /**
221
+ * Resolves once every request the replay has taken is answered and its body
222
+ * is in the page. The driver waits on it after each tick, so a request made
223
+ * in a frame is answered within that frame on every run, however long the
224
+ * network or the disk took: a tile arrives on the frame it was asked for.
225
+ */
226
+ idle(timeoutMs?: number): Promise<void>;
227
+ private settled;
228
+ private handle;
229
+ private record;
230
+ private serve;
231
+ /**
232
+ * What stops the clip: a request the replay was asked for and did not
233
+ * hold. Null when there is none.
234
+ */
235
+ failure(): string | null;
236
+ /**
237
+ * Write what was recorded. Only for a clip that passed: a replay of half a
238
+ * clip would serve a later run a different app than the one it shows.
239
+ */
240
+ write(): void;
241
+ }
242
+ /**
243
+ * Deliver a paced response a piece at a time on the page's own clock.
244
+ *
245
+ * The recorder cannot stream a response into the page: Playwright fulfils a
246
+ * request whole. So the page does it. For a URL the config paces, `fetch`
247
+ * hands back a response whose body is a stream that gives out one event of
248
+ * an event stream, or one line of anything else, every `chunkMs`, on the
249
+ * page's `setTimeout`, which the recorder's clock advances a frame at a
250
+ * time; and `EventSource` dispatches one event every `chunkMs` the same
251
+ * way. The same piece therefore lands on the same frame every run, at any
252
+ * frame rate. `window.__recorderReplay.pending` counts the bodies still
253
+ * arriving, and a settle waits for it to reach none.
254
+ *
255
+ * An `EventSource` whose stream has ended reports an error and stays
256
+ * closed, instead of reconnecting as a live one would: a replay would only
257
+ * answer it with the same stream again.
258
+ */
259
+ export declare function pacedReplayScript(pace: readonly {
260
+ url: RegExp;
261
+ chunkMs: number;
262
+ }[]): string;
263
+ export {};