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,789 @@
|
|
|
1
|
+
// Network: fetch (streamed), XMLHttpRequest, EventSource and WebSocket are
|
|
2
|
+
// recorded as they arrive and replayed at the same virtual moments, without
|
|
3
|
+
// touching the network.
|
|
4
|
+
//
|
|
5
|
+
// - fetch bodies are teed: the app reads a live stream while each chunk is
|
|
6
|
+
// stored (base64) with the moment it arrived, so a streamed reply (an LLM
|
|
7
|
+
// answer) replays token by token.
|
|
8
|
+
// - At the live edge while paused, arrivals are held and delivered when time
|
|
9
|
+
// moves again, so pausing freezes streams too.
|
|
10
|
+
// - Only complete exchanges are replayed. One still in flight when the
|
|
11
|
+
// recording was cut waits until its frame is on show at the live edge, then
|
|
12
|
+
// goes to the network for real, once (F52); so does anything a frame built
|
|
13
|
+
// behind (or a checkpoint) asks that the recording can't answer. In the frame
|
|
14
|
+
// on show, the view-only past just goes to the network, unrecorded.
|
|
15
|
+
// - The dev server's own traffic (HMR sockets, overlays; by call stack or by
|
|
16
|
+
// URL, see isExemptUrl) goes straight to the real API: never recorded, held
|
|
17
|
+
// or replayed. Its sockets are kept in `devSockets`.
|
|
18
|
+
// - Nothing here forks a timeline; only the dock's + does.
|
|
19
|
+
|
|
20
|
+
const NET = { fetch: "fetches", xhr: "xhrs", sse: "sses", ws: "sockets" }
|
|
21
|
+
const used = { fetches: new Set(), xhrs: new Set(), sses: new Set(), sockets: new Set() }
|
|
22
|
+
// Replays in progress: `${list}:${i}` -> handler receiving recorded net events.
|
|
23
|
+
const replaying = new Map()
|
|
24
|
+
const earlyNet = new Map() // events that arrived before their request was made
|
|
25
|
+
|
|
26
|
+
const listOf = (name) => rec[name] || (rec[name] = [])
|
|
27
|
+
|
|
28
|
+
// ---- helpers ------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
function toB64(bytes) {
|
|
31
|
+
let s = ""
|
|
32
|
+
for (let i = 0; i < bytes.length; i += 0x8000) s += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000))
|
|
33
|
+
return btoa(s)
|
|
34
|
+
}
|
|
35
|
+
function fromB64(b64) {
|
|
36
|
+
const s = atob(b64)
|
|
37
|
+
const out = new Uint8Array(s.length)
|
|
38
|
+
for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i)
|
|
39
|
+
return out
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Short stable hash for request bodies (FNV-1a).
|
|
43
|
+
function hashText(s) {
|
|
44
|
+
let h = 0x811c9dc5
|
|
45
|
+
for (let i = 0; i < s.length; i++) h = Math.imul(h ^ s.charCodeAt(i), 0x01000193)
|
|
46
|
+
return (h >>> 0).toString(36)
|
|
47
|
+
}
|
|
48
|
+
function bodyHash(body) {
|
|
49
|
+
try {
|
|
50
|
+
if (body == null) return ""
|
|
51
|
+
if (typeof body === "string") return hashText(body)
|
|
52
|
+
if (body instanceof URLSearchParams) return hashText(body.toString())
|
|
53
|
+
if (body instanceof ArrayBuffer) return hashText(toB64(new Uint8Array(body)))
|
|
54
|
+
if (ArrayBuffer.isView(body)) return hashText(toB64(new Uint8Array(body.buffer, body.byteOffset, body.byteLength)))
|
|
55
|
+
} catch {}
|
|
56
|
+
return "?" // FormData, Blob, streams: not hashed; matched by order
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Request headers that decide which answer a URL gets (Next's router asks the
|
|
60
|
+
// same URL for a prefetch, a page's RSC payload or a server action): part of
|
|
61
|
+
// the key, so a prefetch and a navigation never swap answers on replay.
|
|
62
|
+
const KEY_HEADERS = ["rsc", "next-router-prefetch", "next-router-segment-prefetch", "next-action"]
|
|
63
|
+
function urlKey(url, method, headers) {
|
|
64
|
+
const u = new URL(String(url), location.href)
|
|
65
|
+
// Our frame marker, and Next's `_rsc` (a cache-busting hash of the request's
|
|
66
|
+
// headers, which are in the key themselves), aren't part of what's asked.
|
|
67
|
+
for (const p of ["__wb", "_rsc"]) if (u.searchParams.has(p)) u.searchParams.delete(p)
|
|
68
|
+
let key = `${(method || "GET").toUpperCase()} ${u.origin === location.origin ? u.pathname + u.search : u.href}`
|
|
69
|
+
if (headers) for (const h of KEY_HEADERS) if (headers.has(h)) key += ` ${h}=${headers.get(h)}`
|
|
70
|
+
return key
|
|
71
|
+
}
|
|
72
|
+
// A request's headers as a Headers object (fetch's init wins over a Request's).
|
|
73
|
+
function headersOf(input, init) {
|
|
74
|
+
try {
|
|
75
|
+
if (init && init.headers) return new Headers(init.headers)
|
|
76
|
+
if (typeof Request !== "undefined" && input instanceof Request) return input.headers
|
|
77
|
+
} catch {}
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
// The body hash, plus Next's router state (which page it's coming from): the
|
|
81
|
+
// exact one is preferred, any other still matches in order.
|
|
82
|
+
function requestHash(body, headers) {
|
|
83
|
+
const tree = headers && headers.get("next-router-state-tree")
|
|
84
|
+
return bodyHash(body) + (tree ? "~" + hashText(tree) : "")
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Find a recorded, complete, unused exchange: same key and body first, then
|
|
88
|
+
// same key in order (the body can change after a code edit).
|
|
89
|
+
function match(list, key, hash) {
|
|
90
|
+
const arr = rec[list] || []
|
|
91
|
+
const complete = (e) => e.done || (!e.chunks && (e.body != null || e.error != null)) // older recordings
|
|
92
|
+
const ok = (e, j) => e && complete(e) && !used[list].has(j) && e.key === key
|
|
93
|
+
let i = arr.findIndex((e, j) => ok(e, j) && (e.hash || "") === (hash || ""))
|
|
94
|
+
if (i < 0) i = arr.findIndex((e, j) => ok(e, j))
|
|
95
|
+
if (i >= 0) used[list].add(i)
|
|
96
|
+
return i
|
|
97
|
+
}
|
|
98
|
+
// Was this request in flight when the recording was cut (recorded, never
|
|
99
|
+
// answered)? Then it waits for the live edge in any frame (see defer below).
|
|
100
|
+
function cutInFlight(list, key) {
|
|
101
|
+
const arr = rec[list] || []
|
|
102
|
+
const j = arr.findIndex((e, k) => e && e.key === key && !e.done && !(e.body != null || e.error != null) && !used[list].has(k))
|
|
103
|
+
if (j >= 0) used[list].add(j)
|
|
104
|
+
return j >= 0
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Live arrivals: delivered now, or (at the live edge while paused) held until
|
|
108
|
+
// time moves again, and recorded at the moment they reach the app.
|
|
109
|
+
const held = []
|
|
110
|
+
function arrive(fn) {
|
|
111
|
+
if (!clock.playing && !hasFuture() && !clock.seeking) held.push(fn)
|
|
112
|
+
else fn()
|
|
113
|
+
}
|
|
114
|
+
function releaseHeld() {
|
|
115
|
+
while (held.length && clock.playing) held.shift()()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function netEvent(ev) {
|
|
119
|
+
recordEvent(ev)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---- frames built behind don't go to the network ---------------------------------
|
|
123
|
+
// A frame built behind the one on show (or a checkpoint) replays the past.
|
|
124
|
+
// A request there that the recording has no complete answer for (the
|
|
125
|
+
// recording was cut while it was in flight) would reach the server again from
|
|
126
|
+
// every such frame, ones that change data included. It waits instead, and goes
|
|
127
|
+
// out for real (recorded) once this frame is on show at the live edge. So does
|
|
128
|
+
// one cut in flight that the frame on show makes again, playing the past.
|
|
129
|
+
const deferred = []
|
|
130
|
+
const hiddenFrame = () => !!shell && !onShow()
|
|
131
|
+
function defer(go) {
|
|
132
|
+
const d = { go, dead: false }
|
|
133
|
+
deferred.push(d)
|
|
134
|
+
return d
|
|
135
|
+
}
|
|
136
|
+
function releaseDeferred() {
|
|
137
|
+
if (!deferred.length || hasFuture() || clock.seeking || !onShow()) return
|
|
138
|
+
for (const d of deferred.splice(0)) {
|
|
139
|
+
if (d.dead) continue
|
|
140
|
+
d.dead = true
|
|
141
|
+
d.go()
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function deferFetch(input, init, key, hash, signal, headers) {
|
|
145
|
+
return new Promise((resolve, reject) => {
|
|
146
|
+
const d = defer(() => liveFetch(input, init, key, hash, !hasFuture(), headers).then(resolve, reject))
|
|
147
|
+
if (!signal) return
|
|
148
|
+
const abort = () => {
|
|
149
|
+
if (d.dead) return
|
|
150
|
+
d.dead = true
|
|
151
|
+
reject(new DOMException("The operation was aborted.", "AbortError"))
|
|
152
|
+
}
|
|
153
|
+
if (signal.aborted) return abort()
|
|
154
|
+
signal.addEventListener("abort", abort, { once: true })
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// The dev server's own sockets (HMR), live and unrecorded: 37-next.js talks to them.
|
|
159
|
+
const devSockets = new Set()
|
|
160
|
+
|
|
161
|
+
// Recorded net events during replay.
|
|
162
|
+
function deliverNet(ev) {
|
|
163
|
+
const key = `${ev.list}:${ev.i}`
|
|
164
|
+
const h = replaying.get(key)
|
|
165
|
+
if (h) return h(ev)
|
|
166
|
+
const q = earlyNet.get(key) || []
|
|
167
|
+
q.push(ev)
|
|
168
|
+
earlyNet.set(key, q)
|
|
169
|
+
}
|
|
170
|
+
function onReplay(list, i, handler) {
|
|
171
|
+
const key = `${list}:${i}`
|
|
172
|
+
replaying.set(key, handler)
|
|
173
|
+
const q = earlyNet.get(key)
|
|
174
|
+
if (q) {
|
|
175
|
+
earlyNet.delete(key)
|
|
176
|
+
for (const ev of q) handler(ev)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const doneReplay = (list, i) => replaying.delete(`${list}:${i}`)
|
|
180
|
+
|
|
181
|
+
// Cutting the future at a fork: unused exchanges go, and replays still
|
|
182
|
+
// delivering (a stream half-way through) get the rest of their recording now,
|
|
183
|
+
// recorded into the new timeline, so nothing is left hanging.
|
|
184
|
+
function netFork(cutEvents) {
|
|
185
|
+
for (const list of Object.values(NET)) {
|
|
186
|
+
if (rec[list]) rec[list] = rec[list].map((e, j) => (used[list].has(j) ? e : null))
|
|
187
|
+
}
|
|
188
|
+
for (const ev of cutEvents) {
|
|
189
|
+
if (ev.type !== "net") continue
|
|
190
|
+
const h = replaying.get(`${ev.list}:${ev.i}`)
|
|
191
|
+
if (h) {
|
|
192
|
+
h(ev)
|
|
193
|
+
recordEvent({ ...ev })
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---- fetch -------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
function netError(e) {
|
|
201
|
+
if (e && e.name === "AbortError") return new DOMException(e.message || "The operation was aborted.", "AbortError")
|
|
202
|
+
return new TypeError((e && e.message) || "Failed to fetch")
|
|
203
|
+
}
|
|
204
|
+
const NO_BODY = [101, 204, 205, 304]
|
|
205
|
+
|
|
206
|
+
function makeResponse(entry, body) {
|
|
207
|
+
const res = new Response(NO_BODY.includes(entry.status) ? null : body, { status: entry.status, statusText: entry.statusText, headers: entry.headers })
|
|
208
|
+
try {
|
|
209
|
+
Object.defineProperty(res, "url", { value: entry.url || "" })
|
|
210
|
+
Object.defineProperty(res, "redirected", { value: !!entry.redirected })
|
|
211
|
+
} catch {}
|
|
212
|
+
return res
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function replayFetch(i, signal, headers) {
|
|
216
|
+
const entry = rec.fetches[i]
|
|
217
|
+
// (Next 16 waits for debug data about this request over its HMR socket.)
|
|
218
|
+
const ended = () => nextReplayEnd(headers)
|
|
219
|
+
return new Promise((resolve, reject) => {
|
|
220
|
+
let controller = null
|
|
221
|
+
let settled = false
|
|
222
|
+
const abort = () => {
|
|
223
|
+
const err = new DOMException("The operation was aborted.", "AbortError")
|
|
224
|
+
if (!settled) reject(err)
|
|
225
|
+
else if (controller) {
|
|
226
|
+
try {
|
|
227
|
+
controller.error(err)
|
|
228
|
+
} catch {}
|
|
229
|
+
}
|
|
230
|
+
settled = true
|
|
231
|
+
doneReplay("fetches", i)
|
|
232
|
+
}
|
|
233
|
+
if (signal) {
|
|
234
|
+
if (signal.aborted) return abort()
|
|
235
|
+
signal.addEventListener("abort", abort, { once: true })
|
|
236
|
+
}
|
|
237
|
+
// Old recordings kept the whole body as text on one "fetch" event.
|
|
238
|
+
if (entry.body != null && !entry.chunks) {
|
|
239
|
+
onReplay("fetches", i, () => {
|
|
240
|
+
settled = true
|
|
241
|
+
doneReplay("fetches", i)
|
|
242
|
+
nextReplayStart(entry, headers)
|
|
243
|
+
ended()
|
|
244
|
+
entry.error ? reject(netError(entry.error)) : resolve(makeResponse(entry, entry.body))
|
|
245
|
+
})
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
onReplay("fetches", i, (ev) => {
|
|
249
|
+
if (ev.kind === "error") {
|
|
250
|
+
doneReplay("fetches", i)
|
|
251
|
+
ended()
|
|
252
|
+
if (!settled) reject(netError(entry.error))
|
|
253
|
+
else if (controller) controller.error(netError(entry.error))
|
|
254
|
+
settled = true
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
if (ev.kind === "head") {
|
|
258
|
+
settled = true
|
|
259
|
+
nextReplayStart(entry, headers)
|
|
260
|
+
const stream = new ReadableStream({ start: (c) => (controller = c) })
|
|
261
|
+
resolve(makeResponse(entry, stream))
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
if (ev.kind === "chunk" && controller) {
|
|
265
|
+
try {
|
|
266
|
+
controller.enqueue(fromB64(entry.chunks[ev.n]))
|
|
267
|
+
} catch {}
|
|
268
|
+
}
|
|
269
|
+
if (ev.kind === "end") {
|
|
270
|
+
doneReplay("fetches", i)
|
|
271
|
+
ended()
|
|
272
|
+
try {
|
|
273
|
+
controller && controller.close()
|
|
274
|
+
} catch {}
|
|
275
|
+
}
|
|
276
|
+
})
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function liveFetch(input, init, key, hash, record, headers) {
|
|
281
|
+
const list = listOf("fetches")
|
|
282
|
+
const idx = record ? list.length : -1
|
|
283
|
+
const entry = { key, hash, t0: clock.now, chunks: [] }
|
|
284
|
+
if (record) {
|
|
285
|
+
list.push(entry)
|
|
286
|
+
used.fetches.add(idx)
|
|
287
|
+
nextLiveRequest(entry, headers)
|
|
288
|
+
}
|
|
289
|
+
const ev = (kind, extra) => record && netEvent({ type: "net", list: "fetches", i: idx, kind, ...extra })
|
|
290
|
+
;[input, init] = nextLiveInit(input, init) // (a kept Next page: its own socket's id)
|
|
291
|
+
return new Promise((resolve, reject) => {
|
|
292
|
+
real.fetch(input, init).then(
|
|
293
|
+
(res) => {
|
|
294
|
+
arrive(() => {
|
|
295
|
+
entry.status = res.status
|
|
296
|
+
entry.statusText = res.statusText
|
|
297
|
+
entry.headers = [...res.headers]
|
|
298
|
+
entry.url = res.url
|
|
299
|
+
entry.redirected = res.redirected
|
|
300
|
+
ev("head")
|
|
301
|
+
if (!res.body || NO_BODY.includes(res.status)) {
|
|
302
|
+
entry.done = true
|
|
303
|
+
ev("end")
|
|
304
|
+
return resolve(makeResponse(entry, null))
|
|
305
|
+
}
|
|
306
|
+
// The body is read as it arrives, whether or not the app reads it:
|
|
307
|
+
// a response the app only checks (res.ok) still gets recorded whole,
|
|
308
|
+
// so replays never have to go back to the network for it.
|
|
309
|
+
const reader = res.body.getReader()
|
|
310
|
+
let appStream = null
|
|
311
|
+
let cancelled = false
|
|
312
|
+
const stream = new ReadableStream({
|
|
313
|
+
start(controller) {
|
|
314
|
+
appStream = controller
|
|
315
|
+
},
|
|
316
|
+
cancel(reason) {
|
|
317
|
+
cancelled = true // the app is done with it; the recording keeps reading
|
|
318
|
+
},
|
|
319
|
+
})
|
|
320
|
+
const pump = () =>
|
|
321
|
+
reader.read().then(
|
|
322
|
+
({ done, value }) =>
|
|
323
|
+
arrive(() => {
|
|
324
|
+
if (done) {
|
|
325
|
+
entry.done = true
|
|
326
|
+
ev("end")
|
|
327
|
+
if (!cancelled) {
|
|
328
|
+
try {
|
|
329
|
+
appStream.close()
|
|
330
|
+
} catch {}
|
|
331
|
+
}
|
|
332
|
+
return
|
|
333
|
+
}
|
|
334
|
+
entry.chunks.push(toB64(value))
|
|
335
|
+
ev("chunk", { n: entry.chunks.length - 1 })
|
|
336
|
+
if (!cancelled) {
|
|
337
|
+
try {
|
|
338
|
+
appStream.enqueue(value)
|
|
339
|
+
} catch {}
|
|
340
|
+
}
|
|
341
|
+
pump()
|
|
342
|
+
}),
|
|
343
|
+
(err) =>
|
|
344
|
+
arrive(() => {
|
|
345
|
+
entry.error = { name: err && err.name, message: err && err.message }
|
|
346
|
+
entry.done = true
|
|
347
|
+
ev("error")
|
|
348
|
+
if (!cancelled) {
|
|
349
|
+
try {
|
|
350
|
+
appStream.error(err)
|
|
351
|
+
} catch {}
|
|
352
|
+
}
|
|
353
|
+
}),
|
|
354
|
+
)
|
|
355
|
+
pump()
|
|
356
|
+
resolve(makeResponse(entry, stream))
|
|
357
|
+
})
|
|
358
|
+
},
|
|
359
|
+
(err) =>
|
|
360
|
+
arrive(() => {
|
|
361
|
+
entry.error = { name: err && err.name, message: err && err.message }
|
|
362
|
+
entry.done = true
|
|
363
|
+
ev("error")
|
|
364
|
+
reject(err) // the app gets the real error (AbortError stays AbortError)
|
|
365
|
+
}),
|
|
366
|
+
)
|
|
367
|
+
})
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
W.fetch = function (input, init) {
|
|
371
|
+
if (isExempt()) return real.fetch(input, init)
|
|
372
|
+
let key
|
|
373
|
+
let hash
|
|
374
|
+
let headers
|
|
375
|
+
try {
|
|
376
|
+
const url = typeof input === "string" || input instanceof URL ? input : input.url
|
|
377
|
+
if (isExemptUrl(url)) return real.fetch(input, init)
|
|
378
|
+
headers = headersOf(input, init)
|
|
379
|
+
// Next's router refetching the page after a server component edit: the dev
|
|
380
|
+
// server's traffic too (F77), but with its socket's id like a live request.
|
|
381
|
+
if (nextHmrRefresh(headers)) return real.fetch(...nextLiveInit(input, init))
|
|
382
|
+
key = urlKey(url, (init && init.method) || (input && input.method), headers)
|
|
383
|
+
hash = requestHash(init && init.body, headers)
|
|
384
|
+
} catch {
|
|
385
|
+
return real.fetch(input, init)
|
|
386
|
+
}
|
|
387
|
+
const signal = (init && init.signal) || (input && input.signal)
|
|
388
|
+
if (hasFuture()) {
|
|
389
|
+
const i = match("fetches", key, hash)
|
|
390
|
+
if (i >= 0) return replayFetch(i, signal, headers)
|
|
391
|
+
// Cut mid-flight, or (in a frame built behind) not in the recording: it
|
|
392
|
+
// waits until the frame is on show at the live edge. Anything else in the
|
|
393
|
+
// view-only past just goes to the network, unrecorded.
|
|
394
|
+
if (hiddenFrame() || cutInFlight("fetches", key)) return deferFetch(input, init, key, hash, signal, headers)
|
|
395
|
+
return liveFetch(input, init, key, hash, false, headers)
|
|
396
|
+
}
|
|
397
|
+
return liveFetch(input, init, key, hash, true, headers)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ---- XMLHttpRequest -------------------------------------------------------------------
|
|
401
|
+
|
|
402
|
+
const RealXHR = W.XMLHttpRequest
|
|
403
|
+
const XHR_EVENTS = ["readystatechange", "loadstart", "progress", "load", "loadend", "error", "abort", "timeout"]
|
|
404
|
+
|
|
405
|
+
class RetakeXHR extends EventTarget {
|
|
406
|
+
constructor() {
|
|
407
|
+
super()
|
|
408
|
+
this.readyState = 0
|
|
409
|
+
this.status = 0
|
|
410
|
+
this.statusText = ""
|
|
411
|
+
this.responseType = ""
|
|
412
|
+
this.responseURL = ""
|
|
413
|
+
this.timeout = 0
|
|
414
|
+
this.withCredentials = false
|
|
415
|
+
this.upload = new EventTarget()
|
|
416
|
+
this._headers = []
|
|
417
|
+
this._resHeaders = ""
|
|
418
|
+
this._response = null
|
|
419
|
+
this._text = ""
|
|
420
|
+
for (const t of XHR_EVENTS) this["on" + t] = null
|
|
421
|
+
}
|
|
422
|
+
get response() {
|
|
423
|
+
return this.responseType === "" || this.responseType === "text" ? this._text : this._response
|
|
424
|
+
}
|
|
425
|
+
get responseText() {
|
|
426
|
+
if (this.responseType !== "" && this.responseType !== "text") throw new DOMException("responseText is only for text responses", "InvalidStateError")
|
|
427
|
+
return this._text
|
|
428
|
+
}
|
|
429
|
+
get responseXML() {
|
|
430
|
+
return null
|
|
431
|
+
}
|
|
432
|
+
open(method, url, async = true) {
|
|
433
|
+
this._method = String(method || "GET").toUpperCase()
|
|
434
|
+
this._url = url
|
|
435
|
+
this._async = async !== false
|
|
436
|
+
this._exempt = isExempt() || isExemptUrl(url) // the dev server's own: straight through
|
|
437
|
+
this._fire("readystatechange", 1)
|
|
438
|
+
}
|
|
439
|
+
setRequestHeader(k, v) {
|
|
440
|
+
this._headers.push([k, v])
|
|
441
|
+
}
|
|
442
|
+
overrideMimeType() {}
|
|
443
|
+
getAllResponseHeaders() {
|
|
444
|
+
return this.readyState >= 2 ? this._resHeaders : ""
|
|
445
|
+
}
|
|
446
|
+
getResponseHeader(name) {
|
|
447
|
+
if (this.readyState < 2) return null
|
|
448
|
+
const n = String(name).toLowerCase()
|
|
449
|
+
const vals = this._resHeaders
|
|
450
|
+
.split(/\r?\n/)
|
|
451
|
+
.filter(Boolean)
|
|
452
|
+
.map((l) => [l.slice(0, l.indexOf(":")).trim().toLowerCase(), l.slice(l.indexOf(":") + 1).trim()])
|
|
453
|
+
.filter(([k]) => k === n)
|
|
454
|
+
.map(([, v]) => v)
|
|
455
|
+
return vals.length ? vals.join(", ") : null
|
|
456
|
+
}
|
|
457
|
+
abort() {
|
|
458
|
+
this._aborted = true
|
|
459
|
+
if (this._deferred) this._deferred.dead = true
|
|
460
|
+
if (this._real) this._real.abort()
|
|
461
|
+
if (this.readyState > 0 && this.readyState < 4) {
|
|
462
|
+
this._fire("readystatechange", 4)
|
|
463
|
+
this._fire("abort")
|
|
464
|
+
this._fire("loadend")
|
|
465
|
+
}
|
|
466
|
+
this.readyState = 0
|
|
467
|
+
if (this._list && this._i != null) doneReplay(this._list, this._i)
|
|
468
|
+
}
|
|
469
|
+
_fire(type, state) {
|
|
470
|
+
if (state != null) this.readyState = state
|
|
471
|
+
const ev = new ProgressEvent(type)
|
|
472
|
+
const prop = this["on" + type]
|
|
473
|
+
if (typeof prop === "function") safeCall(prop.bind(this), [ev])
|
|
474
|
+
this.dispatchEvent(ev)
|
|
475
|
+
}
|
|
476
|
+
_apply(entry) {
|
|
477
|
+
this.status = entry.status
|
|
478
|
+
this.statusText = entry.statusText || ""
|
|
479
|
+
this.responseURL = entry.url || ""
|
|
480
|
+
this._resHeaders = entry.headers || ""
|
|
481
|
+
this._fire("readystatechange", 2)
|
|
482
|
+
this._fire("readystatechange", 3)
|
|
483
|
+
if (entry.b64 != null) {
|
|
484
|
+
const bytes = fromB64(entry.b64)
|
|
485
|
+
const type = this.responseType
|
|
486
|
+
if (type === "arraybuffer") this._response = bytes.buffer
|
|
487
|
+
else if (type === "blob") this._response = new Blob([bytes], { type: this.getResponseHeader("content-type") || "" })
|
|
488
|
+
else {
|
|
489
|
+
this._text = new TextDecoder().decode(bytes)
|
|
490
|
+
if (type === "json") {
|
|
491
|
+
try {
|
|
492
|
+
this._response = JSON.parse(this._text)
|
|
493
|
+
} catch {
|
|
494
|
+
this._response = null
|
|
495
|
+
}
|
|
496
|
+
} else this._response = this._text
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
this._fire("readystatechange", 4)
|
|
500
|
+
this._fire("load")
|
|
501
|
+
this._fire("loadend")
|
|
502
|
+
}
|
|
503
|
+
_fail(kind) {
|
|
504
|
+
this.status = 0
|
|
505
|
+
this._fire("readystatechange", 4)
|
|
506
|
+
this._fire(kind || "error")
|
|
507
|
+
this._fire("loadend")
|
|
508
|
+
}
|
|
509
|
+
send(body) {
|
|
510
|
+
let headers = null
|
|
511
|
+
try {
|
|
512
|
+
headers = new Headers(this._headers)
|
|
513
|
+
} catch {}
|
|
514
|
+
const key = urlKey(this._url, this._method, headers)
|
|
515
|
+
const hash = requestHash(body, headers)
|
|
516
|
+
this._fire("loadstart")
|
|
517
|
+
if (this._exempt) return this._live(body, key, hash, false, true)
|
|
518
|
+
if (hasFuture()) {
|
|
519
|
+
const i = match("xhrs", key, hash)
|
|
520
|
+
if (i >= 0) {
|
|
521
|
+
this._list = "xhrs"
|
|
522
|
+
this._i = i
|
|
523
|
+
onReplay("xhrs", i, () => {
|
|
524
|
+
doneReplay("xhrs", i)
|
|
525
|
+
if (this._aborted) return
|
|
526
|
+
const e = rec.xhrs[i]
|
|
527
|
+
e.fail ? this._fail(e.fail) : this._apply(e)
|
|
528
|
+
})
|
|
529
|
+
return
|
|
530
|
+
}
|
|
531
|
+
if (hiddenFrame() || cutInFlight("xhrs", key)) {
|
|
532
|
+
this._deferred = defer(() => this._live(body, key, hash, !hasFuture()))
|
|
533
|
+
return
|
|
534
|
+
}
|
|
535
|
+
return this._live(body, key, hash, false)
|
|
536
|
+
}
|
|
537
|
+
return this._live(body, key, hash, true)
|
|
538
|
+
}
|
|
539
|
+
// raw: the dev server's own request, delivered as it comes (never held).
|
|
540
|
+
_live(body, key, hash, record, raw) {
|
|
541
|
+
const x = (this._real = new RealXHR())
|
|
542
|
+
x.open(this._method, this._url, this._async)
|
|
543
|
+
if (this._async) x.responseType = "arraybuffer"
|
|
544
|
+
x.timeout = this.timeout
|
|
545
|
+
x.withCredentials = this.withCredentials
|
|
546
|
+
for (const [k, v] of this._headers) x.setRequestHeader(k, v)
|
|
547
|
+
for (const t of ["progress", "load", "loadend", "error", "abort", "timeout", "loadstart"]) {
|
|
548
|
+
x.upload.addEventListener(t, (e) => this.upload.dispatchEvent(new ProgressEvent(t, e)))
|
|
549
|
+
}
|
|
550
|
+
const list = listOf("xhrs")
|
|
551
|
+
const idx = record ? list.length : -1
|
|
552
|
+
const entry = { key, hash, t0: clock.now }
|
|
553
|
+
if (record) {
|
|
554
|
+
list.push(entry)
|
|
555
|
+
used.xhrs.add(idx)
|
|
556
|
+
}
|
|
557
|
+
const finish = (fail) =>
|
|
558
|
+
(raw ? (fn) => fn() : arrive)(() => {
|
|
559
|
+
if (this._aborted) return
|
|
560
|
+
if (fail) entry.fail = fail
|
|
561
|
+
else {
|
|
562
|
+
entry.status = x.status
|
|
563
|
+
entry.statusText = x.statusText
|
|
564
|
+
entry.headers = x.getAllResponseHeaders()
|
|
565
|
+
entry.url = x.responseURL
|
|
566
|
+
entry.b64 = !this._async ? toB64(new TextEncoder().encode(x.responseText || "")) : x.response ? toB64(new Uint8Array(x.response)) : ""
|
|
567
|
+
}
|
|
568
|
+
entry.done = true
|
|
569
|
+
if (record) netEvent({ type: "net", list: "xhrs", i: idx, kind: "done" })
|
|
570
|
+
fail ? this._fail(fail) : this._apply(entry)
|
|
571
|
+
})
|
|
572
|
+
x.onload = () => finish(null)
|
|
573
|
+
x.onerror = () => finish("error")
|
|
574
|
+
x.ontimeout = () => finish("timeout")
|
|
575
|
+
x.send(body)
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
for (const [k, v] of Object.entries({ UNSENT: 0, OPENED: 1, HEADERS_RECEIVED: 2, LOADING: 3, DONE: 4 })) {
|
|
579
|
+
RetakeXHR[k] = v
|
|
580
|
+
RetakeXHR.prototype[k] = v
|
|
581
|
+
}
|
|
582
|
+
W.XMLHttpRequest = RetakeXHR
|
|
583
|
+
|
|
584
|
+
// ---- EventSource ------------------------------------------------------------------------
|
|
585
|
+
|
|
586
|
+
const RealES = W.EventSource
|
|
587
|
+
if (RealES) {
|
|
588
|
+
class RetakeEventSource extends EventTarget {
|
|
589
|
+
constructor(url, opts) {
|
|
590
|
+
if (isExempt() || isExemptUrl(url)) return new RealES(url, opts)
|
|
591
|
+
super()
|
|
592
|
+
this.url = new URL(String(url), location.href).href
|
|
593
|
+
this.withCredentials = !!(opts && opts.withCredentials)
|
|
594
|
+
this.readyState = 0
|
|
595
|
+
this.onopen = this.onmessage = this.onerror = null
|
|
596
|
+
this._types = new Set(["message", "open", "error"])
|
|
597
|
+
const key = urlKey(url, "GET")
|
|
598
|
+
if (hasFuture()) {
|
|
599
|
+
const i = match("sses", key, "")
|
|
600
|
+
if (i >= 0) {
|
|
601
|
+
this._i = i
|
|
602
|
+
onReplay("sses", i, (ev) => this._play(rec.sses[i].msgs[ev.n]))
|
|
603
|
+
return
|
|
604
|
+
}
|
|
605
|
+
if (hiddenFrame() || cutInFlight("sses", key)) {
|
|
606
|
+
this._deferred = defer(() => this._live(url, opts, key, !hasFuture()))
|
|
607
|
+
return
|
|
608
|
+
}
|
|
609
|
+
this._live(url, opts, key, false)
|
|
610
|
+
return
|
|
611
|
+
}
|
|
612
|
+
this._live(url, opts, key, true)
|
|
613
|
+
}
|
|
614
|
+
get CONNECTING() {
|
|
615
|
+
return 0
|
|
616
|
+
}
|
|
617
|
+
get OPEN() {
|
|
618
|
+
return 1
|
|
619
|
+
}
|
|
620
|
+
get CLOSED() {
|
|
621
|
+
return 2
|
|
622
|
+
}
|
|
623
|
+
_emit(m) {
|
|
624
|
+
const ev = m.type === "open" || m.type === "error" ? new Event(m.type) : new MessageEvent(m.type, { data: m.data, lastEventId: m.id || "", origin: location.origin })
|
|
625
|
+
const prop = this["on" + m.type]
|
|
626
|
+
if (typeof prop === "function") safeCall(prop.bind(this), [ev])
|
|
627
|
+
this.dispatchEvent(ev)
|
|
628
|
+
}
|
|
629
|
+
_play(m) {
|
|
630
|
+
if (this.readyState === 2 || !m) return
|
|
631
|
+
if (m.type === "open") this.readyState = 1
|
|
632
|
+
if (m.type === "error") this.readyState = m.closed ? 2 : 0
|
|
633
|
+
this._emit(m)
|
|
634
|
+
}
|
|
635
|
+
addEventListener(type, fn, o) {
|
|
636
|
+
if (!this._types.has(type)) {
|
|
637
|
+
this._types.add(type)
|
|
638
|
+
if (this._real) this._wire(type)
|
|
639
|
+
}
|
|
640
|
+
return super.addEventListener(type, fn, o)
|
|
641
|
+
}
|
|
642
|
+
_wire(type) {
|
|
643
|
+
this._real.addEventListener(type, (e) => this._got({ type, data: e.data, id: e.lastEventId }))
|
|
644
|
+
}
|
|
645
|
+
_live(url, opts, key, record) {
|
|
646
|
+
const es = (this._real = new RealES(url, opts))
|
|
647
|
+
for (const type of this._types) if (type !== "message" && type !== "open" && type !== "error") this._wire(type)
|
|
648
|
+
const list = listOf("sses")
|
|
649
|
+
this._record = record
|
|
650
|
+
this._entry = { key, t0: clock.now, msgs: [] }
|
|
651
|
+
if (record) {
|
|
652
|
+
this._idx = list.length
|
|
653
|
+
list.push(this._entry)
|
|
654
|
+
used.sses.add(this._idx)
|
|
655
|
+
}
|
|
656
|
+
es.onopen = () => this._got({ type: "open" })
|
|
657
|
+
es.onmessage = (e) => this._got({ type: "message", data: e.data, id: e.lastEventId })
|
|
658
|
+
es.onerror = () => this._got({ type: "error", closed: es.readyState === 2 })
|
|
659
|
+
}
|
|
660
|
+
_got(m) {
|
|
661
|
+
arrive(() => {
|
|
662
|
+
if (this.readyState === 2) return
|
|
663
|
+
const e = this._entry
|
|
664
|
+
e.msgs.push(m)
|
|
665
|
+
if (m.type === "error" && m.closed) e.done = true
|
|
666
|
+
if (this._record) netEvent({ type: "net", list: "sses", i: this._idx, n: e.msgs.length - 1 })
|
|
667
|
+
this._play(m)
|
|
668
|
+
})
|
|
669
|
+
}
|
|
670
|
+
close() {
|
|
671
|
+
this.readyState = 2
|
|
672
|
+
if (this._deferred) this._deferred.dead = true
|
|
673
|
+
if (this._real) this._real.close()
|
|
674
|
+
if (this._entry) this._entry.done = true
|
|
675
|
+
if (this._i != null) doneReplay("sses", this._i)
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
W.EventSource = RetakeEventSource
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// ---- WebSocket -------------------------------------------------------------------------
|
|
682
|
+
|
|
683
|
+
const RealWS = W.WebSocket
|
|
684
|
+
if (RealWS) {
|
|
685
|
+
class RetakeWebSocket extends EventTarget {
|
|
686
|
+
constructor(url, protocols) {
|
|
687
|
+
// The dev server's own socket (Vite's or Next's HMR client) stays real and unheld.
|
|
688
|
+
if (isExempt() || isExemptUrl(url) || [].concat(protocols || []).some((p) => p === "vite-hmr" || p === "vite-ping")) {
|
|
689
|
+
const ws = new RealWS(nextSocketUrl(url), protocols)
|
|
690
|
+
devSockets.add(ws)
|
|
691
|
+
ws.addEventListener("open", () => (nextDocReplay(), flushNext()))
|
|
692
|
+
ws.addEventListener("message", nextDevMessage)
|
|
693
|
+
ws.addEventListener("close", () => devSockets.delete(ws))
|
|
694
|
+
return ws
|
|
695
|
+
}
|
|
696
|
+
super()
|
|
697
|
+
this.url = new URL(String(url), location.href).href
|
|
698
|
+
this.readyState = 0
|
|
699
|
+
this.protocol = ""
|
|
700
|
+
this.extensions = ""
|
|
701
|
+
this.bufferedAmount = 0
|
|
702
|
+
this.binaryType = "blob"
|
|
703
|
+
this.onopen = this.onmessage = this.onerror = this.onclose = null
|
|
704
|
+
const key = `WS ${this.url}`
|
|
705
|
+
if (hasFuture()) {
|
|
706
|
+
const i = match("sockets", key, "")
|
|
707
|
+
if (i >= 0) {
|
|
708
|
+
this._i = i
|
|
709
|
+
onReplay("sockets", i, (ev) => this._play(rec.sockets[i].msgs[ev.n]))
|
|
710
|
+
return
|
|
711
|
+
}
|
|
712
|
+
if (hiddenFrame() || cutInFlight("sockets", key)) {
|
|
713
|
+
this._deferred = defer(() => this._live(url, protocols, key, !hasFuture()))
|
|
714
|
+
return
|
|
715
|
+
}
|
|
716
|
+
this._live(url, protocols, key, false)
|
|
717
|
+
return
|
|
718
|
+
}
|
|
719
|
+
this._live(url, protocols, key, true)
|
|
720
|
+
}
|
|
721
|
+
_emit(m) {
|
|
722
|
+
let ev
|
|
723
|
+
if (m.type === "message") {
|
|
724
|
+
let data = m.data
|
|
725
|
+
if (m.b64 != null) {
|
|
726
|
+
const bytes = fromB64(m.b64)
|
|
727
|
+
data = this.binaryType === "arraybuffer" ? bytes.buffer : new Blob([bytes])
|
|
728
|
+
}
|
|
729
|
+
ev = new MessageEvent("message", { data, origin: new URL(this.url).origin })
|
|
730
|
+
} else if (m.type === "close") ev = new CloseEvent("close", { code: m.code, reason: m.reason, wasClean: m.wasClean })
|
|
731
|
+
else ev = new Event(m.type)
|
|
732
|
+
const prop = this["on" + m.type]
|
|
733
|
+
if (typeof prop === "function") safeCall(prop.bind(this), [ev])
|
|
734
|
+
this.dispatchEvent(ev)
|
|
735
|
+
}
|
|
736
|
+
_play(m) {
|
|
737
|
+
if (!m || this.readyState === 3) return
|
|
738
|
+
if (m.type === "open") {
|
|
739
|
+
this.readyState = 1
|
|
740
|
+
this.protocol = m.protocol || ""
|
|
741
|
+
}
|
|
742
|
+
if (m.type === "close") this.readyState = 3
|
|
743
|
+
this._emit(m)
|
|
744
|
+
}
|
|
745
|
+
_live(url, protocols, key, record) {
|
|
746
|
+
const ws = (this._real = new RealWS(url, protocols))
|
|
747
|
+
ws.binaryType = "arraybuffer"
|
|
748
|
+
const list = listOf("sockets")
|
|
749
|
+
this._record = record
|
|
750
|
+
this._entry = { key, t0: clock.now, msgs: [] }
|
|
751
|
+
if (record) {
|
|
752
|
+
this._idx = list.length
|
|
753
|
+
list.push(this._entry)
|
|
754
|
+
used.sockets.add(this._idx)
|
|
755
|
+
}
|
|
756
|
+
ws.onopen = () => this._got({ type: "open", protocol: ws.protocol })
|
|
757
|
+
ws.onmessage = (e) => this._got(typeof e.data === "string" ? { type: "message", data: e.data } : { type: "message", b64: toB64(new Uint8Array(e.data)) })
|
|
758
|
+
ws.onerror = () => this._got({ type: "error" })
|
|
759
|
+
ws.onclose = (e) => this._got({ type: "close", code: e.code, reason: e.reason, wasClean: e.wasClean })
|
|
760
|
+
}
|
|
761
|
+
_got(m) {
|
|
762
|
+
arrive(() => {
|
|
763
|
+
const e = this._entry
|
|
764
|
+
e.msgs.push(m)
|
|
765
|
+
if (m.type === "close") e.done = true
|
|
766
|
+
if (this._record) netEvent({ type: "net", list: "sockets", i: this._idx, n: e.msgs.length - 1 })
|
|
767
|
+
this._play(m)
|
|
768
|
+
})
|
|
769
|
+
}
|
|
770
|
+
send(data) {
|
|
771
|
+
if (this.readyState !== 1) throw new DOMException("WebSocket is not open", "InvalidStateError")
|
|
772
|
+
if (this._real) this._real.send(data) // a replayed socket talks to no one
|
|
773
|
+
}
|
|
774
|
+
close(code, reason) {
|
|
775
|
+
if (this._deferred) this._deferred.dead = true
|
|
776
|
+
if (this._real) this._real.close(code, reason)
|
|
777
|
+
else if (this.readyState < 2) {
|
|
778
|
+
this.readyState = 3
|
|
779
|
+
if (this._i != null) doneReplay("sockets", this._i)
|
|
780
|
+
this._emit({ type: "close", code: code || 1000, reason: reason || "", wasClean: true })
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
for (const [k, v] of Object.entries({ CONNECTING: 0, OPEN: 1, CLOSING: 2, CLOSED: 3 })) {
|
|
785
|
+
RetakeWebSocket[k] = v
|
|
786
|
+
RetakeWebSocket.prototype[k] = v
|
|
787
|
+
}
|
|
788
|
+
W.WebSocket = RetakeWebSocket
|
|
789
|
+
}
|