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.
- package/LICENSE +91 -0
- package/README.md +198 -0
- package/bin/retake.js +319 -0
- package/package.json +87 -0
- package/src/code-versions.js +357 -0
- package/src/core.js +265 -0
- package/src/plugin.js +137 -0
- package/src/runtime/00-core.js +308 -0
- package/src/runtime/10-animations.js +362 -0
- package/src/runtime/20-media.js +157 -0
- package/src/runtime/30-recorder.js +315 -0
- package/src/runtime/32-state.js +315 -0
- package/src/runtime/35-network.js +789 -0
- package/src/runtime/36-scripts.js +129 -0
- package/src/runtime/37-next.js +150 -0
- package/src/runtime/38-observers.js +252 -0
- package/src/runtime/40-input.js +795 -0
- package/src/runtime/45-hover.js +78 -0
- package/src/runtime/50-engine.js +341 -0
- package/src/runtime/60-preview.js +437 -0
- package/src/runtime/65-timeline.js +289 -0
- package/src/runtime/66-activity.js +114 -0
- package/src/runtime/67-csssource.js +226 -0
- package/src/runtime/70-boot.js +460 -0
- package/src/server/api.js +285 -0
- package/src/server/child.js +174 -0
- package/src/server/detect.js +167 -0
- package/src/server/front.js +647 -0
- package/src/server/mcp.js +332 -0
- package/src/shell/00-state.js +85 -0
- package/src/shell/05-api.js +136 -0
- package/src/shell/10-dock.js +754 -0
- package/src/shell/12-checkpoint.js +121 -0
- package/src/shell/15-session.js +291 -0
- package/src/shell/20-timeline.js +734 -0
- package/src/shell/25-input.js +537 -0
- package/src/shell/30-notes.js +870 -0
- package/src/shell/40-code.js +80 -0
- package/src/shell/90-handle.js +15 -0
- package/src/shell/shell.css +295 -0
- package/src/shell/shell.html +59 -0
- package/types/client.d.ts +73 -0
- 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
|
+
}
|