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