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,315 @@
1
+ // State that outlives a reload besides web storage: cookies and IndexedDB.
2
+ // A recording snapshots both when it starts; a rebuild puts them back before
3
+ // the app can read them, so a replay starts from the same place.
4
+ //
5
+ // IndexedDB also answers in real time, so outstanding IndexedDB work counts as
6
+ // "the app is busy" (see settle() in 00-core): time only moves on once it's
7
+ // done, while recording and while replaying alike.
8
+
9
+ // ---- cookies ---------------------------------------------------------------------
10
+
11
+ // (Retake's own cookies, `__retake…`, aren't the app's.)
12
+ const ownCookie = (part) => /^\s*__retake/.test(part)
13
+ function snapshotCookies() {
14
+ try {
15
+ return document.cookie
16
+ .split(";")
17
+ .filter((p) => p.trim() && !ownCookie(p))
18
+ .join(";")
19
+ .trim()
20
+ } catch {
21
+ return ""
22
+ }
23
+ }
24
+
25
+ function restoreCookies(snap) {
26
+ if (snap == null) return
27
+ try {
28
+ const expire = "=; expires=Thu, 01 Jan 1970 00:00:00 GMT"
29
+ const paths = ["/", location.pathname.replace(/[^/]*$/, "") || "/"]
30
+ for (const part of document.cookie.split(";")) {
31
+ const name = part.split("=")[0].trim()
32
+ if (!name || ownCookie(part)) continue
33
+ for (const p of paths) document.cookie = `${name}${expire}; path=${p}`
34
+ document.cookie = `${name}${expire}`
35
+ }
36
+ for (const part of snap.split(";")) {
37
+ const s = part.trim()
38
+ if (s) document.cookie = `${s}; path=/`
39
+ }
40
+ } catch {}
41
+ }
42
+
43
+ // ---- values that survive JSON --------------------------------------------------------
44
+
45
+ function encodeValue(v) {
46
+ if (v === undefined) return { $u: 1 }
47
+ if (v === null || typeof v === "boolean" || typeof v === "string") return v
48
+ if (typeof v === "number") return Number.isFinite(v) ? v : { $n: String(v) }
49
+ if (typeof v === "bigint") return { $bi: String(v) }
50
+ if (v instanceof Date) return { $d: v.getTime() }
51
+ if (v instanceof ArrayBuffer) return { $ab: toB64(new Uint8Array(v)) }
52
+ if (ArrayBuffer.isView(v)) return { $ta: v.constructor.name, b: toB64(new Uint8Array(v.buffer, v.byteOffset, v.byteLength)) }
53
+ if (v instanceof Map) return { $map: [...v].map(([k, x]) => [encodeValue(k), encodeValue(x)]) }
54
+ if (v instanceof Set) return { $set: [...v].map(encodeValue) }
55
+ if (Array.isArray(v)) return v.map(encodeValue)
56
+ if (typeof Blob !== "undefined" && v instanceof Blob) throw new Error("blobs in IndexedDB aren't snapshotted")
57
+ const out = {}
58
+ for (const k of Object.keys(v)) out[k] = encodeValue(v[k])
59
+ return Object.keys(out).some((k) => k.startsWith("$")) ? { $o: out } : out
60
+ }
61
+
62
+ function decodeValue(v) {
63
+ if (v === null || typeof v !== "object") return v
64
+ if (Array.isArray(v)) return v.map(decodeValue)
65
+ if ("$u" in v) return undefined
66
+ if ("$n" in v) return Number(v.$n)
67
+ if ("$bi" in v) return BigInt(v.$bi)
68
+ if ("$d" in v) return new Date(v.$d)
69
+ if ("$ab" in v) return fromB64(v.$ab).buffer
70
+ if ("$ta" in v) {
71
+ const bytes = fromB64(v.b)
72
+ const C = W[v.$ta] || Uint8Array
73
+ return new C(bytes.buffer, 0, bytes.byteLength / (C.BYTES_PER_ELEMENT || 1))
74
+ }
75
+ if ("$map" in v) return new Map(v.$map.map(([k, x]) => [decodeValue(k), decodeValue(x)]))
76
+ if ("$set" in v) return new Set(v.$set.map(decodeValue))
77
+ const src = "$o" in v ? v.$o : v
78
+ const out = {}
79
+ for (const k of Object.keys(src)) out[k] = decodeValue(src[k])
80
+ return out
81
+ }
82
+
83
+ // ---- IndexedDB -------------------------------------------------------------------------
84
+
85
+ const idbF = W.indexedDB
86
+ const IDBF = W.IDBFactory && IDBFactory.prototype
87
+ const realOpen = IDBF && IDBF.open
88
+ const realDelete = IDBF && IDBF.deleteDatabase
89
+ let idbBusy = 0 // open requests + live transactions
90
+ // Dev tools' own databases (Next 16 keeps its debug channel in one) aren't the
91
+ // app's state: not snapshotted, restored or waited for.
92
+ const DEV_DB = /^__next/
93
+ const devDb = (name) => DEV_DB.test(String(name || ""))
94
+ let idbGate = null // while snapshotting/restoring, the app's opens wait here
95
+
96
+ // A request still blocked (another tab holding the database) after 3s gives up.
97
+ const promisify = (req) =>
98
+ new Promise((resolve, reject) => {
99
+ let timer = null
100
+ req.onsuccess = () => {
101
+ real.clearTimeout(timer)
102
+ resolve(req.result)
103
+ }
104
+ req.onerror = () => {
105
+ real.clearTimeout(timer)
106
+ reject(req.error)
107
+ }
108
+ req.onblocked = () => {
109
+ timer = real.setTimeout(() => reject(new Error("IndexedDB is held open elsewhere (another tab?)")), 3000)
110
+ }
111
+ })
112
+
113
+ async function snapshotIDB() {
114
+ if (!idbF || !idbF.databases) return null
115
+ const list = await idbF.databases()
116
+ const out = []
117
+ for (const info of list) {
118
+ if (!info.name || devDb(info.name)) continue
119
+ const db = await promisify(realOpen.call(idbF, info.name))
120
+ try {
121
+ const names = [...db.objectStoreNames]
122
+ const stores = []
123
+ if (names.length) {
124
+ const tx = db.transaction(names, "readonly")
125
+ for (const name of names) {
126
+ const st = tx.objectStore(name)
127
+ const meta = {
128
+ name,
129
+ keyPath: st.keyPath,
130
+ autoIncrement: st.autoIncrement,
131
+ indexes: [...st.indexNames].map((n) => {
132
+ const ix = st.index(n)
133
+ return { name: n, keyPath: ix.keyPath, unique: ix.unique, multiEntry: ix.multiEntry }
134
+ }),
135
+ records: [],
136
+ }
137
+ await new Promise((resolve, reject) => {
138
+ const cur = st.openCursor()
139
+ cur.onsuccess = () => {
140
+ const c = cur.result
141
+ if (!c) return resolve(undefined)
142
+ meta.records.push([encodeValue(c.primaryKey), encodeValue(c.value)])
143
+ c.continue()
144
+ }
145
+ cur.onerror = () => reject(cur.error)
146
+ })
147
+ stores.push(meta)
148
+ }
149
+ }
150
+ out.push({ name: info.name, version: db.version, stores })
151
+ } finally {
152
+ db.close()
153
+ }
154
+ }
155
+ return out
156
+ }
157
+
158
+ async function restoreIDB(snap) {
159
+ if (!idbF || !snap) return
160
+ const keep = new Set(snap.map((d) => d.name))
161
+ if (idbF.databases) {
162
+ for (const info of await idbF.databases()) {
163
+ if (info.name && !keep.has(info.name) && !devDb(info.name)) await promisify(realDelete.call(idbF, info.name)).catch(() => {})
164
+ }
165
+ }
166
+ for (const d of snap) {
167
+ await promisify(realDelete.call(idbF, d.name)).catch(() => {})
168
+ const req = realOpen.call(idbF, d.name, d.version)
169
+ req.onupgradeneeded = () => {
170
+ const db = req.result
171
+ for (const s of d.stores) {
172
+ const st = db.createObjectStore(s.name, { keyPath: s.keyPath, autoIncrement: s.autoIncrement })
173
+ for (const ix of s.indexes) st.createIndex(ix.name, ix.keyPath, { unique: ix.unique, multiEntry: ix.multiEntry })
174
+ }
175
+ }
176
+ const db = await promisify(req)
177
+ try {
178
+ const names = d.stores.map((s) => s.name)
179
+ if (names.length) {
180
+ await new Promise((resolve, reject) => {
181
+ const tx = db.transaction(names, "readwrite")
182
+ for (const s of d.stores) {
183
+ const st = tx.objectStore(s.name)
184
+ for (const [k, v] of s.records) (s.keyPath == null ? st.put(decodeValue(v), decodeValue(k)) : st.put(decodeValue(v)))
185
+ }
186
+ tx.oncomplete = resolve
187
+ tx.onerror = () => reject(tx.error)
188
+ tx.onabort = () => reject(tx.error)
189
+ })
190
+ }
191
+ } finally {
192
+ db.close()
193
+ }
194
+ }
195
+ }
196
+
197
+ // An open request that starts once the gate lifts. It is a real EventTarget
198
+ // dressed as an IDBOpenDBRequest (so `instanceof IDBRequest` holds), relaying
199
+ // the real request's events.
200
+ function deferredOpen(start) {
201
+ idbBusy++
202
+ let counted = true
203
+ const uncount = () => {
204
+ if (counted) idbBusy--
205
+ counted = false
206
+ }
207
+ const fake = new EventTarget()
208
+ Object.setPrototypeOf(fake, IDBOpenDBRequest.prototype)
209
+ const props = { result: undefined, error: null, readyState: "pending", source: null, transaction: null }
210
+ for (const k of Object.keys(props)) Object.defineProperty(fake, k, { get: () => props[k], configurable: true })
211
+ const handlers = {}
212
+ for (const type of ["success", "error", "upgradeneeded", "blocked"]) {
213
+ Object.defineProperty(fake, "on" + type, { get: () => handlers[type] || null, set: (fn) => (handlers[type] = fn), configurable: true })
214
+ EventTarget.prototype.addEventListener.call(fake, type, (e) => handlers[type] && handlers[type].call(fake, e))
215
+ }
216
+ idbGate.then(() => {
217
+ const req = start()
218
+ for (const type of ["success", "error", "upgradeneeded", "blocked"]) {
219
+ req.addEventListener(type, (e) => {
220
+ props.readyState = req.readyState
221
+ try {
222
+ props.result = req.result
223
+ } catch {}
224
+ props.error = req.readyState === "done" ? req.error : null
225
+ props.transaction = req.transaction
226
+ const ev = type === "upgradeneeded" || type === "blocked" ? new IDBVersionChangeEvent(type, { oldVersion: e.oldVersion, newVersion: e.newVersion }) : new Event(type, { cancelable: type === "error", bubbles: type === "error" })
227
+ if (type !== "upgradeneeded") uncount()
228
+ EventTarget.prototype.dispatchEvent.call(fake, ev)
229
+ if (ev.defaultPrevented) e.preventDefault()
230
+ })
231
+ }
232
+ })
233
+ return fake
234
+ }
235
+
236
+ // A frame being replaced by a rebuild lets go of its databases when the new
237
+ // frame asks to reset them (otherwise the reset waits on this frame forever).
238
+ function releaseOnRebuild(req) {
239
+ req.addEventListener("success", () => {
240
+ const db = req.result
241
+ if (db && db.addEventListener) {
242
+ db.addEventListener("versionchange", () => {
243
+ try {
244
+ if (shell && shell.rebuilding) db.close()
245
+ } catch {}
246
+ })
247
+ }
248
+ })
249
+ return req
250
+ }
251
+
252
+ function trackOpen(req) {
253
+ releaseOnRebuild(req)
254
+ idbBusy++
255
+ let done = false
256
+ const end = () => {
257
+ if (!done) idbBusy--
258
+ done = true
259
+ }
260
+ req.addEventListener("success", end)
261
+ req.addEventListener("error", end)
262
+ req.addEventListener("blocked", end)
263
+ return req
264
+ }
265
+
266
+ if (IDBF) {
267
+ IDBF.open = function (...args) {
268
+ if (isExempt() || devDb(args[0])) return realOpen.apply(this, args)
269
+ idbOpened = true // this app keeps state in IndexedDB (see storageOk in 30-recorder)
270
+ if (idbGate) return deferredOpen(() => trackOpen(realOpen.apply(idbF, args)))
271
+ return trackOpen(realOpen.apply(this, args))
272
+ }
273
+ IDBF.deleteDatabase = function (...args) {
274
+ if (idbGate) return deferredOpen(() => trackOpen(realDelete.apply(idbF, args)))
275
+ return trackOpen(realDelete.apply(this, args))
276
+ }
277
+ const realTx = IDBDatabase.prototype.transaction
278
+ IDBDatabase.prototype.transaction = function (...args) {
279
+ const tx = realTx.apply(this, args)
280
+ idbBusy++
281
+ let done = false
282
+ const end = () => {
283
+ if (!done) idbBusy--
284
+ done = true
285
+ }
286
+ tx.addEventListener("complete", end)
287
+ tx.addEventListener("abort", end)
288
+ tx.addEventListener("error", end)
289
+ return tx
290
+ }
291
+ }
292
+
293
+ // Run `work` (a snapshot or a restore) with the app's IndexedDB opens held.
294
+ function withIDBGate(work) {
295
+ if (!IDBF) return Promise.resolve()
296
+ let lift
297
+ idbGate = new Promise((r) => (lift = r))
298
+ const gate = idbGate
299
+ return work()
300
+ .catch((err) => console.warn("[retake] IndexedDB snapshot/restore failed:", err && err.message))
301
+ .finally(() => {
302
+ if (idbGate === gate) idbGate = null
303
+ lift()
304
+ })
305
+ }
306
+
307
+ // Service workers keep their own caches and answer requests outside the
308
+ // recording; say so once.
309
+ try {
310
+ if (navigator.serviceWorker && navigator.serviceWorker.getRegistrations) {
311
+ navigator.serviceWorker.getRegistrations().then((regs) => {
312
+ if (regs.length) console.warn("[retake] this page has a service worker; responses it serves from its cache aren't rewound (Cache API isn't snapshotted)")
313
+ })
314
+ }
315
+ } catch {}