@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.
@@ -0,0 +1,497 @@
1
+ // src/react/index.ts
2
+ import { useSyncExternalStore } from "react";
3
+
4
+ // src/bundle.ts
5
+ var SCHEMA_VERSION = 2;
6
+ var DELTA_FORMAT_VERSION = "1.0";
7
+
8
+ // src/env.ts
9
+ var isProd = () => {
10
+ try {
11
+ return process.env.NODE_ENV === "production";
12
+ } catch {
13
+ return false;
14
+ }
15
+ };
16
+ var warned = new Set;
17
+ var warnOnce = (message) => {
18
+ if (isProd())
19
+ return;
20
+ if (warned.has(message))
21
+ return;
22
+ warned.add(message);
23
+ console.warn(`[rewindkit] ${message}`);
24
+ };
25
+
26
+ // src/redaction.ts
27
+ var REDACTED = "‹redacted›";
28
+ var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
29
+ var maskKeys = (value, redactKeys) => {
30
+ if (Array.isArray(value))
31
+ return value.map((item) => maskKeys(item, redactKeys));
32
+ if (isPlainObject(value)) {
33
+ const out = {};
34
+ for (const [key, val] of Object.entries(value)) {
35
+ if (redactKeys.has(key)) {
36
+ warnOnce(`redacted key "${key}" — value masked before capture`);
37
+ out[key] = REDACTED;
38
+ } else {
39
+ out[key] = maskKeys(val, redactKeys);
40
+ }
41
+ }
42
+ return out;
43
+ }
44
+ return value;
45
+ };
46
+ var __maskDeep = (value, redactKeys) => redactKeys && redactKeys.length ? maskKeys(value, new Set(redactKeys)) : value;
47
+ var __redact = (value, opts) => {
48
+ const redactKeys = new Set(opts.redactKeys ?? []);
49
+ if (!isPlainObject(value)) {
50
+ return redactKeys.size ? maskKeys(value, redactKeys) : value;
51
+ }
52
+ let entries = Object.entries(value);
53
+ if (opts.includeStateKeys === undefined) {
54
+ warnOnce("includeStateKeys is not set — capturing ALL slices. Set an allowlist in production (PII risk).");
55
+ } else {
56
+ const allow = new Set(opts.includeStateKeys);
57
+ entries = entries.filter(([key]) => allow.has(key));
58
+ }
59
+ const out = {};
60
+ for (const [key, val] of entries) {
61
+ out[key] = redactKeys.size ? maskKeys(val, redactKeys) : val;
62
+ }
63
+ return out;
64
+ };
65
+ var DEFAULTS = { depth: 6, nodes: 200, strLen: 1e4 };
66
+ var serializeForDisplay = (value, opts = {}) => {
67
+ const cfg = { ...DEFAULTS, ...opts };
68
+ let budget = cfg.nodes;
69
+ const walk = (v, depth) => {
70
+ budget -= 1;
71
+ if (budget < 0)
72
+ return "… (capped)";
73
+ if (typeof v === "string") {
74
+ return v.length > cfg.strLen ? `${v.slice(0, cfg.strLen)}… (${v.length} chars)` : v;
75
+ }
76
+ if (typeof v === "bigint")
77
+ return `${v}n`;
78
+ if (typeof v === "function")
79
+ return `‹function ${v.name || "anonymous"}›`;
80
+ if (typeof v === "symbol")
81
+ return v.toString();
82
+ if (v === null || typeof v !== "object")
83
+ return v;
84
+ if (v instanceof Date)
85
+ return `Date(${v.toISOString()})`;
86
+ if (v instanceof Error)
87
+ return `${v.name}: ${v.message}`;
88
+ if (v instanceof Map) {
89
+ if (v.size === 0)
90
+ return "Map(0) {}";
91
+ if (depth >= cfg.depth)
92
+ return `Map(${v.size}) …`;
93
+ const obj = {};
94
+ for (const [k, val] of v)
95
+ obj[String(k)] = walk(val, depth + 1);
96
+ return { "‹Map›": obj };
97
+ }
98
+ if (v instanceof Set) {
99
+ if (v.size === 0)
100
+ return "Set(0) {}";
101
+ if (depth >= cfg.depth)
102
+ return `Set(${v.size}) …`;
103
+ return { "‹Set›": [...v].map((item) => walk(item, depth + 1)) };
104
+ }
105
+ if (Array.isArray(v)) {
106
+ if (v.length === 0)
107
+ return [];
108
+ if (depth >= cfg.depth)
109
+ return `[Array(${v.length})]`;
110
+ return v.map((item) => walk(item, depth + 1));
111
+ }
112
+ const keys = Object.keys(v);
113
+ if (keys.length === 0)
114
+ return {};
115
+ if (depth >= cfg.depth)
116
+ return `{…(${keys.length} keys)}`;
117
+ const out = {};
118
+ for (const key of keys)
119
+ out[key] = walk(v[key], depth + 1);
120
+ return out;
121
+ };
122
+ return walk(value, 0);
123
+ };
124
+
125
+ // src/recorder.ts
126
+ var DEFAULT_CONFIG = {
127
+ keyframeEvery: 25,
128
+ fullKeyframeRatio: 8,
129
+ sample: 1,
130
+ maxEventsPerSecond: 200
131
+ };
132
+ var UNSERIALIZABLE = "__unserializable";
133
+ var KEY = "__REPLAYKIT_RECORDER__";
134
+ var createRecorder = () => {
135
+ const bundle = {
136
+ version: DELTA_FORMAT_VERSION,
137
+ schemaVersion: SCHEMA_VERSION,
138
+ mode: "state-snapshot",
139
+ sessionId: crypto.randomUUID(),
140
+ startTime: Date.now(),
141
+ duration: 0,
142
+ remotes: [],
143
+ events: []
144
+ };
145
+ const stores = new Map;
146
+ const rawStash = new Map;
147
+ const prevLiveSlices = new Map;
148
+ const actionCount = new Map;
149
+ let windowStart = 0;
150
+ let windowCount = 0;
151
+ const suppression = { depth: 0 };
152
+ const rec = {
153
+ suppression,
154
+ config: { ...DEFAULT_CONFIG },
155
+ recording: false,
156
+ origin: "host",
157
+ now: () => Date.now(),
158
+ start() {
159
+ rec.recording = true;
160
+ for (const [name, store] of stores)
161
+ emitKeyframe(name, store.getState(), true);
162
+ },
163
+ stop() {
164
+ rec.recording = false;
165
+ },
166
+ isRecording() {
167
+ return rec.recording;
168
+ },
169
+ configure(opts) {
170
+ const { origin, now, ...cfg } = opts;
171
+ rec.config = { ...rec.config, ...cfg };
172
+ if (origin !== undefined)
173
+ rec.origin = origin;
174
+ if (now !== undefined) {
175
+ rec.now = now;
176
+ bundle.startTime = now();
177
+ }
178
+ },
179
+ registerStore(name, store) {
180
+ if (stores.has(name)) {
181
+ warnOnce(`store "${name}" already registered — names must be unique (ignoring re-register)`);
182
+ return;
183
+ }
184
+ stores.set(name, store);
185
+ const registeredAtMs = rec.now() - bundle.startTime;
186
+ bundle.remotes.push({ name, storeKey: name, registeredAtMs });
187
+ if (rec.recording)
188
+ emitKeyframe(name, store.getState(), true);
189
+ },
190
+ unregisterStore(name) {
191
+ stores.delete(name);
192
+ },
193
+ getStore(name) {
194
+ return stores.get(name);
195
+ },
196
+ captureAction(storeKey, action) {
197
+ if (!armed())
198
+ return;
199
+ if (rec.config.sample < 1 && Math.random() > rec.config.sample)
200
+ return;
201
+ if (throttled())
202
+ return;
203
+ append({
204
+ type: "action",
205
+ tsMs: tsMs(),
206
+ origin: rec.origin,
207
+ storeKey,
208
+ seq: nextSeq(),
209
+ action: { type: action.type, payload: action.payload }
210
+ });
211
+ },
212
+ captureKeyframe(storeKey) {
213
+ if (!armed())
214
+ return;
215
+ const store = stores.get(storeKey);
216
+ if (!store)
217
+ return;
218
+ const count = (actionCount.get(storeKey) ?? 0) + 1;
219
+ actionCount.set(storeKey, count);
220
+ const fullEvery = rec.config.keyframeEvery * rec.config.fullKeyframeRatio;
221
+ if (count % rec.config.keyframeEvery !== 0)
222
+ return;
223
+ const full = count % fullEvery === 0;
224
+ emitKeyframe(storeKey, store.getState(), full);
225
+ },
226
+ captureRoute(path, search) {
227
+ if (!armed())
228
+ return;
229
+ if (throttled())
230
+ return;
231
+ append({
232
+ type: "route",
233
+ tsMs: tsMs(),
234
+ origin: rec.origin,
235
+ storeKey: rec.origin,
236
+ seq: nextSeq(),
237
+ path,
238
+ search
239
+ });
240
+ },
241
+ pushFeedEvent(ev) {
242
+ if (!armed())
243
+ return;
244
+ const topics = rec.config.includeFeedTopics;
245
+ if (!topics || topics.length === 0) {
246
+ warnOnce("includeFeedTopics is empty — no feed_message recorded. Allowlist a topic.");
247
+ return;
248
+ }
249
+ if (!topics.includes(ev.topic))
250
+ return;
251
+ if (throttled())
252
+ return;
253
+ const redactKeys = rec.config.redactKeys;
254
+ const rows = ev.rows ? structuredClone(__maskDeep(ev.rows, redactKeys)) : ev.rows;
255
+ const data = ev.data !== undefined ? structuredClone(__maskDeep(ev.data, redactKeys)) : ev.data;
256
+ append({
257
+ ...ev,
258
+ rows,
259
+ data,
260
+ type: "feed_message",
261
+ tsMs: tsMs(),
262
+ origin: rec.origin,
263
+ storeKey: ev.storeKey ?? rec.origin,
264
+ seq: nextSeq()
265
+ });
266
+ },
267
+ suppress() {
268
+ suppression.depth += 1;
269
+ },
270
+ unsuppress() {
271
+ suppression.depth -= 1;
272
+ if (suppression.depth < 0) {
273
+ suppression.depth = 0;
274
+ if (!isProdSafe())
275
+ throw new Error("[replaykit] suppression depth went negative");
276
+ }
277
+ },
278
+ getBundle() {
279
+ bundle.duration = rawStashMaxTs();
280
+ return bundle;
281
+ }
282
+ };
283
+ const armed = () => rec.recording && suppression.depth === 0;
284
+ const tsMs = () => rec.now() - bundle.startTime;
285
+ const nextSeq = () => bundle.events.length;
286
+ const append = (event) => {
287
+ bundle.events.push(event);
288
+ rec.config.sink?.push(event);
289
+ };
290
+ const throttled = () => {
291
+ const t = rec.now();
292
+ if (t - windowStart >= 1000) {
293
+ windowStart = t;
294
+ windowCount = 0;
295
+ }
296
+ if (windowCount >= rec.config.maxEventsPerSecond)
297
+ return true;
298
+ windowCount += 1;
299
+ return false;
300
+ };
301
+ const emitKeyframe = (storeKey, state, full) => {
302
+ const liveSlices = state && typeof state === "object" && !Array.isArray(state) ? state : {};
303
+ const redacted = __redact(state, {
304
+ includeStateKeys: rec.config.includeStateKeys,
305
+ redactKeys: rec.config.redactKeys
306
+ });
307
+ const prevStash = rawStash.get(storeKey);
308
+ const prevLive = prevLiveSlices.get(storeKey);
309
+ if (full || !prevStash || !prevLive) {
310
+ const cloned = cloneSlices(redacted);
311
+ rawStash.set(storeKey, cloned);
312
+ prevLiveSlices.set(storeKey, { ...liveSlices });
313
+ append({
314
+ type: "state_snapshot",
315
+ kind: "keyframe",
316
+ tsMs: tsMs(),
317
+ origin: rec.origin,
318
+ storeKey,
319
+ seq: nextSeq(),
320
+ slices: cloned
321
+ });
322
+ return;
323
+ }
324
+ const changedStash = {};
325
+ const nextStash = { ...prevStash };
326
+ for (const key of Object.keys(redacted)) {
327
+ if (!Object.is(prevLive[key], liveSlices[key])) {
328
+ const clonedVal = cloneSingle(redacted[key]);
329
+ changedStash[key] = clonedVal;
330
+ nextStash[key] = clonedVal;
331
+ }
332
+ }
333
+ const removed = Object.keys(prevStash).filter((key) => !(key in redacted));
334
+ for (const key of removed)
335
+ delete nextStash[key];
336
+ rawStash.set(storeKey, nextStash);
337
+ prevLiveSlices.set(storeKey, { ...liveSlices });
338
+ append({
339
+ type: "state_snapshot",
340
+ kind: "delta",
341
+ tsMs: tsMs(),
342
+ origin: rec.origin,
343
+ storeKey,
344
+ seq: nextSeq(),
345
+ slices: changedStash,
346
+ ...removed.length ? { removedSlices: removed } : {}
347
+ });
348
+ };
349
+ const cloneSingle = (val) => {
350
+ try {
351
+ return structuredClone(val);
352
+ } catch {
353
+ return { [UNSERIALIZABLE]: true };
354
+ }
355
+ };
356
+ const cloneSlices = (state) => {
357
+ const out = {};
358
+ for (const [key, val] of Object.entries(state))
359
+ out[key] = cloneSingle(val);
360
+ return out;
361
+ };
362
+ const rawStashMaxTs = () => {
363
+ let max = 0;
364
+ for (const ev of bundle.events)
365
+ if (ev.tsMs > max)
366
+ max = ev.tsMs;
367
+ return max;
368
+ };
369
+ return rec;
370
+ };
371
+ var isProdSafe = () => {
372
+ try {
373
+ return process.env.NODE_ENV === "production";
374
+ } catch {
375
+ return false;
376
+ }
377
+ };
378
+ var acquireRecorder = () => {
379
+ const g = globalThis;
380
+ const existing = g[KEY];
381
+ if (existing)
382
+ return existing;
383
+ const r = createRecorder();
384
+ g[KEY] = r;
385
+ return g[KEY] ?? r;
386
+ };
387
+ var __peekRecorder = () => globalThis[KEY];
388
+
389
+ // src/enhancer.ts
390
+ var RESTORE_TYPE = "__REPLAYKIT_RESTORE";
391
+ var isRestore = (action) => typeof action === "object" && action !== null && action.type === RESTORE_TYPE;
392
+ var wrapReducer = (reducer) => (state, action) => {
393
+ if (isRestore(action)) {
394
+ const base = state ?? {};
395
+ const next = { ...base, ...action.slices };
396
+ for (const key of action.removedSlices ?? [])
397
+ delete next[key];
398
+ return next;
399
+ }
400
+ return reducer(state, action);
401
+ };
402
+ var enhancer = (createStore) => (reducer, preloadedState) => {
403
+ const store = createStore(wrapReducer(reducer), preloadedState);
404
+ const dispatch = (action) => {
405
+ const recorder = __peekRecorder();
406
+ if (!recorder)
407
+ return store.dispatch(action);
408
+ const result = store.dispatch(action);
409
+ if (!isRestore(action) && typeof action.type === "string") {
410
+ const storeKey = recorder.origin;
411
+ recorder.captureAction(storeKey, {
412
+ type: action.type,
413
+ payload: action.payload
414
+ });
415
+ recorder.captureKeyframe(storeKey);
416
+ }
417
+ return result;
418
+ };
419
+ return { ...store, dispatch };
420
+ };
421
+ var replayEnhancer = enhancer;
422
+
423
+ // src/replay.ts
424
+ var mode = "live";
425
+ var listeners = new Set;
426
+ var getReplayMode = () => mode;
427
+ var subscribeReplayMode = (cb) => {
428
+ listeners.add(cb);
429
+ cb(mode);
430
+ return () => {
431
+ listeners.delete(cb);
432
+ };
433
+ };
434
+ var notify = () => {
435
+ for (const cb of listeners)
436
+ cb(mode);
437
+ };
438
+ var setMode = (next) => {
439
+ if (next === mode)
440
+ return;
441
+ const recorder = acquireRecorder();
442
+ if (next === "history") {
443
+ recorder.suppress();
444
+ mode = "history";
445
+ } else {
446
+ mode = "live";
447
+ recorder.unsuppress();
448
+ }
449
+ notify();
450
+ };
451
+ var applyReplayState = (snapshot, storeKey) => {
452
+ const recorder = acquireRecorder();
453
+ const key = storeKey ?? recorder.origin;
454
+ const store = recorder.getStore(key);
455
+ if (!store) {
456
+ warnOnce(`applyReplayState: no store registered for "${key}"`);
457
+ return;
458
+ }
459
+ recorder.suppress();
460
+ try {
461
+ const action = { type: RESTORE_TYPE, slices: { ...snapshot.slices } };
462
+ store.dispatch(action);
463
+ } finally {
464
+ recorder.unsuppress();
465
+ }
466
+ };
467
+ var seek = (player, tMs) => {
468
+ const states = player.statesAt(tMs);
469
+ if (mode !== "history")
470
+ setMode("history");
471
+ const recorder = acquireRecorder();
472
+ recorder.suppress();
473
+ try {
474
+ for (const [storeKey, snap] of Object.entries(states)) {
475
+ if (snap)
476
+ applyReplayState(snap, storeKey);
477
+ }
478
+ } finally {
479
+ recorder.unsuppress();
480
+ }
481
+ };
482
+ var returnToLive = () => {
483
+ setMode("live");
484
+ const recorder = acquireRecorder();
485
+ if (recorder.suppression.depth !== 0) {
486
+ warnOnce("suppression depth non-zero after returnToLive — a suppress/unsuppress pair leaked");
487
+ }
488
+ };
489
+
490
+ // src/react/index.ts
491
+ var useReplayMode = () => useSyncExternalStore(subscribeReplayMode, getReplayMode, getReplayMode);
492
+ export {
493
+ useReplayMode
494
+ };
495
+
496
+ //# debugId=EA02C4FDCADFE80364756E2164756E21
497
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,16 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["..\\src\\react\\index.ts", "..\\src\\bundle.ts", "..\\src\\env.ts", "..\\src\\redaction.ts", "..\\src\\recorder.ts", "..\\src\\enhancer.ts", "..\\src\\replay.ts"],
4
+ "sourcesContent": [
5
+ "// @rewindkit/runtime/react — TIER 3 React companion.\n//\n// useReplayMode is for RENDERING CHROME ONLY. A side-channel handler (a WS onmessage, an in-flight\n// effect registry) MUST gate on getReplayMode()/subscribeReplayMode from the core entry — NOT this\n// hook. A React render lags the event loop, so the hook can read a stale `live` and stomp restored\n// history (the TQ1-A bug). The hook merely keeps panel chrome in sync.\n//\n// ReplayProvider/ReplayPanel (the panel UI) are a separate later task (specs/03) — not built here.\nimport { useSyncExternalStore } from \"react\";\nimport { getReplayMode, subscribeReplayMode, type ReplayMode } from \"../replay\";\n\nexport const useReplayMode = (): ReplayMode =>\n useSyncExternalStore(subscribeReplayMode, getReplayMode, getReplayMode);\n",
6
+ "// SessionBundle + event types — the on-the-wire/export format (ADR-4 + specs/05 §2).\n//\n// TWO distinct version fields, never overloaded (ADR-4):\n// - `version: \"1.0\"` — the delta-format discriminant. Drives the fold math. Bump only when\n// the fold algorithm changes.\n// - `schemaVersion` — a separate envelope schema id. Bump on envelope/event-shape changes.\n// Starts at 2 because `feed_message` (specs/05 §2) is included.\n\nexport type ReplayEventType =\n | \"action\"\n | \"state_snapshot\"\n | \"component_render\"\n | \"route\"\n | \"feed_message\";\n\nexport interface BaseEvent {\n // Tolerate unknown future types — old players skip them (the fold never throws).\n type: ReplayEventType | (string & {});\n tsMs: number; // ms since startTime\n origin: \"host\" | string; // remote name\n storeKey: string; // 'host' or remote name\n seq: number; // monotonic per-bundle ordinal\n}\n\nexport interface ActionEvent extends BaseEvent {\n type: \"action\";\n action: { type: string; payload?: unknown };\n}\n\nexport interface StateSnapshotEvent extends BaseEvent {\n type: \"state_snapshot\";\n kind: \"keyframe\" | \"delta\";\n slices: Record<string, unknown>; // RAW values (the raw stash)\n removedSlices?: string[]; // slices deleted since the prior snapshot\n}\n\nexport interface ComponentRenderEvent extends BaseEvent {\n type: \"component_render\";\n componentId: string;\n props?: Record<string, unknown>; // redacted/serialized\n}\n\nexport interface RouteEvent extends BaseEvent {\n type: \"route\";\n path: string;\n search?: string;\n}\n\n// Captured by the app-owned mamps transport tap, NOT the Redux enhancer (specs/05 §2).\nexport interface FeedMessageEvent extends BaseEvent {\n type: \"feed_message\";\n channel: \"sow_page\" | \"tick\" | \"ack\" | \"oof\"; // kind of mamps traffic\n topic: string; // 'quotes' | 'orders' | ...\n request?: { filter?: string; orderBy?: string; topN?: number; skipN?: number };\n rows?: unknown[]; // sow_page rows (raw, redacted)\n matched?: number; // sow_page total match count\n key?: string; // tick/oof keyed record id (symbol or orderId)\n data?: unknown; // tick/oof record (raw, redacted)\n feedSeq?: number; // mamps Message.seq — server-wide \"newer-than\", tie-breaks tsMs (R-T9)\n}\n\nexport type ReplayEvent =\n | ActionEvent\n | StateSnapshotEvent\n | ComponentRenderEvent\n | RouteEvent\n | FeedMessageEvent\n | BaseEvent; // unknown-future fallthrough\n\nexport interface RemoteInfo {\n name: string;\n storeKey: string;\n registeredAtMs?: number; // absent => present from start (host/eager)\n}\n\nexport interface SessionBundle {\n version: \"1.0\"; // delta-format discriminant — NOT the schema id\n schemaVersion: number; // distinct envelope schema id (2: feed_message included)\n mode: \"state-snapshot\";\n sessionId: string;\n startTime: number; // epoch ms\n duration: number; // ms\n remotes: RemoteInfo[];\n events: ReplayEvent[]; // append-only, seq-ordered\n}\n\nexport const SCHEMA_VERSION = 2;\nexport const DELTA_FORMAT_VERSION = \"1.0\" as const;\n\n// Branded snapshot types make ADR-3's \"restore from RAW, never display\" a COMPILE-TIME guarantee.\n// A DisplaySnapshot cannot be passed where a RawSnapshot is required.\nexport interface RawSnapshot {\n readonly __brand: \"raw\";\n readonly slices: Record<string, unknown>;\n}\n\nexport interface DisplaySnapshot {\n readonly __brand: \"display\";\n readonly slices: Record<string, unknown>;\n}\n\n// The reconstructed feed window for replay (foldFeedAt), shaped like a mamps SowPage.\nexport interface FeedView {\n rows: unknown[];\n matched: number;\n}\n\nexport interface BundleDiff {\n // Events present in b but not a, keyed by seq+storeKey identity.\n added: ReplayEvent[];\n // Events present in a but not b.\n removed: ReplayEvent[];\n // storeKeys whose folded final state differs between the two bundles.\n changedStoreKeys: string[];\n}\n",
7
+ "// Env-gate — the ONE source of truth for \"are we in prod?\" (ADR-5 / guardrail 1).\n//\n// Vite statically replaces `process.env.NODE_ENV` with a literal at build, so no `process`\n// reference survives in the browser bundle. Where it isn't replaced and `process` is undefined\n// (raw browser, some test runners), the catch returns false — a dev-safe default.\n//\n// NEVER `typeof process` (undefined in a Vite browser bundle), NEVER `import.meta.env.DEV` alone\n// (undefined under node/test). This branch is NOT meaningfully unit-testable in bun (NODE_ENV is\n// set by the runner); it needs real-browser verification.\nexport const isProd = (): boolean => {\n try {\n return process.env.NODE_ENV === \"production\";\n } catch {\n return false;\n }\n};\n\n// Dev-only warnings that must fire AT MOST once per message (PII notice, fold anomalies, etc.).\nconst warned = new Set<string>();\n\nexport const warnOnce = (message: string): void => {\n // [fix auditor-L] Check isProd() BEFORE adding to the set so production never populates it.\n if (isProd()) return;\n if (warned.has(message)) return;\n warned.add(message);\n console.warn(`[rewindkit] ${message}`);\n};\n\n// Test-only: reset the warn-once memory so each test starts clean.\nexport const __resetWarnOnce = (): void => {\n warned.clear();\n};\n",
8
+ "// Redaction + display serialization (guardrail 5 + guardrail 3 + ADR-T2).\n//\n// TWO distinct, deliberately-separate transforms:\n// - __redact — what gets STORED in the raw stash / sent to the sink. Allowlist top-level\n// slices, deep-mask `redactKeys`, warn-once on PII. Lossless for kept values.\n// - serializeForDisplay — what the PANEL shows. Depth/size-capped, LOSSY (Map/Set/Date/Error\n// → previews). NEVER fed back into restore (re-drive uses the raw value).\nimport { warnOnce } from \"./env\";\n\nexport interface RedactOptions {\n includeStateKeys?: string[]; // ALLOWLIST of top-level slices; undefined => all (warn once)\n redactKeys?: string[]; // deep key names whose values are masked\n}\n\nconst REDACTED = \"‹redacted›\";\n\nconst isPlainObject = (v: unknown): v is Record<string, unknown> =>\n typeof v === \"object\" && v !== null && !Array.isArray(v);\n\n// Deep-mask any property whose key is in `redactKeys`, anywhere in the tree.\nconst maskKeys = (value: unknown, redactKeys: Set<string>): unknown => {\n if (Array.isArray(value)) return value.map((item) => maskKeys(item, redactKeys));\n if (isPlainObject(value)) {\n const out: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(value)) {\n if (redactKeys.has(key)) {\n warnOnce(`redacted key \"${key}\" — value masked before capture`);\n out[key] = REDACTED;\n } else {\n out[key] = maskKeys(val, redactKeys);\n }\n }\n return out;\n }\n return value;\n};\n\n// Deep-mask `redactKeys` anywhere in an arbitrary value (no allowlist). Used by the feed tap on\n// `rows`/`data`, where the value IS the record and its own top-level keys must be checked (ADR-T2).\nexport const __maskDeep = (value: unknown, redactKeys: string[] | undefined): unknown =>\n redactKeys && redactKeys.length ? maskKeys(value, new Set(redactKeys)) : value;\n\n// Apply the slice allowlist + key-masking to a top-level state object before it is captured.\n// `value` is expected to be a top-level state record (slices keyed by name).\nexport const __redact = (value: unknown, opts: RedactOptions): unknown => {\n const redactKeys = new Set(opts.redactKeys ?? []);\n\n if (!isPlainObject(value)) {\n // Not a slice record (e.g. a single feed payload) — just mask keys.\n return redactKeys.size ? maskKeys(value, redactKeys) : value;\n }\n\n let entries = Object.entries(value);\n if (opts.includeStateKeys === undefined) {\n warnOnce(\n \"includeStateKeys is not set — capturing ALL slices. Set an allowlist in production (PII risk).\",\n );\n } else {\n const allow = new Set(opts.includeStateKeys);\n entries = entries.filter(([key]) => allow.has(key));\n }\n\n const out: Record<string, unknown> = {};\n for (const [key, val] of entries) {\n out[key] = redactKeys.size ? maskKeys(val, redactKeys) : val;\n }\n return out;\n};\n\nexport interface DisplayOptions {\n depth?: number; // max nesting depth (default 6)\n nodes?: number; // max total nodes rendered (default 200)\n strLen?: number; // max string length before truncation (default 10000)\n}\n\nconst DEFAULTS: Required<DisplayOptions> = { depth: 6, nodes: 200, strLen: 10000 };\n\n// Lossy, capped serialization for the panel value-tree. Renders Map/Set/Date/Error/empty\n// meaningfully so the inspector is readable. DISTINCT from the raw stash (guardrail 3).\nexport const serializeForDisplay = (value: unknown, opts: DisplayOptions = {}): unknown => {\n const cfg = { ...DEFAULTS, ...opts };\n let budget = cfg.nodes;\n\n const walk = (v: unknown, depth: number): unknown => {\n budget -= 1;\n if (budget < 0) return \"… (capped)\";\n\n if (typeof v === \"string\") {\n return v.length > cfg.strLen ? `${v.slice(0, cfg.strLen)}… (${v.length} chars)` : v;\n }\n if (typeof v === \"bigint\") return `${v}n`;\n if (typeof v === \"function\") return `‹function ${v.name || \"anonymous\"}›`;\n if (typeof v === \"symbol\") return v.toString();\n if (v === null || typeof v !== \"object\") return v; // number/boolean/undefined\n\n if (v instanceof Date) return `Date(${v.toISOString()})`;\n if (v instanceof Error) return `${v.name}: ${v.message}`;\n if (v instanceof Map) {\n if (v.size === 0) return \"Map(0) {}\";\n if (depth >= cfg.depth) return `Map(${v.size}) …`;\n const obj: Record<string, unknown> = {};\n for (const [k, val] of v) obj[String(k)] = walk(val, depth + 1);\n return { \"‹Map›\": obj };\n }\n if (v instanceof Set) {\n if (v.size === 0) return \"Set(0) {}\";\n if (depth >= cfg.depth) return `Set(${v.size}) …`;\n return { \"‹Set›\": [...v].map((item) => walk(item, depth + 1)) };\n }\n\n if (Array.isArray(v)) {\n if (v.length === 0) return [];\n if (depth >= cfg.depth) return `[Array(${v.length})]`;\n return v.map((item) => walk(item, depth + 1));\n }\n\n const keys = Object.keys(v as Record<string, unknown>);\n if (keys.length === 0) return {};\n if (depth >= cfg.depth) return `{…(${keys.length} keys)}`;\n const out: Record<string, unknown> = {};\n for (const key of keys) out[key] = walk((v as Record<string, unknown>)[key], depth + 1);\n return out;\n };\n\n return walk(value, 0);\n};\n",
9
+ "// The singleton recorder (ADR-1, ADR-2, ADR-3 suppression).\n//\n// ONE recorder for the whole federation, acquired first-write-wins on a globalThis key so plain\n// apps and MF setups both converge on a single instance. Holds the ONE SessionBundle, a RAW stash\n// (separate from any display serialization — guardrail 3), a reentrancy-counted suppression depth\n// (guardrail 4), the registered stores, and the live config.\n//\n// Honest zero-cost (guardrail 2): capture entry points build NO payload object unless recording is\n// armed AND suppression depth is 0. The guard comes first; allocation comes after.\nimport type { Store } from \"redux\";\nimport {\n DELTA_FORMAT_VERSION,\n SCHEMA_VERSION,\n type FeedMessageEvent,\n type ReplayEvent,\n type SessionBundle,\n} from \"./bundle\";\nimport { __maskDeep, __redact } from \"./redaction\";\nimport { warnOnce } from \"./env\";\n\nexport interface ReplaySink {\n push(event: ReplayEvent): void;\n flush?(): Promise<void>;\n}\n\nexport interface RecorderConfig {\n sink?: ReplaySink;\n redactKeys?: string[];\n includeStateKeys?: string[]; // allowlist; undefined => all (warn)\n includeFeedTopics?: string[]; // feed-topic allowlist; empty/undefined => record no feed\n keyframeEvery: number; // FULL/DELTA cadence in actions (default 25)\n fullKeyframeRatio: number; // FULL every keyframeEvery*ratio (default 8)\n sample: number; // 0..1 action sampling (default 1)\n maxEventsPerSecond: number; // throttle cap (default 200)\n}\n\nconst DEFAULT_CONFIG: RecorderConfig = {\n keyframeEvery: 25,\n fullKeyframeRatio: 8,\n sample: 1,\n maxEventsPerSecond: 200,\n};\n\nexport interface Suppression {\n depth: number; // 0 => armed, >0 => suppressed\n}\n\nconst UNSERIALIZABLE = \"__unserializable\" as const;\n\nexport interface Recorder {\n readonly suppression: Suppression;\n config: RecorderConfig;\n recording: boolean;\n // origin tag for events that don't name a store (default \"host\").\n origin: string;\n // injectable clock so tests stay deterministic (defaults to Date.now).\n now: () => number;\n\n start(): void;\n stop(): void;\n isRecording(): boolean;\n\n configure(opts: Partial<RecorderConfig> & { origin?: string; now?: () => number }): void;\n\n registerStore(name: string, store: Store): void;\n unregisterStore(name: string): void;\n getStore(name: string): Store | undefined;\n\n // capture entry points — no-ops unless recording && suppression.depth === 0.\n captureAction(storeKey: string, action: { type: string; payload?: unknown }): void;\n captureKeyframe(storeKey: string): void;\n captureRoute(path: string, search?: string): void;\n pushFeedEvent(\n ev: Omit<FeedMessageEvent, \"tsMs\" | \"seq\" | \"type\" | \"origin\" | \"storeKey\"> & {\n storeKey?: string;\n },\n ): void;\n\n // suppression — covers BOTH the enhancer and the feed tap (specs/05 §4).\n suppress(): void;\n unsuppress(): void;\n\n getBundle(): SessionBundle;\n}\n\nconst KEY = \"__REPLAYKIT_RECORDER__\";\n\nconst createRecorder = (): Recorder => {\n const bundle: SessionBundle = {\n version: DELTA_FORMAT_VERSION,\n schemaVersion: SCHEMA_VERSION,\n mode: \"state-snapshot\",\n // [fix devil-L2] crypto.randomUUID() gives a proper UUID; Math.random() had only ~4 bits of collision resistance per segment.\n sessionId: crypto.randomUUID(),\n startTime: Date.now(),\n duration: 0,\n remotes: [],\n events: [],\n };\n\n const stores = new Map<string, Store>();\n // raw stash: last captured raw slice values per storeKey — the source of truth for fold/restore.\n const rawStash = new Map<string, Record<string, unknown>>();\n // [fix devil-H2] Track PREVIOUS LIVE slice references (pre-clone). Redux only returns a new\n // object reference for a slice that actually changed (reducer immutability contract). Comparing\n // live refs with Object.is correctly identifies changed slices; comparing clones always differs\n // because structuredClone always produces a fresh reference.\n const prevLiveSlices = new Map<string, Record<string, unknown>>();\n // per-store action counter driving the keyframe/delta cadence.\n const actionCount = new Map<string, number>();\n // throttle window: events emitted in the current 1s bucket.\n let windowStart = 0;\n let windowCount = 0;\n\n const suppression: Suppression = { depth: 0 };\n\n const rec: Recorder = {\n suppression,\n config: { ...DEFAULT_CONFIG },\n recording: false,\n origin: \"host\",\n now: () => Date.now(),\n\n start() {\n rec.recording = true;\n // Initial keyframe for every already-registered store so fold has a base from t=0 on.\n for (const [name, store] of stores) emitKeyframe(name, store.getState(), true);\n },\n stop() {\n rec.recording = false;\n // [fix devil-H3b] Do NOT reset suppression.depth here — returnToLive() in the ReplayHandle\n // drives the depth accounting through the normal unsuppress path. If we zeroed depth\n // directly and returnToLive() then called unsuppress(), depth would go negative and throw.\n },\n isRecording() {\n return rec.recording;\n },\n\n configure(opts) {\n const { origin, now, ...cfg } = opts;\n rec.config = { ...rec.config, ...cfg };\n if (origin !== undefined) rec.origin = origin;\n if (now !== undefined) {\n rec.now = now;\n // Re-anchor startTime to the injected clock so tsMs (= now - startTime) stays consistent.\n bundle.startTime = now();\n }\n },\n\n registerStore(name, store) {\n if (stores.has(name)) {\n warnOnce(`store \"${name}\" already registered — names must be unique (ignoring re-register)`);\n return;\n }\n stores.set(name, store);\n const registeredAtMs = rec.now() - bundle.startTime;\n bundle.remotes.push({ name, storeKey: name, registeredAtMs });\n // Lazy remote: synthetic keyframe at registration so state exists from this point on.\n // State BEFORE registration stays honestly absent (no entry in the raw stash / fold).\n if (rec.recording) emitKeyframe(name, store.getState(), true);\n },\n unregisterStore(name) {\n stores.delete(name);\n },\n getStore(name) {\n return stores.get(name);\n },\n\n captureAction(storeKey, action) {\n if (!armed()) return; // guardrail 2: no payload built past this point unless armed\n if (rec.config.sample < 1 && Math.random() > rec.config.sample) return;\n if (throttled()) return;\n // TODO(redaction, BTE): action payloads are not yet masked — redactKeys currently covers\n // state slices + feed only. A token/password in an action payload leaks into the bundle.\n // Fix: payload: redactKeys?.length ? __maskDeep(action.payload, redactKeys) : action.payload\n append({\n type: \"action\",\n tsMs: tsMs(),\n origin: rec.origin,\n storeKey,\n seq: nextSeq(),\n action: { type: action.type, payload: action.payload },\n });\n },\n\n captureKeyframe(storeKey) {\n if (!armed()) return;\n const store = stores.get(storeKey);\n if (!store) return;\n const count = (actionCount.get(storeKey) ?? 0) + 1;\n actionCount.set(storeKey, count);\n const fullEvery = rec.config.keyframeEvery * rec.config.fullKeyframeRatio;\n if (count % rec.config.keyframeEvery !== 0) return; // not a snapshot tick\n const full = count % fullEvery === 0;\n emitKeyframe(storeKey, store.getState(), full);\n },\n\n captureRoute(path, search) {\n if (!armed()) return;\n if (throttled()) return;\n append({\n type: \"route\",\n tsMs: tsMs(),\n origin: rec.origin,\n storeKey: rec.origin,\n seq: nextSeq(),\n path,\n search,\n });\n },\n\n pushFeedEvent(ev) {\n if (!armed()) return;\n const topics = rec.config.includeFeedTopics;\n if (!topics || topics.length === 0) {\n warnOnce(\"includeFeedTopics is empty — no feed_message recorded. Allowlist a topic.\");\n return;\n }\n if (!topics.includes(ev.topic)) return;\n if (throttled()) return;\n // [fix devil-M5/auditor] Redact rows/data at capture (ADR-T2) and structuredClone them so\n // the caller mutating their array/object after pushFeedEvent can't corrupt the bundle.\n const redactKeys = rec.config.redactKeys;\n const rows = ev.rows\n ? (structuredClone(__maskDeep(ev.rows, redactKeys)) as unknown[])\n : ev.rows;\n const data =\n ev.data !== undefined ? structuredClone(__maskDeep(ev.data, redactKeys)) : ev.data;\n append({\n ...ev,\n rows,\n data,\n type: \"feed_message\",\n tsMs: tsMs(),\n origin: rec.origin,\n storeKey: ev.storeKey ?? rec.origin,\n seq: nextSeq(),\n });\n },\n\n suppress() {\n suppression.depth += 1;\n },\n unsuppress() {\n suppression.depth -= 1;\n if (suppression.depth < 0) {\n suppression.depth = 0;\n if (!isProdSafe()) throw new Error(\"[replaykit] suppression depth went negative\");\n }\n },\n\n getBundle() {\n bundle.duration = rawStashMaxTs();\n return bundle;\n },\n };\n\n // ---- internals ----\n\n const armed = (): boolean => rec.recording && suppression.depth === 0;\n\n const tsMs = (): number => rec.now() - bundle.startTime;\n\n const nextSeq = (): number => bundle.events.length;\n\n const append = (event: ReplayEvent): void => {\n bundle.events.push(event);\n rec.config.sink?.push(event);\n };\n\n const throttled = (): boolean => {\n const t = rec.now();\n if (t - windowStart >= 1000) {\n windowStart = t;\n windowCount = 0;\n }\n if (windowCount >= rec.config.maxEventsPerSecond) return true;\n windowCount += 1;\n return false;\n };\n\n // Build a keyframe or delta from the current raw state, applying the allowlist + masking,\n // updating the raw stash, and appending the snapshot event.\n const emitKeyframe = (storeKey: string, state: unknown, full: boolean): void => {\n // `state` is the Redux root state — a record of top-level slice references.\n const liveSlices = (state && typeof state === \"object\" && !Array.isArray(state))\n ? (state as Record<string, unknown>)\n : {} as Record<string, unknown>;\n\n const redacted = __redact(state, {\n includeStateKeys: rec.config.includeStateKeys,\n redactKeys: rec.config.redactKeys,\n }) as Record<string, unknown>;\n\n const prevStash = rawStash.get(storeKey);\n const prevLive = prevLiveSlices.get(storeKey);\n\n if (full || !prevStash || !prevLive) {\n // Full keyframe: clone every allowed slice.\n const cloned = cloneSlices(redacted);\n rawStash.set(storeKey, cloned);\n prevLiveSlices.set(storeKey, { ...liveSlices });\n append({\n type: \"state_snapshot\",\n kind: \"keyframe\",\n tsMs: tsMs(),\n origin: rec.origin,\n storeKey,\n seq: nextSeq(),\n slices: cloned,\n });\n return;\n }\n\n // [fix devil-H2] Delta: use LIVE slice references for change detection, not cloned values.\n // Redux returns the same reference for unchanged slices, a new reference for changed ones.\n // Only clone slices that actually changed to keep the fast path truly cheap.\n const changedStash: Record<string, unknown> = {};\n const nextStash: Record<string, unknown> = { ...prevStash };\n for (const key of Object.keys(redacted)) {\n if (!Object.is(prevLive[key], liveSlices[key])) {\n // This slice changed — clone and record it.\n const clonedVal = cloneSingle(redacted[key]);\n changedStash[key] = clonedVal;\n nextStash[key] = clonedVal;\n }\n // Unchanged slice: nextStash already holds the old clone from prevStash.\n }\n const removed = Object.keys(prevStash).filter((key) => !(key in redacted));\n for (const key of removed) delete nextStash[key];\n\n rawStash.set(storeKey, nextStash);\n prevLiveSlices.set(storeKey, { ...liveSlices });\n append({\n type: \"state_snapshot\",\n kind: \"delta\",\n tsMs: tsMs(),\n origin: rec.origin,\n storeKey,\n seq: nextSeq(),\n slices: changedStash,\n ...(removed.length ? { removedSlices: removed } : {}),\n });\n };\n\n // structuredClone a single slice value; returns __unserializable sentinel on failure (ADR-2 / R-2).\n const cloneSingle = (val: unknown): unknown => {\n try {\n return structuredClone(val);\n } catch {\n return { [UNSERIALIZABLE]: true };\n }\n };\n\n // Clone every slice in a state record (used for full keyframes).\n const cloneSlices = (state: Record<string, unknown>): Record<string, unknown> => {\n const out: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(state)) out[key] = cloneSingle(val);\n return out;\n };\n\n const rawStashMaxTs = (): number => {\n let max = 0;\n for (const ev of bundle.events) if (ev.tsMs > max) max = ev.tsMs;\n return max;\n };\n\n return rec;\n};\n\nconst isProdSafe = (): boolean => {\n try {\n return process.env.NODE_ENV === \"production\";\n } catch {\n return false;\n }\n};\n\n// TODO(BTE, auditor-MEDIUM): ComponentRenderEvent.props has no capture path yet. When component\n// capture is built, props MUST pass through __redact/redactKeys before storage — the same\n// allowlist + mask contract as state slices. Do not ship component capture without it.\n\n// TODO(BTE, R-1): globalThis key is not tamper-proof across multiple realms (iframes/workers).\n// A hostile frame could pre-populate __REPLAYKIT_RECORDER__ with a fake object. Out of scope for\n// v1 (single-realm assumption); document in the security posture before cross-origin iframe use.\n\n// Acquire the singleton: adopt an existing instance, else pin the first one (first-write-wins).\n// Module evaluation is single-threaded per realm so check-then-set can't interleave within a realm\n// (multi-realm is the accepted out-of-scope gap, R-1).\nexport const acquireRecorder = (): Recorder => {\n const g = globalThis as Record<string, unknown>;\n const existing = g[KEY] as Recorder | undefined;\n if (existing) return existing;\n const r = createRecorder();\n g[KEY] = r;\n return (g[KEY] as Recorder) ?? r;\n};\n\n// Test-only: read the current installed recorder without creating one.\nexport const __peekRecorder = (): Recorder | undefined =>\n (globalThis as Record<string, unknown>)[KEY] as Recorder | undefined;\n\n// Test-only: drop the singleton so each test starts from a clean realm.\nexport const __resetRecorder = (): void => {\n delete (globalThis as Record<string, unknown>)[KEY];\n};\n",
10
+ "// replayEnhancer — wraps the store to (a) handle the internal __REPLAYKIT_RESTORE action by\n// replacing named raw slices, and (b) capture actions + keyframes through the singleton recorder.\n//\n// Honest zero-cost (guardrail 2): if no recorder is installed (the singleton was never acquired by\n// configureReplay), the enhancer adds only one identity check per dispatch and builds NOTHING.\nimport type { Reducer, StoreEnhancer, StoreEnhancerStoreCreator, UnknownAction } from \"redux\";\nimport { __peekRecorder } from \"./recorder\";\n\nexport const RESTORE_TYPE = \"__REPLAYKIT_RESTORE\";\n\nexport interface RestoreAction {\n type: typeof RESTORE_TYPE;\n // raw slice values to splice into the named store's state.\n slices: Record<string, unknown>;\n removedSlices?: string[];\n [extra: string]: unknown;\n}\n\nconst isRestore = (action: unknown): action is RestoreAction =>\n typeof action === \"object\" &&\n action !== null &&\n (action as { type?: unknown }).type === RESTORE_TYPE;\n\n// Wrap a reducer so __REPLAYKIT_RESTORE replaces the named slices with their RAW values (ADR-3).\nconst wrapReducer =\n <S>(reducer: Reducer<S>): Reducer<S> =>\n (state, action) => {\n if (isRestore(action)) {\n const base = (state ?? {}) as Record<string, unknown>;\n const next = { ...base, ...action.slices };\n for (const key of action.removedSlices ?? []) delete next[key];\n return next as S;\n }\n return reducer(state, action as UnknownAction);\n };\n\nconst enhancer = (createStore: StoreEnhancerStoreCreator): StoreEnhancerStoreCreator =>\n ((reducer: Reducer, preloadedState?: unknown) => {\n const store = createStore(wrapReducer(reducer), preloadedState);\n\n const dispatch = ((action: UnknownAction) => {\n const recorder = __peekRecorder();\n // No recorder installed => pure pass-through, zero allocation.\n if (!recorder) return store.dispatch(action);\n\n // __REPLAYKIT_RESTORE flows through untouched; suppression is owned by the seek path.\n const result = store.dispatch(action);\n\n // The recorder's own guards make these no-ops unless armed; nothing is built past the guard.\n if (!isRestore(action) && typeof action.type === \"string\") {\n const storeKey = recorder.origin;\n recorder.captureAction(storeKey, {\n type: action.type,\n payload: (action as { payload?: unknown }).payload,\n });\n recorder.captureKeyframe(storeKey);\n }\n return result;\n }) as typeof store.dispatch;\n\n return { ...store, dispatch };\n }) as unknown as StoreEnhancerStoreCreator;\n\nexport const replayEnhancer: StoreEnhancer = enhancer as StoreEnhancer;\n",
11
+ "// Re-drive + the live/history signal (ADR-3, NOQ-2, NOQ-4, NOQ-6).\n//\n// THE single source of truth for replay mode is the module-level `mode` below. It is flipped by\n// ONE synchronous transition (`setMode`) that ALSO bumps recorder suppression in the SAME tick, so\n// the two flags can never skew (NOQ-2). Side-channel handlers (WS onmessage) MUST read\n// getReplayMode()/subscribeReplayMode — never the React hook, which lags the event loop (TQ1-A).\nimport type { RawSnapshot } from \"./bundle\";\nimport type { ReplayPlayer } from \"./player\";\nimport { acquireRecorder } from \"./recorder\";\nimport { RESTORE_TYPE, type RestoreAction } from \"./enhancer\";\nimport { warnOnce } from \"./env\";\n\nexport type ReplayMode = \"live\" | \"history\";\n\nlet mode: ReplayMode = \"live\";\nconst listeners = new Set<(m: ReplayMode) => void>();\n\nexport const getReplayMode = (): ReplayMode => mode; // synchronous read — use in side-channels\n\n// Deliver the CURRENT mode synchronously on subscribe (NOQ-4 initial-value semantics) so a remote\n// mounting mid-history gates correctly before its first dispatch. Returns an unsubscribe.\nexport const subscribeReplayMode = (cb: (m: ReplayMode) => void): (() => void) => {\n listeners.add(cb);\n cb(mode);\n return () => {\n listeners.delete(cb);\n };\n};\n\nconst notify = (): void => {\n for (const cb of listeners) cb(mode);\n};\n\n// The ONE atomic transition: flip mode AND bump suppression together, in one synchronous tick.\n// history => suppression on (depth+1); back to live => suppression off (depth-1). Never skews.\nconst setMode = (next: ReplayMode): void => {\n if (next === mode) return;\n const recorder = acquireRecorder();\n if (next === \"history\") {\n recorder.suppress();\n mode = \"history\";\n } else {\n mode = \"live\";\n recorder.unsuppress();\n }\n notify();\n};\n\n// Apply a RAW snapshot to a store (ADR-3). RAW value only — branded RawSnapshot makes feeding\n// display data here a compile error.\n//\n// [fix devil-C2] Self-suppresses around the restore dispatch (reentrancy-counted, so nesting\n// under seek()'s own suppress is safe). Without this a public bare call while recording would let\n// a synchronous store-subscriber reaction get captured — a feedback echo identical to the one\n// seek() guards against.\nexport const applyReplayState = (snapshot: RawSnapshot, storeKey?: string): void => {\n const recorder = acquireRecorder();\n const key = storeKey ?? recorder.origin;\n const store = recorder.getStore(key);\n if (!store) {\n warnOnce(`applyReplayState: no store registered for \"${key}\"`);\n return;\n }\n recorder.suppress();\n try {\n const action: RestoreAction = { type: RESTORE_TYPE, slices: { ...snapshot.slices } };\n store.dispatch(action);\n } finally {\n recorder.unsuppress();\n }\n};\n\n// Seek to tMs: the synchronous suppressed batch (NOQ-6). Fold ALL stores first (pure), then apply\n// every restore inside ONE synchronous suppress/unsuppress so the reentrancy counter never leaks\n// across an await. Cross-store atomicity (Q2) falls out for free.\nexport const seek = (player: ReplayPlayer, tMs: number): void => {\n const states = player.statesAt(tMs); // pure fold, no dispatch\n if (mode !== \"history\") setMode(\"history\"); // atomic: mode + suppression together\n\n const recorder = acquireRecorder();\n recorder.suppress(); // extra guard around the batch itself (reentrancy-counted)\n try {\n for (const [storeKey, snap] of Object.entries(states)) {\n if (snap) applyReplayState(snap, storeKey);\n }\n } finally {\n recorder.unsuppress();\n }\n};\n\n// Return to live: flip back to live + drop the suppression bump, atomically.\nexport const returnToLive = (): void => {\n setMode(\"live\");\n // [fix devil-H3a] The leak guard belongs HERE, after the mode flip, not inside seek() where\n // mode is always \"history\" making the check dead. After returning to live, depth must be 0\n // (the setMode(\"live\") call dropped the mode-suppression; any seek()-internal suppress was\n // already released in seek's finally block). A non-zero depth means a caller leaked.\n const recorder = acquireRecorder();\n if (recorder.suppression.depth !== 0) {\n warnOnce(\"suppression depth non-zero after returnToLive — a suppress/unsuppress pair leaked\");\n }\n};\n\n// Test-only: reset the module-level mode + listeners between tests.\nexport const __resetReplayMode = (): void => {\n mode = \"live\";\n listeners.clear();\n};\n"
12
+ ],
13
+ "mappings": "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",
14
+ "debugId": "EA02C4FDCADFE80364756E2164756E21",
15
+ "names": []
16
+ }
@@ -0,0 +1,52 @@
1
+ import type { Store } from "redux";
2
+ import { type FeedMessageEvent, type ReplayEvent, type SessionBundle } from "./bundle";
3
+ export interface ReplaySink {
4
+ push(event: ReplayEvent): void;
5
+ flush?(): Promise<void>;
6
+ }
7
+ export interface RecorderConfig {
8
+ sink?: ReplaySink;
9
+ redactKeys?: string[];
10
+ includeStateKeys?: string[];
11
+ includeFeedTopics?: string[];
12
+ keyframeEvery: number;
13
+ fullKeyframeRatio: number;
14
+ sample: number;
15
+ maxEventsPerSecond: number;
16
+ }
17
+ export interface Suppression {
18
+ depth: number;
19
+ }
20
+ export interface Recorder {
21
+ readonly suppression: Suppression;
22
+ config: RecorderConfig;
23
+ recording: boolean;
24
+ origin: string;
25
+ now: () => number;
26
+ start(): void;
27
+ stop(): void;
28
+ isRecording(): boolean;
29
+ configure(opts: Partial<RecorderConfig> & {
30
+ origin?: string;
31
+ now?: () => number;
32
+ }): void;
33
+ registerStore(name: string, store: Store): void;
34
+ unregisterStore(name: string): void;
35
+ getStore(name: string): Store | undefined;
36
+ captureAction(storeKey: string, action: {
37
+ type: string;
38
+ payload?: unknown;
39
+ }): void;
40
+ captureKeyframe(storeKey: string): void;
41
+ captureRoute(path: string, search?: string): void;
42
+ pushFeedEvent(ev: Omit<FeedMessageEvent, "tsMs" | "seq" | "type" | "origin" | "storeKey"> & {
43
+ storeKey?: string;
44
+ }): void;
45
+ suppress(): void;
46
+ unsuppress(): void;
47
+ getBundle(): SessionBundle;
48
+ }
49
+ export declare const acquireRecorder: () => Recorder;
50
+ export declare const __peekRecorder: () => Recorder | undefined;
51
+ export declare const __resetRecorder: () => void;
52
+ //# sourceMappingURL=recorder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../src/recorder.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AACnC,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,aAAa,EACnB,MAAM,UAAU,CAAC;AAIlB,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;IAC/B,KAAK,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AASD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;CACf;AAID,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,MAAM,EAAE,cAAc,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;IAEnB,MAAM,EAAE,MAAM,CAAC;IAEf,GAAG,EAAE,MAAM,MAAM,CAAC;IAElB,KAAK,IAAI,IAAI,CAAC;IACd,IAAI,IAAI,IAAI,CAAC;IACb,WAAW,IAAI,OAAO,CAAC;IAEvB,SAAS,CAAC,IAAI,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAEzF,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAChD,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS,CAAC;IAG1C,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IACnF,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxC,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClD,aAAa,CACX,EAAE,EAAE,IAAI,CAAC,gBAAgB,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,GAAG,UAAU,CAAC,GAAG;QAC5E,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,GACA,IAAI,CAAC;IAGR,QAAQ,IAAI,IAAI,CAAC;IACjB,UAAU,IAAI,IAAI,CAAC;IAEnB,SAAS,IAAI,aAAa,CAAC;CAC5B;AAkTD,eAAO,MAAM,eAAe,QAAO,QAOlC,CAAC;AAGF,eAAO,MAAM,cAAc,QAAO,QAAQ,GAAG,SACyB,CAAC;AAGvE,eAAO,MAAM,eAAe,QAAO,IAElC,CAAC"}