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,129 @@
1
+ // Scripts and stylesheets the app adds once its clock runs (chunk loaders:
2
+ // Turbopack, webpack, next/script, third-party tags) load in real time. Live,
3
+ // a lazy chunk landed at 233ms; in a rebuild (from the cache) at 83ms, so the
4
+ // code in it ran at another moment and every Math.random() after it came out
5
+ // different (F48). With RT.holdScripts (the front server's frames), when each
6
+ // one finished loading is recorded ({ type: "script", url, n, ok }; n counts
7
+ // the earlier loads of that URL on this page), and a replay holds the element
8
+ // back (preloading it) until the recording reaches that moment, then puts it
9
+ // in and waits for it to load (scriptBusy, see settle() in 00-core.js). One
10
+ // the recording doesn't have goes in at once, as before.
11
+ // Native import() can't be held (Vite's lazy routes, Astro islands).
12
+
13
+ let scriptBusy = 0
14
+ const scriptSeen = new WeakSet() // elements already counted (a preview moving one back isn't a load)
15
+ const scriptCount = new Map() // url -> loads so far on this page
16
+ const scriptHeld = new Map() // "url#n" -> puts the held element in
17
+ const scriptGiven = new Set() // "url#n" the replay reached before the app added it
18
+
19
+ const SCRIPT_TYPES = /^(|module|text\/javascript|application\/javascript|text\/ecmascript|application\/ecmascript)$/
20
+ function loadUrl(node) {
21
+ if (!node || node.nodeType !== 1) return null
22
+ if (node.tagName === "SCRIPT") return node.src && SCRIPT_TYPES.test((node.type || "").trim().toLowerCase()) ? node.src : null
23
+ if (node.tagName === "LINK") return /(^|\s)stylesheet(\s|$)/i.test(node.rel || "") && !node.disabled ? node.href || null : null
24
+ return null
25
+ }
26
+ const loadsSomething = (n) => !!n && (n.tagName === "SCRIPT" || n.tagName === "LINK") && !!loadUrl(n)
27
+
28
+ // Is that load in the recording still ahead of this replay?
29
+ function scriptAhead(url, n) {
30
+ for (let i = cursor.event; i < rec.events.length; i++) {
31
+ const e = rec.events[i]
32
+ if (e.type === "script" && e.url === url && e.n === n) return true
33
+ }
34
+ return false
35
+ }
36
+
37
+ // Put it in now, and have time wait for it (3s at most: one that never loads
38
+ // mustn't stall every frame after it).
39
+ function insertWaited(node, insert, preload) {
40
+ scriptBusy++
41
+ let done = false
42
+ const end = () => {
43
+ if (done) return
44
+ done = true
45
+ scriptBusy--
46
+ }
47
+ node.addEventListener("load", end, { once: true })
48
+ node.addEventListener("error", end, { once: true })
49
+ real.setTimeout(end, 3000)
50
+ if (preload) preload.remove()
51
+ return insert()
52
+ }
53
+
54
+ // The app adding a script or stylesheet (`insert` does it for real).
55
+ function trackLoad(node, insert) {
56
+ if (!clock.booted || previewing || scriptSeen.has(node) || !rec) return insert()
57
+ const url = loadUrl(node)
58
+ if (!url) return insert()
59
+ scriptSeen.add(node)
60
+ const n = scriptCount.get(url) || 0
61
+ scriptCount.set(url, n + 1)
62
+ const key = url + "#" + n
63
+ if (!hasFuture() && !clock.seeking) {
64
+ // Live: when it's in is recorded.
65
+ const done = (ok) => recordEvent(ok ? { type: "script", url, n } : { type: "script", url, n, ok: false })
66
+ node.addEventListener("load", () => done(true), { once: true })
67
+ node.addEventListener("error", () => done(false), { once: true })
68
+ return insert()
69
+ }
70
+ if (scriptGiven.delete(key)) return insertWaited(node, insert)
71
+ if (!scriptAhead(url, n)) return insert()
72
+ // Held until its moment; fetched meanwhile, so it's in the cache by then.
73
+ let preload = null
74
+ try {
75
+ preload = document.createElement("link")
76
+ preload.rel = "preload"
77
+ preload.as = node.tagName === "SCRIPT" ? "script" : "style"
78
+ if (node.crossOrigin != null) preload.crossOrigin = node.crossOrigin
79
+ preload.href = url
80
+ scriptOrig.appendChild.call(document.head || document.documentElement, preload)
81
+ } catch {}
82
+ scriptHeld.set(key, () => insertWaited(node, insert, preload))
83
+ return node
84
+ }
85
+
86
+ // The recording reached a load.
87
+ function deliverScript(ev) {
88
+ const key = ev.url + "#" + ev.n
89
+ const go = scriptHeld.get(key)
90
+ if (!go) return scriptGiven.add(key)
91
+ scriptHeld.delete(key)
92
+ go()
93
+ }
94
+ // At the live edge nothing more is coming from the recording (it was cut, or
95
+ // forked): whatever is still held goes in.
96
+ function releaseHeldScripts() {
97
+ if (!scriptHeld.size || hasFuture()) return
98
+ const all = [...scriptHeld.values()]
99
+ scriptHeld.clear()
100
+ for (const go of all) go()
101
+ }
102
+
103
+ const scriptOrig = { appendChild: Node.prototype.appendChild, insertBefore: Node.prototype.insertBefore }
104
+ if (RT.holdScripts) {
105
+ const NP = Node.prototype
106
+ NP.appendChild = function (node) {
107
+ if (!loadsSomething(node)) return scriptOrig.appendChild.call(this, node)
108
+ return trackLoad(node, () => scriptOrig.appendChild.call(this, node))
109
+ }
110
+ NP.insertBefore = function (node, ref) {
111
+ if (!loadsSomething(node)) return scriptOrig.insertBefore.call(this, node, ref)
112
+ return trackLoad(node, () => scriptOrig.insertBefore.call(this, node, ref))
113
+ }
114
+ // append/prepend/before/after take several nodes: with a script among them,
115
+ // each goes in on its own, in the same order.
116
+ const EP = Element.prototype
117
+ for (const m of ["append", "prepend", "before", "after"]) {
118
+ const orig = EP[m]
119
+ if (typeof orig !== "function") continue
120
+ const reversed = m === "prepend" || m === "after"
121
+ EP[m] = function (...nodes) {
122
+ if (!nodes.some(loadsSomething)) return orig.apply(this, nodes)
123
+ for (const n of reversed ? nodes.reverse() : nodes) {
124
+ if (loadsSomething(n)) trackLoad(n, () => orig.call(this, n))
125
+ else orig.call(this, n)
126
+ }
127
+ }
128
+ }
129
+ }
@@ -0,0 +1,150 @@
1
+ // Next.js 16's debug channel (F49). In development, Next sends React's debug
2
+ // data for each request over its HMR socket, keyed by the request's
3
+ // `x-nextjs-request-id` header, and React waits for that stream (and its
4
+ // end) before it shows a soft navigation or a server action's result. A
5
+ // replayed request never reached the server, so nothing would come. So the
6
+ // debug data that arrives for a recorded request is kept with it
7
+ // (`entry.debug`), and a replay hands it, then its end, to Next's HMR client
8
+ // the way the server sent it: the data as the answer starts, the end once the
9
+ // answer has ended.
10
+ //
11
+ // The page's own debug data (for its HTML request, `self.__next_r`) is kept
12
+ // with its segment too: a rebuild given the page as it was recorded (F56)
13
+ // gets it the same way, since the server sent it to the original page only.
14
+ //
15
+ // The message (next@16.3, client/dev/hot-reloader/app/web-socket.js) is
16
+ // binary: [0 (REACT_DEBUG_CHUNK), id length, id bytes, chunk bytes]; no chunk
17
+ // bytes means the stream has ended. Only for the Next version the server
18
+ // detected (RT.next); any other version gets the CLI's warning only.
19
+ // RT.next "auto": the server knows it's Next but not which (fronting a bare
20
+ // URL, `retake http://localhost:3000`): Next's client says, `window.next.version`,
21
+ // set before it connects its HMR socket or sends a request.
22
+
23
+ const nextOn = () => RT.next === 16 || (RT.next === "auto" && /^16\./.test(String((W.next && W.next.version) || "")))
24
+ const nextLive = new Map() // request id -> the recorded fetch entry it's for (live requests)
25
+ let nextOwn = false // dispatching our own message (not the server's)
26
+ const nextQueue = [] // [id, chunk bytes | null] waiting for Next's HMR socket
27
+ const nextId = (headers) => (nextOn() && headers && headers.get("x-nextjs-request-id")) || null
28
+
29
+ // A live request Next will send debug data for: keep it with the recording.
30
+ function nextLiveRequest(entry, headers) {
31
+ const id = nextId(headers)
32
+ if (!id) return
33
+ nextLive.set(id, entry)
34
+ if (nextLive.size > 200) nextLive.delete(nextLive.keys().next().value)
35
+ }
36
+ // A message on Next's HMR socket (the server's): debug data for a live request.
37
+ function nextDevMessage(e) {
38
+ if (nextOwn || !nextOn() || !(e.data instanceof ArrayBuffer) || e.data.byteLength < 2) return
39
+ const bytes = new Uint8Array(e.data)
40
+ if (bytes[0] !== 0) return
41
+ const len = bytes[1]
42
+ if (bytes.length <= 2 + len) return
43
+ const id = new TextDecoder().decode(bytes.subarray(2, 2 + len))
44
+ const chunk = toB64(bytes.subarray(2 + len))
45
+ const entry = nextLive.get(id)
46
+ if (entry) (entry.debug || (entry.debug = [])).push(chunk)
47
+ else if (id === W.__next_r && RT.docId && !RT.docStored && rec) {
48
+ // This page's own: kept with the segment this page is (rec itself for the first).
49
+ const seg = rec.doc === RT.docId ? rec : (rec.segments || []).find((s) => s.doc === RT.docId)
50
+ if (seg) (seg.docDebug || (seg.docDebug = [])).push(chunk)
51
+ }
52
+ }
53
+
54
+ // Next's HMR socket names the page's request (`/_next/hmr?id=<__next_r>`), and
55
+ // the server keeps one socket per id: when one closes, the id is dropped, along
56
+ // with whichever socket last registered it. A page served as it was recorded
57
+ // (F56) carries the recorded page's id, shared with the frame it was recorded
58
+ // in and every other build of it, so swapping one in for another left the new
59
+ // frame's socket without hot updates. Such a page's socket gets an id of its own
60
+ // (its debug data comes from the recording, below, not from the server).
61
+ //
62
+ // The server also sends a live request's debug data to the socket registered
63
+ // under the request's `x-nextjs-html-request-id` (= `__next_r`): with no socket
64
+ // under that id any more, a soft navigation or server action made live from
65
+ // such a frame waited for ever. So its live requests carry the socket's id
66
+ // (`nextLiveInit`).
67
+ let nextSocketId = null
68
+ function nextSocketUrl(url) {
69
+ if (!RT.docStored || !W.__next_r) return url
70
+ try {
71
+ const u = new URL(String(url), location.href)
72
+ if (!/^\/_next\/(webpack-)?hmr\b/.test(u.pathname) || u.searchParams.get("id") !== W.__next_r) return url
73
+ nextSocketId = nextSocketId || `${W.__next_r}-retake-${Math.round(performance.timeOrigin + real.perfNow()).toString(36)}${real.random().toString(36).slice(2, 6)}`
74
+ u.searchParams.set("id", nextSocketId)
75
+ return u.href
76
+ } catch {
77
+ return url
78
+ }
79
+ }
80
+ // A server component edit makes Next's router refetch the page's RSC payload,
81
+ // marked `next-hmr-refresh: 1`. It's the dev server's doing, not the app's:
82
+ // recorded, a later edit's refetch in a frame with a future (after a rewind)
83
+ // was answered with the payload of the edit before, and the page kept showing
84
+ // old code (F77). It goes to the network, unrecorded, like the HMR socket.
85
+ const nextHmrRefresh = (headers) => !!headers && headers.get("next-hmr-refresh") === "1"
86
+
87
+ // A live request from such a page: [input, init] naming its socket, not the page.
88
+ const HTML_ID = "x-nextjs-html-request-id"
89
+ function nextLiveInit(input, init) {
90
+ if (!nextSocketId) return [input, init]
91
+ try {
92
+ if (init && init.headers) {
93
+ const h = new Headers(init.headers)
94
+ if (h.get(HTML_ID) !== W.__next_r) return [input, init]
95
+ h.set(HTML_ID, nextSocketId)
96
+ return [input, { ...init, headers: h }]
97
+ }
98
+ if (typeof Request !== "undefined" && input instanceof Request && input.headers.get(HTML_ID) === W.__next_r) {
99
+ const r = new Request(input, init)
100
+ r.headers.set(HTML_ID, nextSocketId)
101
+ return [r, undefined]
102
+ }
103
+ } catch {}
104
+ return [input, init]
105
+ }
106
+
107
+ // A page served as it was recorded: its debug data comes from the recording.
108
+ let nextDocSent = false
109
+ function nextDocReplay() {
110
+ if (nextDocSent || !nextOn() || !RT.docStored || !W.__next_r || !rec) return
111
+ nextDocSent = true
112
+ const seg = segmentsOf().find((s) => s.doc === RT.docId)
113
+ for (const b64 of (seg && seg.docDebug) || []) nextQueue.push([W.__next_r, fromB64(b64)])
114
+ nextQueue.push([W.__next_r, null])
115
+ }
116
+
117
+ // A replayed request's answer starts: its debug data goes to Next.
118
+ function nextReplayStart(entry, headers) {
119
+ const id = nextId(headers)
120
+ if (!id) return
121
+ for (const b64 of entry.debug || []) nextQueue.push([id, fromB64(b64)])
122
+ flushNext()
123
+ }
124
+ // ...and ends: so does its debug stream.
125
+ function nextReplayEnd(headers) {
126
+ const id = nextId(headers)
127
+ if (!id) return
128
+ nextQueue.push([id, null])
129
+ flushNext()
130
+ }
131
+ function flushNext() {
132
+ if (!nextQueue.length) return
133
+ let ws = null
134
+ for (const w of devSockets) if (/\/_next\/hmr\b/.test(w.url) && w.readyState === 1) ws = w
135
+ if (!ws) return // sent once the socket opens (see 35-network.js)
136
+ for (const [id, chunk] of nextQueue.splice(0)) {
137
+ const idBytes = new TextEncoder().encode(id)
138
+ if (idBytes.length > 255) continue
139
+ const data = new Uint8Array(2 + idBytes.length + (chunk ? chunk.length : 0))
140
+ data[1] = idBytes.length
141
+ data.set(idBytes, 2)
142
+ if (chunk) data.set(chunk, 2 + idBytes.length)
143
+ nextOwn = true
144
+ try {
145
+ ws.dispatchEvent(new MessageEvent("message", { data: data.buffer, origin: new URL(ws.url).origin }))
146
+ } catch {} finally {
147
+ nextOwn = false
148
+ }
149
+ }
150
+ }
@@ -0,0 +1,252 @@
1
+ // Things the browser tells the app on its own schedule: IntersectionObserver
2
+ // and ResizeObserver callbacks (on real rendering steps) and Worker messages
3
+ // (from another thread). Each delivery is recorded with its entries and
4
+ // moment and replayed from the recording, so they land at the same virtual
5
+ // time and with the same values.
6
+
7
+ // ---- IntersectionObserver / ResizeObserver --------------------------------------
8
+
9
+ let obsSeq = 0
10
+ const observers = new Map() // creation order -> wrapper
11
+
12
+ const rectOf = (r) => (r ? [r.x, r.y, r.width, r.height] : null)
13
+ const toRect = (a) => (a ? DOMRectReadOnly.fromRect({ x: a[0], y: a[1], width: a[2], height: a[3] }) : null)
14
+ const sizeOf = (list) => [...(list || [])].map((s) => [s.inlineSize, s.blockSize])
15
+ const toSizes = (a) => (a || []).map(([inlineSize, blockSize]) => ({ inlineSize, blockSize }))
16
+
17
+ function wrapObserver(Real, pack, unpack) {
18
+ const Wrapped = function (callback, options) {
19
+ if (!new.target) throw new TypeError("Failed to construct: Please use the 'new' operator")
20
+ const self = this
21
+ this._id = ++obsSeq
22
+ this._cb = callback
23
+ this._targets = []
24
+ this._off = false
25
+ observers.set(this._id, this)
26
+ this._real = new Real((entries) => self._got(entries), options)
27
+ }
28
+ Wrapped.prototype.observe = function (target, opts) {
29
+ if (!this._targets.includes(target)) this._targets.push(target)
30
+ this._off = false
31
+ return this._real.observe(target, opts)
32
+ }
33
+ Wrapped.prototype.unobserve = function (target) {
34
+ return this._real.unobserve(target)
35
+ }
36
+ Wrapped.prototype.disconnect = function () {
37
+ this._off = true
38
+ return this._real.disconnect()
39
+ }
40
+ Wrapped.prototype.takeRecords = function () {
41
+ return this._real.takeRecords ? this._real.takeRecords() : []
42
+ }
43
+ for (const k of ["root", "rootMargin", "thresholds", "scrollMargin", "delay", "trackVisibility"]) {
44
+ Object.defineProperty(Wrapped.prototype, k, {
45
+ get() {
46
+ return this._real[k]
47
+ },
48
+ })
49
+ }
50
+ Wrapped.prototype._got = function (entries) {
51
+ // In the past, the recording delivers them; while rebuilding, too.
52
+ if (hasFuture() || clock.seeking) return
53
+ const data = []
54
+ for (const e of entries) {
55
+ let k = this._targets.indexOf(e.target)
56
+ if (k < 0) {
57
+ this._targets.push(e.target)
58
+ k = this._targets.length - 1
59
+ }
60
+ data.push(pack(e, k))
61
+ }
62
+ recordEvent({ type: "obs", o: this._id, entries: data })
63
+ this._deliver(data)
64
+ }
65
+ Wrapped.prototype._deliver = function (data) {
66
+ if (this._off) return
67
+ const list = data.map((d) => unpack(d, this._targets[d.k])).filter((e) => e.target)
68
+ if (list.length) safeCall(this._cb, [list, this])
69
+ }
70
+ return Wrapped
71
+ }
72
+
73
+ function deliverObserved(ev) {
74
+ const o = observers.get(ev.o)
75
+ if (o) o._deliver(ev.entries)
76
+ }
77
+
78
+ if (W.IntersectionObserver) {
79
+ W.IntersectionObserver = wrapObserver(
80
+ W.IntersectionObserver,
81
+ (e, k) => ({ k, i: e.isIntersecting ? 1 : 0, r: e.intersectionRatio, b: rectOf(e.boundingClientRect), n: rectOf(e.intersectionRect), rb: rectOf(e.rootBounds), v: e.isVisible ? 1 : 0 }),
82
+ (d, target) => ({
83
+ target,
84
+ time: performance.now(),
85
+ isIntersecting: !!d.i,
86
+ intersectionRatio: d.r,
87
+ boundingClientRect: toRect(d.b),
88
+ intersectionRect: toRect(d.n),
89
+ rootBounds: toRect(d.rb),
90
+ isVisible: !!d.v,
91
+ }),
92
+ )
93
+ }
94
+ if (W.ResizeObserver) {
95
+ W.ResizeObserver = wrapObserver(
96
+ W.ResizeObserver,
97
+ (e, k) => ({ k, cr: rectOf(e.contentRect), bb: sizeOf(e.borderBoxSize), cb: sizeOf(e.contentBoxSize), dp: sizeOf(e.devicePixelContentBoxSize) }),
98
+ (d, target) => ({
99
+ target,
100
+ contentRect: toRect(d.cr),
101
+ borderBoxSize: toSizes(d.bb),
102
+ contentBoxSize: toSizes(d.cb),
103
+ devicePixelContentBoxSize: toSizes(d.dp),
104
+ }),
105
+ )
106
+ }
107
+
108
+ // ---- Workers ------------------------------------------------------------------------
109
+ // A worker's messages are recorded (when they're plain data) and replayed
110
+ // without starting the worker. A worker that sends anything else (transferables,
111
+ // class instances) runs for real on replay too, and isn't deterministic.
112
+
113
+ let workerSeq = 0
114
+ const workers = new Map()
115
+ const clockedWorkers = new Set()
116
+ function tickWorkers(t) {
117
+ for (const w of clockedWorkers) {
118
+ if (w._clocked && w._real && !w._dead) {
119
+ try {
120
+ w._real.postMessage({ __retakeTick: t })
121
+ } catch {}
122
+ }
123
+ }
124
+ }
125
+ const RealWorker = W.Worker
126
+
127
+ function plainData(v, depth = 0) {
128
+ if (depth > 50) return false
129
+ if (v === null || ["string", "number", "boolean", "undefined"].includes(typeof v)) return true
130
+ if (Array.isArray(v)) return v.every((x) => plainData(x, depth + 1))
131
+ if (typeof v === "object" && Object.getPrototypeOf(v) === Object.prototype) return Object.values(v).every((x) => plainData(x, depth + 1))
132
+ return false
133
+ }
134
+
135
+ // Workers run on the page's virtual clock too: each one starts with a prelude
136
+ // that replaces its timers, rAF, performance.now, Date and Math.random with
137
+ // versions driven by the page (a tick per frame) and seeded per worker, then
138
+ // loads the real worker script. So a worker drawing confetti on an
139
+ // OffscreenCanvas draws the same thing at the same moment on every replay.
140
+ function workerPrelude(url, type, seedFor, epochAt) {
141
+ const load = type === "module" ? `await import(${JSON.stringify(url)})` : `importScripts(${JSON.stringify(url)})`
142
+ return `(() => {
143
+ const BASE = ${JSON.stringify(url)}, PERF = 1000, EPOCH = ${epochAt}
144
+ let now = 0, state = ${seedFor} >>> 0, seq = 1
145
+ const timers = new Map(), rafs = new Map()
146
+ Math.random = () => { state = (state + 0x6d2b79f5) >>> 0; let x = state; x = Math.imul(x ^ (x >>> 15), x | 1); x ^= x + Math.imul(x ^ (x >>> 7), x | 61); return ((x ^ (x >>> 14)) >>> 0) / 4294967296 }
147
+ performance.now = () => now + PERF
148
+ const RD = Date
149
+ function VDate(...a) { if (!new.target) return new RD(EPOCH + now).toString(); return a.length ? new RD(...a) : new RD(EPOCH + now) }
150
+ VDate.prototype = RD.prototype; VDate.now = () => EPOCH + now; VDate.parse = RD.parse; VDate.UTC = RD.UTC
151
+ self.Date = VDate
152
+ const add = (fn, d, a, rep) => { const id = seq++; const delay = Math.max(rep ? 4 : 0, Number(d) || 0); timers.set(id, { fn, a, due: now + delay, delay, rep }); return id }
153
+ self.setTimeout = (fn, d, ...a) => (typeof fn === "function" ? add(fn, d, a, false) : 0)
154
+ self.setInterval = (fn, d, ...a) => (typeof fn === "function" ? add(fn, d, a, true) : 0)
155
+ self.clearTimeout = self.clearInterval = (id) => timers.delete(id)
156
+ self.requestAnimationFrame = (cb) => { const id = seq++; rafs.set(id, cb); return id }
157
+ self.cancelAnimationFrame = (id) => rafs.delete(id)
158
+ if (self.importScripts) { const ri = self.importScripts.bind(self); self.importScripts = (...u) => ri(...u.map((x) => new URL(x, BASE).href)) }
159
+ function advance(t) {
160
+ for (let n = 0; n < 5000; n++) {
161
+ let best = null
162
+ for (const x of timers.values()) if (x.due <= t && (!best || x.due < best.due)) best = x
163
+ if (!best) break
164
+ now = Math.max(now, best.due)
165
+ if (best.rep) best.due += best.delay; else for (const [k, v] of timers) if (v === best) timers.delete(k)
166
+ try { best.fn(...best.a) } catch (e) { setTimeout(() => { throw e }) }
167
+ }
168
+ now = t
169
+ const q = [...rafs.values()]; rafs.clear()
170
+ for (const cb of q) try { cb(now + PERF) } catch (e) {}
171
+ }
172
+ self.addEventListener("message", (e) => { if (e.data && e.data.__retakeTick != null) { e.stopImmediatePropagation(); advance(e.data.__retakeTick) } })
173
+ })();
174
+ ${load}
175
+ `
176
+ }
177
+
178
+ if (RealWorker) {
179
+ class RetakeWorker extends EventTarget {
180
+ constructor(url, opts) {
181
+ if (isExempt()) return new RealWorker(url, opts)
182
+ super()
183
+ this.onmessage = this.onerror = this.onmessageerror = null
184
+ this._id = ++workerSeq
185
+ workers.set(this._id, this)
186
+ const list = rec.workers || (rec.workers = {})
187
+ const known = list[this._id]
188
+ if (hasFuture() && known && known.ok) {
189
+ this._replay = true // messages come from the recording
190
+ return
191
+ }
192
+ if (!known) list[this._id] = { url: String(url), ok: true }
193
+ let real = null
194
+ try {
195
+ const abs = new URL(String(url), location.href).href
196
+ const type = opts && opts.type === "module" ? "module" : "classic"
197
+ const code = workerPrelude(abs, type, (seed ^ Math.imul(this._id, 0x9e3779b1)) >>> 0, rec.epoch)
198
+ const blob = URL.createObjectURL(new Blob([code], { type: "text/javascript" }))
199
+ real = new RealWorker(blob, { ...opts, type: type === "module" ? "module" : "classic" })
200
+ this._clocked = true
201
+ } catch {
202
+ real = new RealWorker(url, opts) // e.g. a CSP that forbids blob: workers
203
+ }
204
+ this._real = real
205
+ clockedWorkers.add(this)
206
+ this._real.onmessage = (e) => this._got(e.data)
207
+ this._real.onmessageerror = (e) => this._emit("messageerror", e)
208
+ this._real.onerror = (e) => {
209
+ const ev = new ErrorEvent("error", { message: e.message, filename: e.filename, lineno: e.lineno, colno: e.colno, error: e.error })
210
+ this._emit("error", ev)
211
+ if (ev.defaultPrevented) e.preventDefault()
212
+ }
213
+ }
214
+ _emit(type, ev) {
215
+ const prop = this["on" + type]
216
+ if (typeof prop === "function") safeCall(prop.bind(this), [ev])
217
+ this.dispatchEvent(ev)
218
+ }
219
+ _got(data) {
220
+ arrive(() => {
221
+ if (!hasFuture() && !clock.seeking) {
222
+ const entry = rec.workers[this._id]
223
+ if (entry && entry.ok && plainData(data)) recordEvent({ type: "worker", w: this._id, d: data === undefined ? { $u: 1 } : { v: data } })
224
+ else if (entry) entry.ok = false
225
+ }
226
+ this._emit("message", new MessageEvent("message", { data }))
227
+ })
228
+ }
229
+ postMessage(msg, transfer) {
230
+ // A worker handed a canvas draws on the page: it has to run for real on
231
+ // replays (on the virtual clock), not be stood in for by its messages.
232
+ const list = [].concat((transfer && transfer.transfer) || transfer || [])
233
+ if (typeof OffscreenCanvas !== "undefined" && list.some((x) => x instanceof OffscreenCanvas)) {
234
+ const entry = rec.workers && rec.workers[this._id]
235
+ if (entry) entry.ok = false
236
+ }
237
+ if (this._real) this._real.postMessage(msg, transfer)
238
+ }
239
+ terminate() {
240
+ this._dead = true
241
+ clockedWorkers.delete(this)
242
+ if (this._real) this._real.terminate()
243
+ }
244
+ }
245
+ W.Worker = RetakeWorker
246
+ }
247
+
248
+ function deliverWorker(ev) {
249
+ const w = workers.get(ev.w)
250
+ if (!w || !w._replay || w._dead) return
251
+ w._emit("message", new MessageEvent("message", { data: ev.d && "v" in ev.d ? ev.d.v : undefined }))
252
+ }