retake-dev 0.4.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 (43) hide show
  1. package/LICENSE +91 -0
  2. package/README.md +198 -0
  3. package/bin/retake.js +319 -0
  4. package/package.json +87 -0
  5. package/src/code-versions.js +357 -0
  6. package/src/core.js +265 -0
  7. package/src/plugin.js +137 -0
  8. package/src/runtime/00-core.js +308 -0
  9. package/src/runtime/10-animations.js +362 -0
  10. package/src/runtime/20-media.js +157 -0
  11. package/src/runtime/30-recorder.js +315 -0
  12. package/src/runtime/32-state.js +315 -0
  13. package/src/runtime/35-network.js +789 -0
  14. package/src/runtime/36-scripts.js +129 -0
  15. package/src/runtime/37-next.js +150 -0
  16. package/src/runtime/38-observers.js +252 -0
  17. package/src/runtime/40-input.js +795 -0
  18. package/src/runtime/45-hover.js +78 -0
  19. package/src/runtime/50-engine.js +341 -0
  20. package/src/runtime/60-preview.js +437 -0
  21. package/src/runtime/65-timeline.js +289 -0
  22. package/src/runtime/66-activity.js +114 -0
  23. package/src/runtime/67-csssource.js +226 -0
  24. package/src/runtime/70-boot.js +460 -0
  25. package/src/server/api.js +285 -0
  26. package/src/server/child.js +174 -0
  27. package/src/server/detect.js +167 -0
  28. package/src/server/front.js +647 -0
  29. package/src/server/mcp.js +332 -0
  30. package/src/shell/00-state.js +85 -0
  31. package/src/shell/05-api.js +136 -0
  32. package/src/shell/10-dock.js +754 -0
  33. package/src/shell/12-checkpoint.js +121 -0
  34. package/src/shell/15-session.js +291 -0
  35. package/src/shell/20-timeline.js +734 -0
  36. package/src/shell/25-input.js +537 -0
  37. package/src/shell/30-notes.js +870 -0
  38. package/src/shell/40-code.js +80 -0
  39. package/src/shell/90-handle.js +15 -0
  40. package/src/shell/shell.css +295 -0
  41. package/src/shell/shell.html +59 -0
  42. package/types/client.d.ts +73 -0
  43. package/types/index.d.ts +111 -0
@@ -0,0 +1,157 @@
1
+ // Audio/video can't be scrubbed, so it only plays while the clock plays at
2
+ // normal pace. Elements the app wanted playing are resumed when the clock does.
3
+
4
+ const MP = HTMLMediaElement.prototype
5
+ const mediaOrig = { play: MP.play, pause: MP.pause }
6
+ const media = new Set()
7
+
8
+ // play() settles in real time (and autoplay may reject it), so its outcome is
9
+ // recorded and replayed like a network response.
10
+ MP.play = function () {
11
+ media.add(this)
12
+ const el = this
13
+ return recordedAsync(() => {
14
+ if (!clock.playing || clock.seeking) return Promise.resolve()
15
+ return mediaOrig.play.call(el)
16
+ }).then(() => {
17
+ el.__ptWants = true
18
+ // Where the media was when it started, on the virtual clock.
19
+ el.__ptAt = clock.now
20
+ el.__ptFrom = el.currentTime || 0
21
+ mediaRun(el, el.__ptFrom, true)
22
+ })
23
+ }
24
+ MP.pause = function () {
25
+ if (this.__ptWants) mediaRun(this, this.currentTime || 0, false)
26
+ this.__ptWants = false
27
+ return mediaOrig.pause.call(this)
28
+ }
29
+
30
+ // Each element's runs on the virtual clock: { at, from, on } from `at` on, it
31
+ // played from `from` (on) or stood at it (off). The preview places media at
32
+ // the run in effect at its moment (F50), including media removed since.
33
+ function mediaRun(el, from, on) {
34
+ const runs = el.__ptRuns || (el.__ptRuns = [])
35
+ const last = runs[runs.length - 1]
36
+ if (last && last.at === clock.now) runs.pop()
37
+ runs.push({ at: clock.now, from, on, src: el.currentSrc || el.src || "" })
38
+ }
39
+
40
+ function syncMedia() {
41
+ const live = clock.playing && !clock.seeking
42
+ for (const el of media) {
43
+ if (el.__ptAlign && el.readyState >= 1 && !clock.seeking && !previewing) alignMedia()
44
+ // Removed media is kept (the DOM log keeps it too): a preview of a moment
45
+ // when it was there puts it back, at its time then.
46
+ if (!el.isConnected) {
47
+ if (!el.paused) mediaOrig.pause.call(el)
48
+ continue
49
+ }
50
+ if (!live && !el.paused) mediaOrig.pause.call(el)
51
+ else if (live && el.__ptWants && el.paused) mediaOrig.play.call(el).catch(() => {})
52
+ if (live && el.playbackRate !== clock.rate) el.playbackRate = clock.rate
53
+ }
54
+ }
55
+
56
+ // After a seek, media the app had playing jumps to where it would be at this
57
+ // moment on the virtual clock.
58
+ function alignMedia(at = clock.now) {
59
+ for (const el of media) {
60
+ if (previewing && el.__ptRuns && el.__ptRuns.length) {
61
+ placeMedia(el, at)
62
+ continue
63
+ }
64
+ if (!el.__ptWants || el.__ptAt == null) continue
65
+ // Too early to seek (no metadata yet): syncMedia does it once it can.
66
+ if (el.readyState < 1) {
67
+ el.__ptAlign = true
68
+ continue
69
+ }
70
+ el.__ptAlign = false
71
+ const t = el.__ptFrom + ((at - el.__ptAt) / 1000) * (el.playbackRate || 1)
72
+ const end = Number.isFinite(el.duration) ? el.duration : Infinity
73
+ try {
74
+ el.currentTime = el.loop && Number.isFinite(end) && end > 0 ? t % end : Math.min(t, end)
75
+ } catch {}
76
+ }
77
+ }
78
+
79
+ // The preview: where it was at `at`, from its runs (before the first: where that one started).
80
+ function placeMedia(el, at) {
81
+ if (el.readyState < 1) return
82
+ const runs = el.__ptRuns
83
+ let run = runs[0]
84
+ for (const r of runs) if (r.at <= at) run = r
85
+ const t = run.on && run.at <= at ? run.from + ((at - run.at) / 1000) * (el.playbackRate || 1) : run.from
86
+ const end = Number.isFinite(el.duration) ? el.duration : Infinity
87
+ try {
88
+ el.currentTime = el.loop && Number.isFinite(end) && end > 0 ? t % end : Math.min(t, end)
89
+ } catch {}
90
+ }
91
+
92
+ // ---- readiness events ------------------------------------------------------------
93
+ // A media file or image finishing loading is the network talking, in real
94
+ // time. Apps react to it (reveal UI on loadeddata, fade in on load), so the
95
+ // moment it happened is recorded, and on replay the real event is held back
96
+ // and the recorded one is dispatched at its virtual moment instead.
97
+ const READY = ["loadedmetadata", "loadeddata", "canplay", "canplaythrough", "playing", "load", "error"]
98
+ const readyTarget = (t) => t instanceof HTMLMediaElement || t instanceof HTMLImageElement
99
+ const delivered = new WeakMap() // element -> Set of types already given from the recording
100
+
101
+ function onReady(e) {
102
+ if (!e.isTrusted || !rec || !readyTarget(e.target)) return
103
+ const el = e.target
104
+ const given = delivered.get(el)
105
+ if (hasFuture() || clock.seeking) {
106
+ e.stopImmediatePropagation() // the recording delivers it at its moment
107
+ return
108
+ }
109
+ if (given && given.has(e.type)) {
110
+ // Already delivered from the recording before we reached the live edge.
111
+ given.delete(e.type)
112
+ e.stopImmediatePropagation()
113
+ return
114
+ }
115
+ const path = pathOf(el)
116
+ if (e.type === "playing" && el instanceof HTMLMediaElement) startedAt(el, el.currentTime)
117
+ if (path) recordEvent(e.type === "playing" ? { type: "ready", ev: e.type, path, ct: el.currentTime || 0 } : { type: "ready", ev: e.type, path })
118
+ }
119
+
120
+ // Media that starts by itself (autoplay) never calls play(): it's taken in
121
+ // when it starts, paused whenever the clock isn't playing at normal pace, and
122
+ // placed at its virtual time after a seek, like media the app played.
123
+ function startedAt(el, from) {
124
+ media.add(el)
125
+ el.__ptWants = true
126
+ if (el.__ptAt == null) {
127
+ el.__ptAt = clock.now
128
+ el.__ptFrom = from || 0
129
+ mediaRun(el, el.__ptFrom, true)
130
+ }
131
+ }
132
+ document.addEventListener(
133
+ "play",
134
+ (e) => {
135
+ const el = e.target
136
+ if (!(el instanceof HTMLMediaElement)) return
137
+ media.add(el)
138
+ el.__ptWants = true
139
+ if (!(clock.playing && !clock.seeking)) mediaOrig.pause.call(el)
140
+ },
141
+ true,
142
+ )
143
+ // On the document, not window: element load/error events don't reach window.
144
+ for (const type of READY) document.addEventListener(type, onReady, true)
145
+
146
+ function deliverReady(ev) {
147
+ const el = resolvePath(ev.path)
148
+ if (!el || !readyTarget(el)) return
149
+ if (!delivered.has(el)) delivered.set(el, new Set())
150
+ delivered.get(el).add(ev.ev)
151
+ // The recorded moment it started playing (autoplay included).
152
+ if (ev.ev === "playing" && el instanceof HTMLMediaElement) {
153
+ el.__ptAt = null
154
+ startedAt(el, ev.ct)
155
+ }
156
+ el.dispatchEvent(new Event(ev.ev))
157
+ }
@@ -0,0 +1,315 @@
1
+ // The recording: everything needed to rebuild the page at any moment by
2
+ // reloading and replaying — input events, fetch responses, the frame times the
3
+ // clock advanced through, the random seed, Date epoch and web storage.
4
+
5
+ function snapshotStorage(store) {
6
+ const out = {}
7
+ try {
8
+ for (let i = 0; i < store.length; i++) {
9
+ const k = store.key(i)
10
+ if (!k.startsWith(KEY)) out[k] = store.getItem(k)
11
+ }
12
+ } catch {}
13
+ return out
14
+ }
15
+
16
+ function restoreStorage(store, snap) {
17
+ try {
18
+ for (let i = store.length - 1; i >= 0; i--) {
19
+ const k = store.key(i)
20
+ if (!k.startsWith(KEY)) store.removeItem(k)
21
+ }
22
+ for (const k in snap) store.setItem(k, snap[k])
23
+ } catch {}
24
+ }
25
+
26
+ // ---- frames take turns with the origin's storage ------------------------------------
27
+ // The visible frame, a frame being built behind it and a checkpoint all share
28
+ // the origin's web storage and cookies, and a frame that boots puts them back
29
+ // to its own segment's start. So whichever frame last ran app code "owns" them
30
+ // (shell.storageOwner): a frame keeps a copy of its own when it stops running
31
+ // app code, and puts that copy back before it runs app code again if another
32
+ // frame took over meanwhile. IndexedDB can't be put back synchronously: a frame
33
+ // of an app that uses it reports storageOk false, and the dock rebuilds rather
34
+ // than resume it.
35
+ const storageId = real.random()
36
+ let ownStorage = null
37
+ let idbOpened = false
38
+ function keepStorage() {
39
+ if (shell && shell.storageOwner !== storageId) return
40
+ ownStorage = { local: snapshotStorage(real.local), session: snapshotStorage(real.session), cookies: snapshotCookies() }
41
+ }
42
+ function takeStorage() {
43
+ if (!shell || shell.storageOwner === storageId) return
44
+ if (ownStorage) {
45
+ restoreStorage(real.local, ownStorage.local)
46
+ restoreStorage(real.session, ownStorage.session)
47
+ restoreCookies(ownStorage.cookies)
48
+ }
49
+ shell.storageOwner = storageId
50
+ }
51
+ const usesIDB = () => idbOpened || segmentsOf().some((s) => s.idb && s.idb.length > 0)
52
+ const storageOk = () => !shell || shell.storageOwner === storageId || !usesIDB()
53
+
54
+ function newRecording() {
55
+ return withPacking({
56
+ v: 1,
57
+ url: location.href,
58
+ seed: Math.floor(real.random() * 2 ** 32),
59
+ epoch: real.Date.now(),
60
+ storage: { local: snapshotStorage(real.local), session: snapshotStorage(real.session) },
61
+ // Replays run in a frame of this size, so layout matches (F18).
62
+ viewport: { w: W.innerWidth, h: W.innerHeight },
63
+ // Element paths start at an id and skip scripts (F51; see pathOf).
64
+ pathV: 2,
65
+ // The front server's kept copy of this page (F56; RT.docId).
66
+ ...(RT.docId ? { doc: RT.docId } : {}),
67
+ frames: [],
68
+ events: [],
69
+ fetches: [],
70
+ end: 0,
71
+ })
72
+ }
73
+
74
+ let rec = null
75
+ const cursor = { event: 0, frame: 0 }
76
+
77
+ // ---- compact serialisation ----------------------------------------------------------
78
+ // JSON.stringify(rec) writes a compact form (v: 2): every DOM path is stored
79
+ // once in a table and events refer to it by index, which on a deep DOM
80
+ // halves the size. readRec() takes either form back. The in-memory object
81
+ // (history()) keeps plain paths.
82
+ // A packer keeps the tables, so events can be packed a slice at a time.
83
+ function packer(r) {
84
+ const tables = { paths: [], strs: [] }
85
+ const idx = { paths: new Map(), strs: new Map() }
86
+ const ref = (kind, v) => {
87
+ if (v == null) return v
88
+ const key = typeof v === "string" ? v : Array.isArray(v) ? v.join(",") : "=" + v
89
+ let i = idx[kind].get(key)
90
+ if (i == null) {
91
+ i = tables[kind].length
92
+ tables[kind].push(v)
93
+ idx[kind].set(key, i)
94
+ }
95
+ return i
96
+ }
97
+ // An event's time is usually exactly a frame boundary: store the frame's index.
98
+ const frameAt = new Map()
99
+ r.frames.forEach((f, i) => frameAt.set(f, i))
100
+ let lastF = 0
101
+ return {
102
+ event(ev) {
103
+ const out = { ...ev }
104
+ out.y = ref("strs", out.type)
105
+ delete out.type
106
+ if ("path" in out) (out.p = ref("paths", out.path)), delete out.path
107
+ if ("related" in out) (out.rp = ref("paths", out.related)), delete out.related
108
+ if (out.css != null) (out.c = ref("strs", out.css)), delete out.css
109
+ const fi = frameAt.get(out.t)
110
+ if (fi != null) {
111
+ out.f = fi - lastF
112
+ lastF = fi
113
+ delete out.t
114
+ }
115
+ return out
116
+ },
117
+ clip(c) {
118
+ const out = { ...c }
119
+ if (out.selector != null) (out.s = ref("strs", out.selector)), delete out.selector
120
+ if (out.component != null) (out.co = ref("strs", out.component)), delete out.component
121
+ if (out.label != null) (out.l = ref("strs", out.label)), delete out.label
122
+ if (out.path != null) (out.p = ref("paths", out.path)), delete out.path
123
+ return out
124
+ },
125
+ // Everything but the events, with the tables as they stand.
126
+ head(events) {
127
+ const out = {}
128
+ for (const k of Object.keys(r)) out[k] = r[k]
129
+ out.v = 3
130
+ out.events = events
131
+ if (r.clips) out.clips = r.clips.map((c) => this.clip(c))
132
+ out.paths = tables.paths
133
+ out.strs = tables.strs
134
+ return out
135
+ },
136
+ }
137
+ }
138
+ function packRec(r) {
139
+ const pk = packer(r)
140
+ const events = r.events.map((ev) => pk.event(ev))
141
+ return pk.head(events)
142
+ }
143
+ function unpackRec(o) {
144
+ if (o && o.v === 2 && o.paths) {
145
+ // v2: paths table only
146
+ const table = o.paths
147
+ o.events = o.events.map((ev) => {
148
+ const out = { ...ev }
149
+ if ("p" in out) (out.path = table[out.p]), delete out.p
150
+ if ("rp" in out) (out.related = table[out.rp]), delete out.rp
151
+ return out
152
+ })
153
+ delete o.paths
154
+ o.v = 1
155
+ } else if (o && o.v === 3) {
156
+ const P = o.paths
157
+ const S = o.strs
158
+ let lastF = 0
159
+ o.events = o.events.map((ev) => {
160
+ const out = { ...ev }
161
+ out.type = S[out.y]
162
+ delete out.y
163
+ if ("p" in out) (out.path = P[out.p]), delete out.p
164
+ if ("rp" in out) (out.related = P[out.rp]), delete out.rp
165
+ if ("c" in out) (out.css = S[out.c]), delete out.c
166
+ if ("f" in out) {
167
+ lastF += out.f
168
+ out.t = o.frames[lastF]
169
+ delete out.f
170
+ }
171
+ return out
172
+ })
173
+ if (o.clips) {
174
+ o.clips = o.clips.map((c) => {
175
+ const out = { ...c }
176
+ if ("s" in out) (out.selector = S[out.s]), delete out.s
177
+ if ("co" in out) (out.component = S[out.co]), delete out.co
178
+ if ("l" in out) (out.label = S[out.l]), delete out.l
179
+ if ("p" in out) (out.path = P[out.p]), delete out.p
180
+ return out
181
+ })
182
+ }
183
+ delete o.paths
184
+ delete o.strs
185
+ o.v = 1
186
+ }
187
+ return withPacking(o)
188
+ }
189
+ function withPacking(o) {
190
+ if (o && typeof o === "object" && !Object.prototype.hasOwnProperty.call(o, "toJSON")) {
191
+ Object.defineProperty(o, "toJSON", { value: function () { return packRec(this) }, enumerable: false, configurable: true })
192
+ }
193
+ return o
194
+ }
195
+ const readRec = (json) => unpackRec(typeof json === "string" ? JSON.parse(json) : json)
196
+
197
+ // Segments: a recording starts one on load, and another each time the app
198
+ // reloads itself. A moment is rebuilt from the start of its segment: that
199
+ // segment's URL, storage and seed, replaying only what came after it.
200
+ function segmentsOf(r = rec) {
201
+ const first = { t: 0, ev: 0, fr: 0, url: r.url, storage: r.storage, cookies: r.cookies, idb: r.idb, seed: r.seed, doc: r.doc, docDebug: r.docDebug }
202
+ return [first, ...(r.segments || [])]
203
+ }
204
+ function segmentIndex(t, r = rec) {
205
+ if (typeof r === "string") r = readRec(r)
206
+ let i = 0
207
+ for (const s of r.segments || []) if (s.t <= t) i++
208
+ return i
209
+ }
210
+ function segmentAt(t, r = rec) {
211
+ return segmentsOf(r)[segmentIndex(t, r)]
212
+ }
213
+ // Does this page already show that URL (ignoring our own __wb parameter)?
214
+ function sameDocUrl(url) {
215
+ try {
216
+ const a = new URL(url, location.href)
217
+ const b = new URL(location.href)
218
+ for (const u of [a, b]) u.searchParams.delete("__wb")
219
+ return a.origin === b.origin && a.pathname === b.pathname && a.search === b.search
220
+ } catch {
221
+ return true
222
+ }
223
+ }
224
+
225
+ const hasFuture = () =>
226
+ !!rec && (cursor.event < rec.events.length || cursor.frame < rec.frames.length)
227
+
228
+ // New input after going back starts a new future. The old one isn't lost: the
229
+ // dock keeps it as its own branch before we cut it off here.
230
+ function fork() {
231
+ if (!hasFuture()) return
232
+ if (shell && rec.start != null) shell.branchOff(JSON.stringify(rec), rec.end, clock.now)
233
+ const cut = rec.events.slice(cursor.event)
234
+ rec.events.length = cursor.event
235
+ rec.frames.length = cursor.frame
236
+ if (rec.routes) rec.routes = rec.routes.filter((r) => r.t <= clock.now)
237
+ if (rec.reloads) rec.reloads = rec.reloads.filter((t) => t <= clock.now)
238
+ if (rec.segments) rec.segments = rec.segments.filter((sg) => sg.t <= clock.now)
239
+ if (rec.clips) rec.clips = rec.clips.filter((c) => c.start <= clock.now)
240
+ cutActivity(clock.now)
241
+ netFork(cut)
242
+ rec.end = clock.now
243
+ PT.emit()
244
+ }
245
+
246
+ // The frame boundary whose recorded input (live, or replayed) is being handled:
247
+ // the DOM changes that follow are logged just after it (see logMutations).
248
+ // Moves, hovers, wheel and scroll don't count: they come every frame while the
249
+ // pointer moves, in between the frame's own work, which would be logged late.
250
+ let inputAt = null
251
+ const CONTINUOUS = /^(pointermove|mousemove|pointerover|pointerout|pointerenter|pointerleave|mouseover|mouseout|mouseenter|mouseleave|wheel|scroll|touchmove)$/
252
+ const markInput = (ev) => {
253
+ if (!CONTINUOUS.test(ev.type)) inputAt = clock.now
254
+ }
255
+
256
+ function recordEvent(ev) {
257
+ if (hasFuture()) return
258
+ ev.t = clock.now
259
+ markInput(ev)
260
+ const last = rec.events[rec.events.length - 1]
261
+ // Collapse pointer moves within one frame; only the latest position matters.
262
+ if (ev.type === "pointermove" && last && last.type === "pointermove" && last.t === ev.t && last.path + "" === ev.path + "") {
263
+ rec.events[rec.events.length - 1] = ev
264
+ } else {
265
+ rec.events.push(ev)
266
+ }
267
+ cursor.event = rec.events.length
268
+ }
269
+
270
+ function recordFrame(t) {
271
+ if (!hasFuture() && t > (rec.frames[rec.frames.length - 1] ?? -1)) {
272
+ rec.frames.push(t)
273
+ cursor.frame = rec.frames.length
274
+ }
275
+ if (t > rec.end) rec.end = t
276
+ }
277
+
278
+ // ---- other real-time promises (e.g. media.play()) ----------------------------
279
+ // Their outcome and the moment they settle are recorded, keyed by call order,
280
+ // and replayed identically.
281
+
282
+ let asyncSeq = 0
283
+ const pendingAsync = new Map()
284
+ const settledEarly = new Map()
285
+
286
+ function recordedAsync(realCall) {
287
+ const i = asyncSeq++
288
+ const replayable = hasFuture() && rec.events.some((e, j) => j >= cursor.event && e.type === "async" && e.i === i)
289
+ if (replayable) {
290
+ if (settledEarly.has(i)) return asyncOutcome(settledEarly.get(i))
291
+ return new Promise((resolve, reject) => pendingAsync.set(i, { resolve, reject }))
292
+ }
293
+ if (hasFuture() || (rec.start != null && !clock.playing)) return realCall()
294
+ return realCall().then(
295
+ (value) => {
296
+ recordEvent({ type: "async", i, ok: true })
297
+ return value
298
+ },
299
+ (err) => {
300
+ recordEvent({ type: "async", i, ok: false, name: err && err.name, message: err && err.message })
301
+ throw err
302
+ },
303
+ )
304
+ }
305
+
306
+ function asyncOutcome(ev) {
307
+ return ev.ok ? Promise.resolve() : Promise.reject(new DOMException(ev.message || "", ev.name || "Error"))
308
+ }
309
+
310
+ function settleAsync(ev) {
311
+ const p = pendingAsync.get(ev.i)
312
+ if (!p) return settledEarly.set(ev.i, ev)
313
+ pendingAsync.delete(ev.i)
314
+ asyncOutcome(ev).then(p.resolve, p.reject)
315
+ }