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,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
+ }