@rewindkit/runtime 0.1.0-alpha.1
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 +21 -0
- package/README.md +178 -0
- package/dist/bundle-io.d.ts +8 -0
- package/dist/bundle-io.d.ts.map +1 -0
- package/dist/bundle.d.ts +83 -0
- package/dist/bundle.d.ts.map +1 -0
- package/dist/configure.d.ts +27 -0
- package/dist/configure.d.ts.map +1 -0
- package/dist/enhancer.d.ts +10 -0
- package/dist/enhancer.d.ts.map +1 -0
- package/dist/env.d.ts +4 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +736 -0
- package/dist/index.js.map +19 -0
- package/dist/player.d.ts +13 -0
- package/dist/player.d.ts.map +1 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +497 -0
- package/dist/react/index.js.map +16 -0
- package/dist/recorder.d.ts +52 -0
- package/dist/recorder.d.ts.map +1 -0
- package/dist/redaction.d.ts +13 -0
- package/dist/redaction.d.ts.map +1 -0
- package/dist/replay.d.ts +10 -0
- package/dist/replay.d.ts.map +1 -0
- package/package.json +42 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
// src/bundle.ts
|
|
2
|
+
var SCHEMA_VERSION = 2;
|
|
3
|
+
var DELTA_FORMAT_VERSION = "1.0";
|
|
4
|
+
// src/env.ts
|
|
5
|
+
var isProd = () => {
|
|
6
|
+
try {
|
|
7
|
+
return process.env.NODE_ENV === "production";
|
|
8
|
+
} catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var warned = new Set;
|
|
13
|
+
var warnOnce = (message) => {
|
|
14
|
+
if (isProd())
|
|
15
|
+
return;
|
|
16
|
+
if (warned.has(message))
|
|
17
|
+
return;
|
|
18
|
+
warned.add(message);
|
|
19
|
+
console.warn(`[rewindkit] ${message}`);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// src/redaction.ts
|
|
23
|
+
var REDACTED = "‹redacted›";
|
|
24
|
+
var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
25
|
+
var maskKeys = (value, redactKeys) => {
|
|
26
|
+
if (Array.isArray(value))
|
|
27
|
+
return value.map((item) => maskKeys(item, redactKeys));
|
|
28
|
+
if (isPlainObject(value)) {
|
|
29
|
+
const out = {};
|
|
30
|
+
for (const [key, val] of Object.entries(value)) {
|
|
31
|
+
if (redactKeys.has(key)) {
|
|
32
|
+
warnOnce(`redacted key "${key}" — value masked before capture`);
|
|
33
|
+
out[key] = REDACTED;
|
|
34
|
+
} else {
|
|
35
|
+
out[key] = maskKeys(val, redactKeys);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
};
|
|
42
|
+
var __maskDeep = (value, redactKeys) => redactKeys && redactKeys.length ? maskKeys(value, new Set(redactKeys)) : value;
|
|
43
|
+
var __redact = (value, opts) => {
|
|
44
|
+
const redactKeys = new Set(opts.redactKeys ?? []);
|
|
45
|
+
if (!isPlainObject(value)) {
|
|
46
|
+
return redactKeys.size ? maskKeys(value, redactKeys) : value;
|
|
47
|
+
}
|
|
48
|
+
let entries = Object.entries(value);
|
|
49
|
+
if (opts.includeStateKeys === undefined) {
|
|
50
|
+
warnOnce("includeStateKeys is not set — capturing ALL slices. Set an allowlist in production (PII risk).");
|
|
51
|
+
} else {
|
|
52
|
+
const allow = new Set(opts.includeStateKeys);
|
|
53
|
+
entries = entries.filter(([key]) => allow.has(key));
|
|
54
|
+
}
|
|
55
|
+
const out = {};
|
|
56
|
+
for (const [key, val] of entries) {
|
|
57
|
+
out[key] = redactKeys.size ? maskKeys(val, redactKeys) : val;
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
};
|
|
61
|
+
var DEFAULTS = { depth: 6, nodes: 200, strLen: 1e4 };
|
|
62
|
+
var serializeForDisplay = (value, opts = {}) => {
|
|
63
|
+
const cfg = { ...DEFAULTS, ...opts };
|
|
64
|
+
let budget = cfg.nodes;
|
|
65
|
+
const walk = (v, depth) => {
|
|
66
|
+
budget -= 1;
|
|
67
|
+
if (budget < 0)
|
|
68
|
+
return "… (capped)";
|
|
69
|
+
if (typeof v === "string") {
|
|
70
|
+
return v.length > cfg.strLen ? `${v.slice(0, cfg.strLen)}… (${v.length} chars)` : v;
|
|
71
|
+
}
|
|
72
|
+
if (typeof v === "bigint")
|
|
73
|
+
return `${v}n`;
|
|
74
|
+
if (typeof v === "function")
|
|
75
|
+
return `‹function ${v.name || "anonymous"}›`;
|
|
76
|
+
if (typeof v === "symbol")
|
|
77
|
+
return v.toString();
|
|
78
|
+
if (v === null || typeof v !== "object")
|
|
79
|
+
return v;
|
|
80
|
+
if (v instanceof Date)
|
|
81
|
+
return `Date(${v.toISOString()})`;
|
|
82
|
+
if (v instanceof Error)
|
|
83
|
+
return `${v.name}: ${v.message}`;
|
|
84
|
+
if (v instanceof Map) {
|
|
85
|
+
if (v.size === 0)
|
|
86
|
+
return "Map(0) {}";
|
|
87
|
+
if (depth >= cfg.depth)
|
|
88
|
+
return `Map(${v.size}) …`;
|
|
89
|
+
const obj = {};
|
|
90
|
+
for (const [k, val] of v)
|
|
91
|
+
obj[String(k)] = walk(val, depth + 1);
|
|
92
|
+
return { "‹Map›": obj };
|
|
93
|
+
}
|
|
94
|
+
if (v instanceof Set) {
|
|
95
|
+
if (v.size === 0)
|
|
96
|
+
return "Set(0) {}";
|
|
97
|
+
if (depth >= cfg.depth)
|
|
98
|
+
return `Set(${v.size}) …`;
|
|
99
|
+
return { "‹Set›": [...v].map((item) => walk(item, depth + 1)) };
|
|
100
|
+
}
|
|
101
|
+
if (Array.isArray(v)) {
|
|
102
|
+
if (v.length === 0)
|
|
103
|
+
return [];
|
|
104
|
+
if (depth >= cfg.depth)
|
|
105
|
+
return `[Array(${v.length})]`;
|
|
106
|
+
return v.map((item) => walk(item, depth + 1));
|
|
107
|
+
}
|
|
108
|
+
const keys = Object.keys(v);
|
|
109
|
+
if (keys.length === 0)
|
|
110
|
+
return {};
|
|
111
|
+
if (depth >= cfg.depth)
|
|
112
|
+
return `{…(${keys.length} keys)}`;
|
|
113
|
+
const out = {};
|
|
114
|
+
for (const key of keys)
|
|
115
|
+
out[key] = walk(v[key], depth + 1);
|
|
116
|
+
return out;
|
|
117
|
+
};
|
|
118
|
+
return walk(value, 0);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// src/recorder.ts
|
|
122
|
+
var DEFAULT_CONFIG = {
|
|
123
|
+
keyframeEvery: 25,
|
|
124
|
+
fullKeyframeRatio: 8,
|
|
125
|
+
sample: 1,
|
|
126
|
+
maxEventsPerSecond: 200
|
|
127
|
+
};
|
|
128
|
+
var UNSERIALIZABLE = "__unserializable";
|
|
129
|
+
var KEY = "__REPLAYKIT_RECORDER__";
|
|
130
|
+
var createRecorder = () => {
|
|
131
|
+
const bundle = {
|
|
132
|
+
version: DELTA_FORMAT_VERSION,
|
|
133
|
+
schemaVersion: SCHEMA_VERSION,
|
|
134
|
+
mode: "state-snapshot",
|
|
135
|
+
sessionId: crypto.randomUUID(),
|
|
136
|
+
startTime: Date.now(),
|
|
137
|
+
duration: 0,
|
|
138
|
+
remotes: [],
|
|
139
|
+
events: []
|
|
140
|
+
};
|
|
141
|
+
const stores = new Map;
|
|
142
|
+
const rawStash = new Map;
|
|
143
|
+
const prevLiveSlices = new Map;
|
|
144
|
+
const actionCount = new Map;
|
|
145
|
+
let windowStart = 0;
|
|
146
|
+
let windowCount = 0;
|
|
147
|
+
const suppression = { depth: 0 };
|
|
148
|
+
const rec = {
|
|
149
|
+
suppression,
|
|
150
|
+
config: { ...DEFAULT_CONFIG },
|
|
151
|
+
recording: false,
|
|
152
|
+
origin: "host",
|
|
153
|
+
now: () => Date.now(),
|
|
154
|
+
start() {
|
|
155
|
+
rec.recording = true;
|
|
156
|
+
for (const [name, store] of stores)
|
|
157
|
+
emitKeyframe(name, store.getState(), true);
|
|
158
|
+
},
|
|
159
|
+
stop() {
|
|
160
|
+
rec.recording = false;
|
|
161
|
+
},
|
|
162
|
+
isRecording() {
|
|
163
|
+
return rec.recording;
|
|
164
|
+
},
|
|
165
|
+
configure(opts) {
|
|
166
|
+
const { origin, now, ...cfg } = opts;
|
|
167
|
+
rec.config = { ...rec.config, ...cfg };
|
|
168
|
+
if (origin !== undefined)
|
|
169
|
+
rec.origin = origin;
|
|
170
|
+
if (now !== undefined) {
|
|
171
|
+
rec.now = now;
|
|
172
|
+
bundle.startTime = now();
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
registerStore(name, store) {
|
|
176
|
+
if (stores.has(name)) {
|
|
177
|
+
warnOnce(`store "${name}" already registered — names must be unique (ignoring re-register)`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
stores.set(name, store);
|
|
181
|
+
const registeredAtMs = rec.now() - bundle.startTime;
|
|
182
|
+
bundle.remotes.push({ name, storeKey: name, registeredAtMs });
|
|
183
|
+
if (rec.recording)
|
|
184
|
+
emitKeyframe(name, store.getState(), true);
|
|
185
|
+
},
|
|
186
|
+
unregisterStore(name) {
|
|
187
|
+
stores.delete(name);
|
|
188
|
+
},
|
|
189
|
+
getStore(name) {
|
|
190
|
+
return stores.get(name);
|
|
191
|
+
},
|
|
192
|
+
captureAction(storeKey, action) {
|
|
193
|
+
if (!armed())
|
|
194
|
+
return;
|
|
195
|
+
if (rec.config.sample < 1 && Math.random() > rec.config.sample)
|
|
196
|
+
return;
|
|
197
|
+
if (throttled())
|
|
198
|
+
return;
|
|
199
|
+
append({
|
|
200
|
+
type: "action",
|
|
201
|
+
tsMs: tsMs(),
|
|
202
|
+
origin: rec.origin,
|
|
203
|
+
storeKey,
|
|
204
|
+
seq: nextSeq(),
|
|
205
|
+
action: { type: action.type, payload: action.payload }
|
|
206
|
+
});
|
|
207
|
+
},
|
|
208
|
+
captureKeyframe(storeKey) {
|
|
209
|
+
if (!armed())
|
|
210
|
+
return;
|
|
211
|
+
const store = stores.get(storeKey);
|
|
212
|
+
if (!store)
|
|
213
|
+
return;
|
|
214
|
+
const count = (actionCount.get(storeKey) ?? 0) + 1;
|
|
215
|
+
actionCount.set(storeKey, count);
|
|
216
|
+
const fullEvery = rec.config.keyframeEvery * rec.config.fullKeyframeRatio;
|
|
217
|
+
if (count % rec.config.keyframeEvery !== 0)
|
|
218
|
+
return;
|
|
219
|
+
const full = count % fullEvery === 0;
|
|
220
|
+
emitKeyframe(storeKey, store.getState(), full);
|
|
221
|
+
},
|
|
222
|
+
captureRoute(path, search) {
|
|
223
|
+
if (!armed())
|
|
224
|
+
return;
|
|
225
|
+
if (throttled())
|
|
226
|
+
return;
|
|
227
|
+
append({
|
|
228
|
+
type: "route",
|
|
229
|
+
tsMs: tsMs(),
|
|
230
|
+
origin: rec.origin,
|
|
231
|
+
storeKey: rec.origin,
|
|
232
|
+
seq: nextSeq(),
|
|
233
|
+
path,
|
|
234
|
+
search
|
|
235
|
+
});
|
|
236
|
+
},
|
|
237
|
+
pushFeedEvent(ev) {
|
|
238
|
+
if (!armed())
|
|
239
|
+
return;
|
|
240
|
+
const topics = rec.config.includeFeedTopics;
|
|
241
|
+
if (!topics || topics.length === 0) {
|
|
242
|
+
warnOnce("includeFeedTopics is empty — no feed_message recorded. Allowlist a topic.");
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (!topics.includes(ev.topic))
|
|
246
|
+
return;
|
|
247
|
+
if (throttled())
|
|
248
|
+
return;
|
|
249
|
+
const redactKeys = rec.config.redactKeys;
|
|
250
|
+
const rows = ev.rows ? structuredClone(__maskDeep(ev.rows, redactKeys)) : ev.rows;
|
|
251
|
+
const data = ev.data !== undefined ? structuredClone(__maskDeep(ev.data, redactKeys)) : ev.data;
|
|
252
|
+
append({
|
|
253
|
+
...ev,
|
|
254
|
+
rows,
|
|
255
|
+
data,
|
|
256
|
+
type: "feed_message",
|
|
257
|
+
tsMs: tsMs(),
|
|
258
|
+
origin: rec.origin,
|
|
259
|
+
storeKey: ev.storeKey ?? rec.origin,
|
|
260
|
+
seq: nextSeq()
|
|
261
|
+
});
|
|
262
|
+
},
|
|
263
|
+
suppress() {
|
|
264
|
+
suppression.depth += 1;
|
|
265
|
+
},
|
|
266
|
+
unsuppress() {
|
|
267
|
+
suppression.depth -= 1;
|
|
268
|
+
if (suppression.depth < 0) {
|
|
269
|
+
suppression.depth = 0;
|
|
270
|
+
if (!isProdSafe())
|
|
271
|
+
throw new Error("[replaykit] suppression depth went negative");
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
getBundle() {
|
|
275
|
+
bundle.duration = rawStashMaxTs();
|
|
276
|
+
return bundle;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
const armed = () => rec.recording && suppression.depth === 0;
|
|
280
|
+
const tsMs = () => rec.now() - bundle.startTime;
|
|
281
|
+
const nextSeq = () => bundle.events.length;
|
|
282
|
+
const append = (event) => {
|
|
283
|
+
bundle.events.push(event);
|
|
284
|
+
rec.config.sink?.push(event);
|
|
285
|
+
};
|
|
286
|
+
const throttled = () => {
|
|
287
|
+
const t = rec.now();
|
|
288
|
+
if (t - windowStart >= 1000) {
|
|
289
|
+
windowStart = t;
|
|
290
|
+
windowCount = 0;
|
|
291
|
+
}
|
|
292
|
+
if (windowCount >= rec.config.maxEventsPerSecond)
|
|
293
|
+
return true;
|
|
294
|
+
windowCount += 1;
|
|
295
|
+
return false;
|
|
296
|
+
};
|
|
297
|
+
const emitKeyframe = (storeKey, state, full) => {
|
|
298
|
+
const liveSlices = state && typeof state === "object" && !Array.isArray(state) ? state : {};
|
|
299
|
+
const redacted = __redact(state, {
|
|
300
|
+
includeStateKeys: rec.config.includeStateKeys,
|
|
301
|
+
redactKeys: rec.config.redactKeys
|
|
302
|
+
});
|
|
303
|
+
const prevStash = rawStash.get(storeKey);
|
|
304
|
+
const prevLive = prevLiveSlices.get(storeKey);
|
|
305
|
+
if (full || !prevStash || !prevLive) {
|
|
306
|
+
const cloned = cloneSlices(redacted);
|
|
307
|
+
rawStash.set(storeKey, cloned);
|
|
308
|
+
prevLiveSlices.set(storeKey, { ...liveSlices });
|
|
309
|
+
append({
|
|
310
|
+
type: "state_snapshot",
|
|
311
|
+
kind: "keyframe",
|
|
312
|
+
tsMs: tsMs(),
|
|
313
|
+
origin: rec.origin,
|
|
314
|
+
storeKey,
|
|
315
|
+
seq: nextSeq(),
|
|
316
|
+
slices: cloned
|
|
317
|
+
});
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const changedStash = {};
|
|
321
|
+
const nextStash = { ...prevStash };
|
|
322
|
+
for (const key of Object.keys(redacted)) {
|
|
323
|
+
if (!Object.is(prevLive[key], liveSlices[key])) {
|
|
324
|
+
const clonedVal = cloneSingle(redacted[key]);
|
|
325
|
+
changedStash[key] = clonedVal;
|
|
326
|
+
nextStash[key] = clonedVal;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const removed = Object.keys(prevStash).filter((key) => !(key in redacted));
|
|
330
|
+
for (const key of removed)
|
|
331
|
+
delete nextStash[key];
|
|
332
|
+
rawStash.set(storeKey, nextStash);
|
|
333
|
+
prevLiveSlices.set(storeKey, { ...liveSlices });
|
|
334
|
+
append({
|
|
335
|
+
type: "state_snapshot",
|
|
336
|
+
kind: "delta",
|
|
337
|
+
tsMs: tsMs(),
|
|
338
|
+
origin: rec.origin,
|
|
339
|
+
storeKey,
|
|
340
|
+
seq: nextSeq(),
|
|
341
|
+
slices: changedStash,
|
|
342
|
+
...removed.length ? { removedSlices: removed } : {}
|
|
343
|
+
});
|
|
344
|
+
};
|
|
345
|
+
const cloneSingle = (val) => {
|
|
346
|
+
try {
|
|
347
|
+
return structuredClone(val);
|
|
348
|
+
} catch {
|
|
349
|
+
return { [UNSERIALIZABLE]: true };
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
const cloneSlices = (state) => {
|
|
353
|
+
const out = {};
|
|
354
|
+
for (const [key, val] of Object.entries(state))
|
|
355
|
+
out[key] = cloneSingle(val);
|
|
356
|
+
return out;
|
|
357
|
+
};
|
|
358
|
+
const rawStashMaxTs = () => {
|
|
359
|
+
let max = 0;
|
|
360
|
+
for (const ev of bundle.events)
|
|
361
|
+
if (ev.tsMs > max)
|
|
362
|
+
max = ev.tsMs;
|
|
363
|
+
return max;
|
|
364
|
+
};
|
|
365
|
+
return rec;
|
|
366
|
+
};
|
|
367
|
+
var isProdSafe = () => {
|
|
368
|
+
try {
|
|
369
|
+
return process.env.NODE_ENV === "production";
|
|
370
|
+
} catch {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
var acquireRecorder = () => {
|
|
375
|
+
const g = globalThis;
|
|
376
|
+
const existing = g[KEY];
|
|
377
|
+
if (existing)
|
|
378
|
+
return existing;
|
|
379
|
+
const r = createRecorder();
|
|
380
|
+
g[KEY] = r;
|
|
381
|
+
return g[KEY] ?? r;
|
|
382
|
+
};
|
|
383
|
+
var __peekRecorder = () => globalThis[KEY];
|
|
384
|
+
|
|
385
|
+
// src/enhancer.ts
|
|
386
|
+
var RESTORE_TYPE = "__REPLAYKIT_RESTORE";
|
|
387
|
+
var isRestore = (action) => typeof action === "object" && action !== null && action.type === RESTORE_TYPE;
|
|
388
|
+
var wrapReducer = (reducer) => (state, action) => {
|
|
389
|
+
if (isRestore(action)) {
|
|
390
|
+
const base = state ?? {};
|
|
391
|
+
const next = { ...base, ...action.slices };
|
|
392
|
+
for (const key of action.removedSlices ?? [])
|
|
393
|
+
delete next[key];
|
|
394
|
+
return next;
|
|
395
|
+
}
|
|
396
|
+
return reducer(state, action);
|
|
397
|
+
};
|
|
398
|
+
var enhancer = (createStore) => (reducer, preloadedState) => {
|
|
399
|
+
const store = createStore(wrapReducer(reducer), preloadedState);
|
|
400
|
+
const dispatch = (action) => {
|
|
401
|
+
const recorder = __peekRecorder();
|
|
402
|
+
if (!recorder)
|
|
403
|
+
return store.dispatch(action);
|
|
404
|
+
const result = store.dispatch(action);
|
|
405
|
+
if (!isRestore(action) && typeof action.type === "string") {
|
|
406
|
+
const storeKey = recorder.origin;
|
|
407
|
+
recorder.captureAction(storeKey, {
|
|
408
|
+
type: action.type,
|
|
409
|
+
payload: action.payload
|
|
410
|
+
});
|
|
411
|
+
recorder.captureKeyframe(storeKey);
|
|
412
|
+
}
|
|
413
|
+
return result;
|
|
414
|
+
};
|
|
415
|
+
return { ...store, dispatch };
|
|
416
|
+
};
|
|
417
|
+
var replayEnhancer = enhancer;
|
|
418
|
+
|
|
419
|
+
// src/replay.ts
|
|
420
|
+
var mode = "live";
|
|
421
|
+
var listeners = new Set;
|
|
422
|
+
var getReplayMode = () => mode;
|
|
423
|
+
var subscribeReplayMode = (cb) => {
|
|
424
|
+
listeners.add(cb);
|
|
425
|
+
cb(mode);
|
|
426
|
+
return () => {
|
|
427
|
+
listeners.delete(cb);
|
|
428
|
+
};
|
|
429
|
+
};
|
|
430
|
+
var notify = () => {
|
|
431
|
+
for (const cb of listeners)
|
|
432
|
+
cb(mode);
|
|
433
|
+
};
|
|
434
|
+
var setMode = (next) => {
|
|
435
|
+
if (next === mode)
|
|
436
|
+
return;
|
|
437
|
+
const recorder = acquireRecorder();
|
|
438
|
+
if (next === "history") {
|
|
439
|
+
recorder.suppress();
|
|
440
|
+
mode = "history";
|
|
441
|
+
} else {
|
|
442
|
+
mode = "live";
|
|
443
|
+
recorder.unsuppress();
|
|
444
|
+
}
|
|
445
|
+
notify();
|
|
446
|
+
};
|
|
447
|
+
var applyReplayState = (snapshot, storeKey) => {
|
|
448
|
+
const recorder = acquireRecorder();
|
|
449
|
+
const key = storeKey ?? recorder.origin;
|
|
450
|
+
const store = recorder.getStore(key);
|
|
451
|
+
if (!store) {
|
|
452
|
+
warnOnce(`applyReplayState: no store registered for "${key}"`);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
recorder.suppress();
|
|
456
|
+
try {
|
|
457
|
+
const action = { type: RESTORE_TYPE, slices: { ...snapshot.slices } };
|
|
458
|
+
store.dispatch(action);
|
|
459
|
+
} finally {
|
|
460
|
+
recorder.unsuppress();
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
var seek = (player, tMs) => {
|
|
464
|
+
const states = player.statesAt(tMs);
|
|
465
|
+
if (mode !== "history")
|
|
466
|
+
setMode("history");
|
|
467
|
+
const recorder = acquireRecorder();
|
|
468
|
+
recorder.suppress();
|
|
469
|
+
try {
|
|
470
|
+
for (const [storeKey, snap] of Object.entries(states)) {
|
|
471
|
+
if (snap)
|
|
472
|
+
applyReplayState(snap, storeKey);
|
|
473
|
+
}
|
|
474
|
+
} finally {
|
|
475
|
+
recorder.unsuppress();
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
var returnToLive = () => {
|
|
479
|
+
setMode("live");
|
|
480
|
+
const recorder = acquireRecorder();
|
|
481
|
+
if (recorder.suppression.depth !== 0) {
|
|
482
|
+
warnOnce("suppression depth non-zero after returnToLive — a suppress/unsuppress pair leaked");
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
// src/configure.ts
|
|
487
|
+
var configureReplay = (opts = {}) => {
|
|
488
|
+
const recorder = acquireRecorder();
|
|
489
|
+
recorder.configure({
|
|
490
|
+
sink: opts.sink,
|
|
491
|
+
redactKeys: opts.redactKeys,
|
|
492
|
+
includeStateKeys: opts.includeStateKeys,
|
|
493
|
+
includeFeedTopics: opts.includeFeedTopics,
|
|
494
|
+
...opts.keyframeEvery !== undefined ? { keyframeEvery: opts.keyframeEvery } : {},
|
|
495
|
+
...opts.fullKeyframeRatio !== undefined ? { fullKeyframeRatio: opts.fullKeyframeRatio } : {},
|
|
496
|
+
...opts.sample !== undefined ? { sample: opts.sample } : {},
|
|
497
|
+
...opts.maxEventsPerSecond !== undefined ? { maxEventsPerSecond: opts.maxEventsPerSecond } : {},
|
|
498
|
+
origin: opts.origin,
|
|
499
|
+
now: opts.now
|
|
500
|
+
});
|
|
501
|
+
if (opts.store)
|
|
502
|
+
recorder.registerStore(opts.origin ?? "host", opts.store);
|
|
503
|
+
const allowed = opts.force === true || !isProd();
|
|
504
|
+
if (!allowed) {
|
|
505
|
+
warnOnce("recording disabled in production — pass force: true to enable (staging demos only).");
|
|
506
|
+
}
|
|
507
|
+
return {
|
|
508
|
+
start() {
|
|
509
|
+
if (!allowed)
|
|
510
|
+
return;
|
|
511
|
+
recorder.start();
|
|
512
|
+
},
|
|
513
|
+
stop() {
|
|
514
|
+
recorder.stop();
|
|
515
|
+
returnToLive();
|
|
516
|
+
},
|
|
517
|
+
isRecording() {
|
|
518
|
+
return recorder.isRecording();
|
|
519
|
+
},
|
|
520
|
+
getBundle() {
|
|
521
|
+
return structuredClone(recorder.getBundle());
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
};
|
|
525
|
+
var registerStore = (name, store) => {
|
|
526
|
+
acquireRecorder().registerStore(name, store);
|
|
527
|
+
};
|
|
528
|
+
var unregisterStore = (name) => {
|
|
529
|
+
acquireRecorder().unregisterStore(name);
|
|
530
|
+
};
|
|
531
|
+
// src/player.ts
|
|
532
|
+
var asRaw = (slices) => ({ __brand: "raw", slices });
|
|
533
|
+
var foldStateAt = (bundle, storeKey, tMs) => {
|
|
534
|
+
let acc;
|
|
535
|
+
for (const ev of bundle.events) {
|
|
536
|
+
if (ev.tsMs > tMs)
|
|
537
|
+
break;
|
|
538
|
+
if (ev.storeKey !== storeKey)
|
|
539
|
+
continue;
|
|
540
|
+
if (ev.type !== "state_snapshot")
|
|
541
|
+
continue;
|
|
542
|
+
const snap = ev;
|
|
543
|
+
if (snap.kind === "keyframe") {
|
|
544
|
+
acc = { ...snap.slices };
|
|
545
|
+
} else if (snap.kind === "delta") {
|
|
546
|
+
if (!acc) {
|
|
547
|
+
warnOnce("delta before keyframe — skipped");
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
acc = { ...acc, ...snap.slices };
|
|
551
|
+
for (const k of snap.removedSlices ?? [])
|
|
552
|
+
delete acc[k];
|
|
553
|
+
} else {
|
|
554
|
+
warnOnce(`unknown snapshot kind — skipped`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return acc === undefined ? undefined : asRaw(acc);
|
|
558
|
+
};
|
|
559
|
+
var snapshotStoreKeys = (bundle) => {
|
|
560
|
+
const keys = new Set;
|
|
561
|
+
for (const ev of bundle.events) {
|
|
562
|
+
if (ev.type === "state_snapshot")
|
|
563
|
+
keys.add(ev.storeKey);
|
|
564
|
+
}
|
|
565
|
+
return keys;
|
|
566
|
+
};
|
|
567
|
+
var foldFeedAt = (bundle, origin, topic, tMs) => {
|
|
568
|
+
const relevant = bundle.events.filter((ev) => ev.type === "feed_message" && ev.origin === origin && ev.topic === topic && ev.tsMs <= tMs).sort((a, b) => a.seq - b.seq || (a.feedSeq ?? 0) - (b.feedSeq ?? 0));
|
|
569
|
+
if (relevant.length === 0)
|
|
570
|
+
return;
|
|
571
|
+
let base;
|
|
572
|
+
for (const ev of relevant)
|
|
573
|
+
if (ev.channel === "sow_page")
|
|
574
|
+
base = ev;
|
|
575
|
+
if (!base)
|
|
576
|
+
return;
|
|
577
|
+
const rows = new Map;
|
|
578
|
+
let nextKey = 0;
|
|
579
|
+
for (const row of base.rows ?? []) {
|
|
580
|
+
const key = keyOf(row) ?? `__idx${nextKey++}`;
|
|
581
|
+
rows.set(key, row);
|
|
582
|
+
}
|
|
583
|
+
let matched = base.matched ?? rows.size;
|
|
584
|
+
const lastFeedSeq = new Map;
|
|
585
|
+
const baseSeq = base.seq;
|
|
586
|
+
for (const ev of relevant) {
|
|
587
|
+
if (ev.seq <= baseSeq)
|
|
588
|
+
continue;
|
|
589
|
+
const key = ev.key;
|
|
590
|
+
if (ev.channel === "tick" || ev.channel === "ack") {
|
|
591
|
+
if (key === undefined)
|
|
592
|
+
continue;
|
|
593
|
+
const evFeedSeq = ev.feedSeq ?? 0;
|
|
594
|
+
const seenFeedSeq = lastFeedSeq.get(key) ?? -1;
|
|
595
|
+
if (evFeedSeq >= seenFeedSeq) {
|
|
596
|
+
rows.set(key, ev.data);
|
|
597
|
+
lastFeedSeq.set(key, evFeedSeq);
|
|
598
|
+
}
|
|
599
|
+
} else if (ev.channel === "oof") {
|
|
600
|
+
if (key !== undefined && rows.delete(key)) {
|
|
601
|
+
matched = Math.max(0, matched - 1);
|
|
602
|
+
lastFeedSeq.delete(key);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return { rows: [...rows.values()], matched };
|
|
607
|
+
};
|
|
608
|
+
var keyOf = (row) => {
|
|
609
|
+
if (row && typeof row === "object") {
|
|
610
|
+
const r = row;
|
|
611
|
+
for (const cand of ["symbol", "orderId", "id", "key"]) {
|
|
612
|
+
if (typeof r[cand] === "string")
|
|
613
|
+
return r[cand];
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return;
|
|
617
|
+
};
|
|
618
|
+
var routeAt = (bundle, tMs) => {
|
|
619
|
+
let last;
|
|
620
|
+
for (const ev of bundle.events) {
|
|
621
|
+
if (ev.tsMs > tMs)
|
|
622
|
+
break;
|
|
623
|
+
if (ev.type === "route")
|
|
624
|
+
last = ev;
|
|
625
|
+
}
|
|
626
|
+
return last;
|
|
627
|
+
};
|
|
628
|
+
var eventId = (ev) => {
|
|
629
|
+
let content = "";
|
|
630
|
+
const e = ev;
|
|
631
|
+
if (ev.type === "action")
|
|
632
|
+
content = JSON.stringify(e["action"]);
|
|
633
|
+
else if (ev.type === "state_snapshot")
|
|
634
|
+
content = JSON.stringify(e["slices"]);
|
|
635
|
+
else if (ev.type === "feed_message")
|
|
636
|
+
content = JSON.stringify(e["data"] ?? e["rows"]);
|
|
637
|
+
else if (ev.type === "route")
|
|
638
|
+
content = String(e["path"]);
|
|
639
|
+
return `${ev.seq}|${ev.storeKey}|${ev.type}|${ev.tsMs}|${content}`;
|
|
640
|
+
};
|
|
641
|
+
var diffBundles = (a, b) => {
|
|
642
|
+
const aIds = new Set(a.events.map(eventId));
|
|
643
|
+
const bIds = new Set(b.events.map(eventId));
|
|
644
|
+
const added = b.events.filter((ev) => !aIds.has(eventId(ev)));
|
|
645
|
+
const removed = a.events.filter((ev) => !bIds.has(eventId(ev)));
|
|
646
|
+
const tA = a.duration;
|
|
647
|
+
const tB = b.duration;
|
|
648
|
+
const keys = new Set([...snapshotStoreKeys(a), ...snapshotStoreKeys(b)]);
|
|
649
|
+
const changedStoreKeys = [];
|
|
650
|
+
for (const key of keys) {
|
|
651
|
+
const sa = foldStateAt(a, key, tA);
|
|
652
|
+
const sb = foldStateAt(b, key, tB);
|
|
653
|
+
if (JSON.stringify(sa?.slices) !== JSON.stringify(sb?.slices))
|
|
654
|
+
changedStoreKeys.push(key);
|
|
655
|
+
}
|
|
656
|
+
return { added, removed, changedStoreKeys };
|
|
657
|
+
};
|
|
658
|
+
var createPlayer = (bundle) => ({
|
|
659
|
+
statesAt(tMs) {
|
|
660
|
+
const out = {};
|
|
661
|
+
for (const key of snapshotStoreKeys(bundle))
|
|
662
|
+
out[key] = foldStateAt(bundle, key, tMs);
|
|
663
|
+
return out;
|
|
664
|
+
},
|
|
665
|
+
routeAt(tMs) {
|
|
666
|
+
return routeAt(bundle, tMs);
|
|
667
|
+
},
|
|
668
|
+
feedAt(origin, topic, tMs) {
|
|
669
|
+
return foldFeedAt(bundle, origin, topic, tMs);
|
|
670
|
+
},
|
|
671
|
+
diffBundles(a, b) {
|
|
672
|
+
return diffBundles(a, b);
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
// src/bundle-io.ts
|
|
676
|
+
class BundleValidationError extends Error {
|
|
677
|
+
constructor(message) {
|
|
678
|
+
super(message);
|
|
679
|
+
this.name = "BundleValidationError";
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
var validateBundle = (b) => {
|
|
683
|
+
if (!b || typeof b !== "object")
|
|
684
|
+
throw new BundleValidationError("bundle is not an object");
|
|
685
|
+
const bundle = b;
|
|
686
|
+
if (bundle.version !== DELTA_FORMAT_VERSION) {
|
|
687
|
+
throw new BundleValidationError(`unsupported bundle version "${String(bundle.version)}" — expected "${DELTA_FORMAT_VERSION}". ` + `A different fold algorithm may be required.`);
|
|
688
|
+
}
|
|
689
|
+
if (typeof bundle.schemaVersion !== "number" || bundle.schemaVersion > SCHEMA_VERSION) {
|
|
690
|
+
throw new BundleValidationError(`unknown schemaVersion ${String(bundle.schemaVersion)} — this runtime understands up to ${SCHEMA_VERSION}`);
|
|
691
|
+
}
|
|
692
|
+
if (!Array.isArray(bundle.events)) {
|
|
693
|
+
throw new BundleValidationError("bundle.events must be an array");
|
|
694
|
+
}
|
|
695
|
+
if (!bundle.mode) {
|
|
696
|
+
throw new BundleValidationError("bundle.mode is missing");
|
|
697
|
+
}
|
|
698
|
+
return b;
|
|
699
|
+
};
|
|
700
|
+
var exportBundle = () => {
|
|
701
|
+
const bundle = acquireRecorder().getBundle();
|
|
702
|
+
return structuredClone(bundle);
|
|
703
|
+
};
|
|
704
|
+
var importBundle = (json) => {
|
|
705
|
+
const parsed = typeof json === "string" ? JSON.parse(json) : json;
|
|
706
|
+
return validateBundle(parsed);
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
// src/index.ts
|
|
710
|
+
var VERSION = "0.1.0-alpha.1";
|
|
711
|
+
export {
|
|
712
|
+
unregisterStore,
|
|
713
|
+
subscribeReplayMode,
|
|
714
|
+
serializeForDisplay,
|
|
715
|
+
seek,
|
|
716
|
+
routeAt,
|
|
717
|
+
returnToLive,
|
|
718
|
+
replayEnhancer,
|
|
719
|
+
registerStore,
|
|
720
|
+
importBundle,
|
|
721
|
+
getReplayMode,
|
|
722
|
+
foldStateAt,
|
|
723
|
+
foldFeedAt,
|
|
724
|
+
exportBundle,
|
|
725
|
+
diffBundles,
|
|
726
|
+
createPlayer,
|
|
727
|
+
configureReplay,
|
|
728
|
+
applyReplayState,
|
|
729
|
+
__redact,
|
|
730
|
+
VERSION,
|
|
731
|
+
SCHEMA_VERSION,
|
|
732
|
+
DELTA_FORMAT_VERSION
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
//# debugId=093C993363C31DB564756E2164756E21
|
|
736
|
+
//# sourceMappingURL=index.js.map
|