state-surgeon 1.0.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/dist/index.js ADDED
@@ -0,0 +1,1828 @@
1
+ 'use strict';
2
+
3
+ var uuid = require('uuid');
4
+ var express = require('express');
5
+ var ws = require('ws');
6
+
7
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
+
9
+ var express__default = /*#__PURE__*/_interopDefault(express);
10
+
11
+ var __defProp = Object.defineProperty;
12
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
13
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
14
+ }) : x)(function(x) {
15
+ if (typeof require !== "undefined") return require.apply(this, arguments);
16
+ throw Error('Dynamic require of "' + x + '" is not supported');
17
+ });
18
+ var __export = (target, all) => {
19
+ for (var name in all)
20
+ __defProp(target, name, { get: all[name], enumerable: true });
21
+ };
22
+
23
+ // src/core/diff.ts
24
+ function deepClone(obj) {
25
+ if (obj === null || typeof obj !== "object") {
26
+ return obj;
27
+ }
28
+ if (Array.isArray(obj)) {
29
+ return obj.map((item) => deepClone(item));
30
+ }
31
+ if (obj instanceof Date) {
32
+ return new Date(obj.getTime());
33
+ }
34
+ if (obj instanceof Map) {
35
+ const clonedMap = /* @__PURE__ */ new Map();
36
+ obj.forEach((value, key) => {
37
+ clonedMap.set(deepClone(key), deepClone(value));
38
+ });
39
+ return clonedMap;
40
+ }
41
+ if (obj instanceof Set) {
42
+ const clonedSet = /* @__PURE__ */ new Set();
43
+ obj.forEach((value) => {
44
+ clonedSet.add(deepClone(value));
45
+ });
46
+ return clonedSet;
47
+ }
48
+ const cloned = {};
49
+ for (const key in obj) {
50
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
51
+ cloned[key] = deepClone(obj[key]);
52
+ }
53
+ }
54
+ return cloned;
55
+ }
56
+ function calculateDiff(before, after, path = "") {
57
+ const diffs = [];
58
+ if (before === after) {
59
+ return diffs;
60
+ }
61
+ if (before === null || before === void 0 || typeof before !== "object") {
62
+ if (after === null || after === void 0 || typeof after !== "object") {
63
+ if (before !== after) {
64
+ if (before === void 0) {
65
+ diffs.push({ path: path || "root", operation: "ADD", newValue: after });
66
+ } else if (after === void 0) {
67
+ diffs.push({ path: path || "root", operation: "REMOVE", oldValue: before });
68
+ } else {
69
+ diffs.push({ path: path || "root", operation: "UPDATE", oldValue: before, newValue: after });
70
+ }
71
+ }
72
+ return diffs;
73
+ }
74
+ diffs.push({ path: path || "root", operation: "UPDATE", oldValue: before, newValue: after });
75
+ return diffs;
76
+ }
77
+ if (after === null || after === void 0 || typeof after !== "object") {
78
+ diffs.push({ path: path || "root", operation: "UPDATE", oldValue: before, newValue: after });
79
+ return diffs;
80
+ }
81
+ if (Array.isArray(before) || Array.isArray(after)) {
82
+ if (!Array.isArray(before) || !Array.isArray(after)) {
83
+ diffs.push({ path: path || "root", operation: "UPDATE", oldValue: before, newValue: after });
84
+ return diffs;
85
+ }
86
+ const maxLength = Math.max(before.length, after.length);
87
+ for (let i = 0; i < maxLength; i++) {
88
+ const itemPath = path ? `${path}[${i}]` : `[${i}]`;
89
+ if (i >= before.length) {
90
+ diffs.push({ path: itemPath, operation: "ADD", newValue: after[i] });
91
+ } else if (i >= after.length) {
92
+ diffs.push({ path: itemPath, operation: "REMOVE", oldValue: before[i] });
93
+ } else {
94
+ diffs.push(...calculateDiff(before[i], after[i], itemPath));
95
+ }
96
+ }
97
+ return diffs;
98
+ }
99
+ const beforeObj = before;
100
+ const afterObj = after;
101
+ const allKeys = /* @__PURE__ */ new Set([...Object.keys(beforeObj), ...Object.keys(afterObj)]);
102
+ for (const key of allKeys) {
103
+ const keyPath = path ? `${path}.${key}` : key;
104
+ const beforeValue = beforeObj[key];
105
+ const afterValue = afterObj[key];
106
+ if (!(key in beforeObj)) {
107
+ diffs.push({ path: keyPath, operation: "ADD", newValue: afterValue });
108
+ } else if (!(key in afterObj)) {
109
+ diffs.push({ path: keyPath, operation: "REMOVE", oldValue: beforeValue });
110
+ } else {
111
+ diffs.push(...calculateDiff(beforeValue, afterValue, keyPath));
112
+ }
113
+ }
114
+ return diffs;
115
+ }
116
+ function applyDiff(state, diffs) {
117
+ const result = deepClone(state);
118
+ for (const diff of diffs) {
119
+ setValueAtPath(result, diff.path, diff.operation === "REMOVE" ? void 0 : diff.newValue);
120
+ }
121
+ return result;
122
+ }
123
+ function getValueAtPath(obj, path) {
124
+ if (!path || path === "root") return obj;
125
+ const parts = parsePath(path);
126
+ let current = obj;
127
+ for (const part of parts) {
128
+ if (current === null || current === void 0) {
129
+ return void 0;
130
+ }
131
+ current = current[part];
132
+ }
133
+ return current;
134
+ }
135
+ function setValueAtPath(obj, path, value) {
136
+ if (!path || path === "root") return;
137
+ const parts = parsePath(path);
138
+ let current = obj;
139
+ for (let i = 0; i < parts.length - 1; i++) {
140
+ const part = parts[i];
141
+ if (current[part] === void 0) {
142
+ const nextPart = parts[i + 1];
143
+ current[part] = /^\d+$/.test(nextPart) ? [] : {};
144
+ }
145
+ current = current[part];
146
+ }
147
+ const lastPart = parts[parts.length - 1];
148
+ if (value === void 0) {
149
+ delete current[lastPart];
150
+ } else {
151
+ current[lastPart] = value;
152
+ }
153
+ }
154
+ function parsePath(path) {
155
+ const parts = [];
156
+ let current = "";
157
+ let inBracket = false;
158
+ for (const char of path) {
159
+ if (char === "." && !inBracket) {
160
+ if (current) parts.push(current);
161
+ current = "";
162
+ } else if (char === "[") {
163
+ if (current) parts.push(current);
164
+ current = "";
165
+ inBracket = true;
166
+ } else if (char === "]") {
167
+ if (current) parts.push(current);
168
+ current = "";
169
+ inBracket = false;
170
+ } else {
171
+ current += char;
172
+ }
173
+ }
174
+ if (current) parts.push(current);
175
+ return parts;
176
+ }
177
+ function deepEqual(a, b) {
178
+ if (a === b) return true;
179
+ if (typeof a !== typeof b) return false;
180
+ if (a === null || b === null) return a === b;
181
+ if (typeof a !== "object") return a === b;
182
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
183
+ if (Array.isArray(a) && Array.isArray(b)) {
184
+ if (a.length !== b.length) return false;
185
+ return a.every((item, index) => deepEqual(item, b[index]));
186
+ }
187
+ const aObj = a;
188
+ const bObj = b;
189
+ const aKeys = Object.keys(aObj);
190
+ const bKeys = Object.keys(bObj);
191
+ if (aKeys.length !== bKeys.length) return false;
192
+ return aKeys.every((key) => deepEqual(aObj[key], bObj[key]));
193
+ }
194
+ var logicalClock = 0;
195
+ function createMutation(options) {
196
+ const {
197
+ source,
198
+ sessionId,
199
+ previousState,
200
+ nextState,
201
+ actionType = "CUSTOM",
202
+ actionPayload,
203
+ component,
204
+ function: funcName,
205
+ captureStack = true,
206
+ metadata
207
+ } = options;
208
+ logicalClock++;
209
+ const mutation = {
210
+ id: `mut_${Date.now()}_${uuid.v4().slice(0, 8)}`,
211
+ timestamp: typeof performance !== "undefined" ? performance.now() : Date.now(),
212
+ logicalClock,
213
+ sessionId,
214
+ source,
215
+ component,
216
+ function: funcName,
217
+ actionType,
218
+ actionPayload,
219
+ previousState,
220
+ nextState,
221
+ metadata
222
+ };
223
+ if (captureStack) {
224
+ mutation.callStack = parseCallStack(new Error().stack);
225
+ }
226
+ return mutation;
227
+ }
228
+ function parseCallStack(stack) {
229
+ if (!stack) return [];
230
+ return stack.split("\n").slice(2).map((line) => line.trim()).filter((line) => line.startsWith("at ")).map((line) => {
231
+ const match = line.match(/at\s+(.+?)\s+\((.+):(\d+):(\d+)\)/);
232
+ if (match) {
233
+ return `${match[1]} (${match[2].split("/").pop()}:${match[3]})`;
234
+ }
235
+ const simpleMatch = line.match(/at\s+(.+):(\d+):(\d+)/);
236
+ if (simpleMatch) {
237
+ return `anonymous (${simpleMatch[1].split("/").pop()}:${simpleMatch[2]})`;
238
+ }
239
+ return line.replace("at ", "");
240
+ }).slice(0, 10);
241
+ }
242
+ function getLogicalClock() {
243
+ return logicalClock;
244
+ }
245
+ function resetLogicalClock() {
246
+ logicalClock = 0;
247
+ }
248
+ function generateSessionId() {
249
+ return `session_${Date.now()}_${uuid.v4().slice(0, 8)}`;
250
+ }
251
+
252
+ // src/core/utils.ts
253
+ function deepMerge(target, source) {
254
+ const result = { ...target };
255
+ for (const key in source) {
256
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
257
+ const sourceValue = source[key];
258
+ const targetValue = target[key];
259
+ if (sourceValue !== null && typeof sourceValue === "object" && !Array.isArray(sourceValue) && targetValue !== null && typeof targetValue === "object" && !Array.isArray(targetValue)) {
260
+ result[key] = deepMerge(
261
+ targetValue,
262
+ sourceValue
263
+ );
264
+ } else {
265
+ result[key] = sourceValue;
266
+ }
267
+ }
268
+ }
269
+ return result;
270
+ }
271
+ function debounce(fn, ms) {
272
+ let timeoutId;
273
+ return function(...args) {
274
+ if (timeoutId) {
275
+ clearTimeout(timeoutId);
276
+ }
277
+ timeoutId = setTimeout(() => {
278
+ fn.apply(this, args);
279
+ }, ms);
280
+ };
281
+ }
282
+ function throttle(fn, ms) {
283
+ let lastCall = 0;
284
+ let timeoutId;
285
+ return function(...args) {
286
+ const now = Date.now();
287
+ const remaining = ms - (now - lastCall);
288
+ if (remaining <= 0) {
289
+ if (timeoutId) {
290
+ clearTimeout(timeoutId);
291
+ timeoutId = void 0;
292
+ }
293
+ lastCall = now;
294
+ fn.apply(this, args);
295
+ } else if (!timeoutId) {
296
+ timeoutId = setTimeout(() => {
297
+ lastCall = Date.now();
298
+ timeoutId = void 0;
299
+ fn.apply(this, args);
300
+ }, remaining);
301
+ }
302
+ };
303
+ }
304
+ function compress(data) {
305
+ return data;
306
+ }
307
+ function decompress(data) {
308
+ return data;
309
+ }
310
+ function formatBytes(bytes) {
311
+ if (bytes === 0) return "0 B";
312
+ const k = 1024;
313
+ const sizes = ["B", "KB", "MB", "GB"];
314
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
315
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
316
+ }
317
+ function formatDuration(ms) {
318
+ if (ms < 1) return `${(ms * 1e3).toFixed(2)}\u03BCs`;
319
+ if (ms < 1e3) return `${ms.toFixed(2)}ms`;
320
+ return `${(ms / 1e3).toFixed(2)}s`;
321
+ }
322
+ function simpleHash(str) {
323
+ let hash = 0;
324
+ for (let i = 0; i < str.length; i++) {
325
+ const char = str.charCodeAt(i);
326
+ hash = (hash << 5) - hash + char;
327
+ hash = hash & hash;
328
+ }
329
+ return Math.abs(hash).toString(16);
330
+ }
331
+ function isBrowser() {
332
+ return typeof window !== "undefined" && typeof document !== "undefined";
333
+ }
334
+ function isNode() {
335
+ return typeof process !== "undefined" && process.versions?.node !== void 0;
336
+ }
337
+ function safeStringify(obj, space) {
338
+ const seen = /* @__PURE__ */ new WeakSet();
339
+ return JSON.stringify(obj, (_, value) => {
340
+ if (typeof value === "object" && value !== null) {
341
+ if (seen.has(value)) {
342
+ return "[Circular]";
343
+ }
344
+ seen.add(value);
345
+ }
346
+ if (typeof value === "function") {
347
+ return `[Function: ${value.name || "anonymous"}]`;
348
+ }
349
+ if (typeof value === "symbol") {
350
+ return value.toString();
351
+ }
352
+ if (value instanceof Error) {
353
+ return {
354
+ name: value.name,
355
+ message: value.message,
356
+ stack: value.stack
357
+ };
358
+ }
359
+ if (value instanceof Map) {
360
+ return { __type: "Map", entries: Array.from(value.entries()) };
361
+ }
362
+ if (value instanceof Set) {
363
+ return { __type: "Set", values: Array.from(value.values()) };
364
+ }
365
+ return value;
366
+ }, space);
367
+ }
368
+ function safeParse(json) {
369
+ return JSON.parse(json, (_, value) => {
370
+ if (value && typeof value === "object") {
371
+ if (value.__type === "Map") {
372
+ return new Map(value.entries);
373
+ }
374
+ if (value.__type === "Set") {
375
+ return new Set(value.values);
376
+ }
377
+ }
378
+ return value;
379
+ });
380
+ }
381
+
382
+ // src/instrument/index.ts
383
+ var instrument_exports = {};
384
+ __export(instrument_exports, {
385
+ MutationTransport: () => MutationTransport,
386
+ StateSurgeonClient: () => StateSurgeonClient,
387
+ createReduxMiddleware: () => createReduxMiddleware,
388
+ instrumentFetch: () => instrumentFetch,
389
+ instrumentReact: () => instrumentReact,
390
+ instrumentReduxStore: () => instrumentReduxStore,
391
+ instrumentXHR: () => instrumentXHR,
392
+ instrumentZustand: () => instrumentZustand
393
+ });
394
+
395
+ // src/instrument/transport.ts
396
+ var MutationTransport = class {
397
+ constructor(url, options = {}) {
398
+ this.ws = null;
399
+ this.buffer = [];
400
+ this.flushTimer = null;
401
+ this.reconnectAttempts = 0;
402
+ this.isConnecting = false;
403
+ this.shouldReconnect = true;
404
+ this.url = url;
405
+ this.options = {
406
+ batchSize: options.batchSize ?? 50,
407
+ flushInterval: options.flushInterval ?? 100,
408
+ reconnectDelay: options.reconnectDelay ?? 1e3,
409
+ maxReconnectAttempts: options.maxReconnectAttempts ?? 10,
410
+ onConnect: options.onConnect ?? (() => {
411
+ }),
412
+ onDisconnect: options.onDisconnect ?? (() => {
413
+ }),
414
+ onError: options.onError ?? (() => {
415
+ }),
416
+ onMessage: options.onMessage ?? (() => {
417
+ })
418
+ };
419
+ }
420
+ /**
421
+ * Establishes WebSocket connection
422
+ */
423
+ connect() {
424
+ if (this.ws || this.isConnecting) {
425
+ return;
426
+ }
427
+ this.isConnecting = true;
428
+ this.shouldReconnect = true;
429
+ try {
430
+ const WebSocketImpl = typeof WebSocket !== "undefined" ? WebSocket : __require("ws");
431
+ this.ws = new WebSocketImpl(this.url);
432
+ this.ws.onopen = () => {
433
+ this.isConnecting = false;
434
+ this.reconnectAttempts = 0;
435
+ this.startFlushTimer();
436
+ this.options.onConnect();
437
+ };
438
+ this.ws.onclose = () => {
439
+ this.isConnecting = false;
440
+ this.stopFlushTimer();
441
+ this.ws = null;
442
+ this.options.onDisconnect();
443
+ this.attemptReconnect();
444
+ };
445
+ this.ws.onerror = (event) => {
446
+ this.isConnecting = false;
447
+ const error = new Error("WebSocket error");
448
+ this.options.onError(error);
449
+ };
450
+ this.ws.onmessage = (event) => {
451
+ try {
452
+ const message = JSON.parse(event.data);
453
+ this.options.onMessage(message);
454
+ } catch (e) {
455
+ }
456
+ };
457
+ } catch (error) {
458
+ this.isConnecting = false;
459
+ this.options.onError(error instanceof Error ? error : new Error(String(error)));
460
+ this.attemptReconnect();
461
+ }
462
+ }
463
+ /**
464
+ * Closes the WebSocket connection
465
+ */
466
+ disconnect() {
467
+ this.shouldReconnect = false;
468
+ this.stopFlushTimer();
469
+ if (this.ws) {
470
+ this.ws.close();
471
+ this.ws = null;
472
+ }
473
+ }
474
+ /**
475
+ * Sends a message (adds to buffer)
476
+ */
477
+ send(message) {
478
+ this.buffer.push(message);
479
+ if (this.buffer.length >= this.options.batchSize) {
480
+ this.flush();
481
+ }
482
+ }
483
+ /**
484
+ * Flushes the buffer immediately
485
+ */
486
+ flush() {
487
+ if (this.buffer.length === 0) {
488
+ return;
489
+ }
490
+ if (this.ws && this.ws.readyState === 1) {
491
+ try {
492
+ const batch = {
493
+ type: "MUTATION_BATCH",
494
+ payload: {
495
+ mutations: this.buffer.map((m) => m.payload),
496
+ timestamp: Date.now()
497
+ }
498
+ };
499
+ this.ws.send(JSON.stringify(batch));
500
+ this.buffer = [];
501
+ } catch (error) {
502
+ this.options.onError(
503
+ error instanceof Error ? error : new Error(String(error))
504
+ );
505
+ }
506
+ }
507
+ }
508
+ /**
509
+ * Checks if transport is connected
510
+ */
511
+ isConnected() {
512
+ return this.ws !== null && this.ws.readyState === 1;
513
+ }
514
+ /**
515
+ * Gets the current buffer size
516
+ */
517
+ getBufferSize() {
518
+ return this.buffer.length;
519
+ }
520
+ startFlushTimer() {
521
+ if (this.flushTimer) {
522
+ clearInterval(this.flushTimer);
523
+ }
524
+ this.flushTimer = setInterval(() => {
525
+ this.flush();
526
+ }, this.options.flushInterval);
527
+ }
528
+ stopFlushTimer() {
529
+ if (this.flushTimer) {
530
+ clearInterval(this.flushTimer);
531
+ this.flushTimer = null;
532
+ }
533
+ }
534
+ attemptReconnect() {
535
+ if (!this.shouldReconnect) {
536
+ return;
537
+ }
538
+ if (this.reconnectAttempts >= this.options.maxReconnectAttempts) {
539
+ this.options.onError(new Error("Max reconnection attempts reached"));
540
+ return;
541
+ }
542
+ this.reconnectAttempts++;
543
+ const delay = this.options.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1);
544
+ setTimeout(() => {
545
+ if (this.shouldReconnect) {
546
+ this.connect();
547
+ }
548
+ }, Math.min(delay, 3e4));
549
+ }
550
+ };
551
+
552
+ // src/instrument/client.ts
553
+ var StateSurgeonClient = class {
554
+ constructor(options = {}) {
555
+ this.transport = null;
556
+ this.listeners = /* @__PURE__ */ new Set();
557
+ this.isConnected = false;
558
+ // Track mutations locally for offline support
559
+ this.localMutations = [];
560
+ this.maxLocalMutations = 1e4;
561
+ this.sessionId = options.sessionId || generateSessionId();
562
+ this.appId = options.appId || "default";
563
+ this.debug = options.debug || false;
564
+ this.serverUrl = options.serverUrl || "ws://localhost:8081";
565
+ this.transportOptions = {
566
+ batchSize: options.batchSize || 50,
567
+ flushInterval: options.flushInterval || 100,
568
+ ...options.transport
569
+ };
570
+ if (options.autoConnect !== false) {
571
+ this.connect();
572
+ }
573
+ this.log("State Surgeon Client initialized", { sessionId: this.sessionId, appId: this.appId });
574
+ }
575
+ /**
576
+ * Connects to the recorder server
577
+ */
578
+ connect() {
579
+ if (this.transport) {
580
+ return;
581
+ }
582
+ this.transport = new MutationTransport(this.serverUrl, {
583
+ ...this.transportOptions,
584
+ onConnect: () => {
585
+ this.isConnected = true;
586
+ this.log("Connected to State Surgeon recorder");
587
+ this.sendHandshake();
588
+ },
589
+ onDisconnect: () => {
590
+ this.isConnected = false;
591
+ this.log("Disconnected from State Surgeon recorder");
592
+ },
593
+ onError: (error) => {
594
+ this.log("Transport error:", error);
595
+ }
596
+ });
597
+ this.transport.connect();
598
+ }
599
+ /**
600
+ * Disconnects from the recorder server
601
+ */
602
+ disconnect() {
603
+ if (this.transport) {
604
+ this.transport.disconnect();
605
+ this.transport = null;
606
+ }
607
+ this.isConnected = false;
608
+ }
609
+ /**
610
+ * Sends the initial handshake to register the session
611
+ */
612
+ sendHandshake() {
613
+ if (this.transport) {
614
+ this.transport.send({
615
+ type: "REGISTER_SESSION",
616
+ payload: {
617
+ appId: this.appId,
618
+ sessionId: this.sessionId,
619
+ timestamp: Date.now(),
620
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : "node",
621
+ url: typeof window !== "undefined" ? window.location.href : void 0
622
+ }
623
+ });
624
+ }
625
+ }
626
+ /**
627
+ * Records a mutation
628
+ */
629
+ recordMutation(mutation) {
630
+ this.localMutations.push(mutation);
631
+ if (this.localMutations.length > this.maxLocalMutations) {
632
+ this.localMutations.shift();
633
+ }
634
+ for (const listener of this.listeners) {
635
+ try {
636
+ listener(mutation);
637
+ } catch (error) {
638
+ this.log("Listener error:", error);
639
+ }
640
+ }
641
+ if (this.transport && this.isConnected) {
642
+ this.transport.send({
643
+ type: "MUTATION_RECORDED",
644
+ payload: mutation
645
+ });
646
+ }
647
+ this.log("Mutation recorded:", mutation.id, mutation.source);
648
+ }
649
+ /**
650
+ * Creates and records a mutation from state change
651
+ */
652
+ captureStateChange(source, previousState, nextState, options = {}) {
653
+ const mutation = createMutation({
654
+ source,
655
+ sessionId: this.sessionId,
656
+ previousState: deepClone(previousState),
657
+ nextState: deepClone(nextState),
658
+ actionType: options.actionType || "CUSTOM",
659
+ actionPayload: options.actionPayload,
660
+ component: options.component,
661
+ function: options.function,
662
+ captureStack: true
663
+ });
664
+ mutation.diff = calculateDiff(mutation.previousState, mutation.nextState);
665
+ this.recordMutation(mutation);
666
+ return mutation;
667
+ }
668
+ /**
669
+ * Adds a mutation listener
670
+ */
671
+ addListener(listener) {
672
+ this.listeners.add(listener);
673
+ return () => this.listeners.delete(listener);
674
+ }
675
+ /**
676
+ * Gets the current session ID
677
+ */
678
+ getSessionId() {
679
+ return this.sessionId;
680
+ }
681
+ /**
682
+ * Gets all local mutations
683
+ */
684
+ getMutations() {
685
+ return [...this.localMutations];
686
+ }
687
+ /**
688
+ * Clears local mutations
689
+ */
690
+ clearMutations() {
691
+ this.localMutations = [];
692
+ }
693
+ /**
694
+ * Checks if connected to server
695
+ */
696
+ isServerConnected() {
697
+ return this.isConnected;
698
+ }
699
+ /**
700
+ * Flushes any pending mutations
701
+ */
702
+ flush() {
703
+ if (this.transport) {
704
+ this.transport.flush();
705
+ }
706
+ }
707
+ log(...args) {
708
+ if (this.debug) {
709
+ console.log("[State Surgeon]", ...args);
710
+ }
711
+ }
712
+ };
713
+ var globalClient = null;
714
+ function getClient(options) {
715
+ if (!globalClient) {
716
+ globalClient = new StateSurgeonClient(options);
717
+ }
718
+ return globalClient;
719
+ }
720
+
721
+ // src/instrument/api.ts
722
+ var originalFetch = null;
723
+ var fetchInstrumented = false;
724
+ var originalXHROpen = null;
725
+ var originalXHRSend = null;
726
+ var xhrInstrumented = false;
727
+ function instrumentFetch(options = {}) {
728
+ if (fetchInstrumented) {
729
+ console.warn("[State Surgeon] Fetch already instrumented");
730
+ return () => {
731
+ };
732
+ }
733
+ if (typeof fetch === "undefined") {
734
+ console.warn("[State Surgeon] Fetch not available in this environment");
735
+ return () => {
736
+ };
737
+ }
738
+ const client = options.client || getClient();
739
+ const ignoreUrls = options.ignoreUrls || [];
740
+ const captureRequestBody = options.captureRequestBody !== false;
741
+ const captureResponseBody = options.captureResponseBody !== false;
742
+ const maxBodySize = options.maxBodySize || 1e4;
743
+ originalFetch = fetch;
744
+ globalThis.fetch = async function instrumentedFetch(input, init) {
745
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
746
+ if (ignoreUrls.some((ignore) => url.includes(ignore))) {
747
+ return originalFetch(input, init);
748
+ }
749
+ const startTime = Date.now();
750
+ const requestId = `req_${startTime}_${Math.random().toString(36).slice(2, 8)}`;
751
+ const requestInfo = {
752
+ url,
753
+ method: init?.method || "GET",
754
+ headers: init?.headers
755
+ };
756
+ if (captureRequestBody && init?.body) {
757
+ requestInfo.body = truncateBody(init.body, maxBodySize);
758
+ }
759
+ try {
760
+ const response = await originalFetch(input, init);
761
+ const duration = Date.now() - startTime;
762
+ const clonedResponse = response.clone();
763
+ const responseInfo = {
764
+ status: response.status,
765
+ statusText: response.statusText,
766
+ headers: Object.fromEntries(response.headers.entries())
767
+ };
768
+ if (captureResponseBody) {
769
+ try {
770
+ const text = await clonedResponse.text();
771
+ responseInfo.body = truncateBody(text, maxBodySize);
772
+ } catch {
773
+ responseInfo.body = "[Unable to read response body]";
774
+ }
775
+ }
776
+ client.captureStateChange("api", requestInfo, responseInfo, {
777
+ actionType: "API_RESPONSE",
778
+ actionPayload: {
779
+ requestId,
780
+ url,
781
+ method: requestInfo.method,
782
+ status: response.status,
783
+ duration
784
+ },
785
+ function: "fetch"
786
+ });
787
+ return response;
788
+ } catch (error) {
789
+ const duration = Date.now() - startTime;
790
+ client.captureStateChange("api", requestInfo, {
791
+ error: error instanceof Error ? error.message : String(error),
792
+ stack: error instanceof Error ? error.stack : void 0
793
+ }, {
794
+ actionType: "API_RESPONSE",
795
+ actionPayload: {
796
+ requestId,
797
+ url,
798
+ method: requestInfo.method,
799
+ status: 0,
800
+ error: true,
801
+ duration
802
+ },
803
+ function: "fetch"
804
+ });
805
+ throw error;
806
+ }
807
+ };
808
+ fetchInstrumented = true;
809
+ return () => {
810
+ if (originalFetch) {
811
+ globalThis.fetch = originalFetch;
812
+ originalFetch = null;
813
+ }
814
+ fetchInstrumented = false;
815
+ };
816
+ }
817
+ function instrumentXHR(options = {}) {
818
+ if (xhrInstrumented) {
819
+ console.warn("[State Surgeon] XHR already instrumented");
820
+ return () => {
821
+ };
822
+ }
823
+ if (typeof XMLHttpRequest === "undefined") {
824
+ console.warn("[State Surgeon] XMLHttpRequest not available in this environment");
825
+ return () => {
826
+ };
827
+ }
828
+ const client = options.client || getClient();
829
+ const ignoreUrls = options.ignoreUrls || [];
830
+ const captureResponseBody = options.captureResponseBody !== false;
831
+ const maxBodySize = options.maxBodySize || 1e4;
832
+ originalXHROpen = XMLHttpRequest.prototype.open;
833
+ originalXHRSend = XMLHttpRequest.prototype.send;
834
+ XMLHttpRequest.prototype.open = function(method, url, async = true, username, password) {
835
+ this._stateSurgeon = {
836
+ method,
837
+ url: url.toString(),
838
+ startTime: 0
839
+ };
840
+ return originalXHROpen.call(this, method, url, async, username, password);
841
+ };
842
+ XMLHttpRequest.prototype.send = function(body) {
843
+ const info = this._stateSurgeon;
844
+ if (!info || ignoreUrls.some((ignore) => info.url.includes(ignore))) {
845
+ return originalXHRSend.call(this, body);
846
+ }
847
+ info.startTime = Date.now();
848
+ const requestId = `xhr_${info.startTime}_${Math.random().toString(36).slice(2, 8)}`;
849
+ const requestInfo = {
850
+ url: info.url,
851
+ method: info.method
852
+ };
853
+ this.addEventListener("loadend", () => {
854
+ const duration = Date.now() - info.startTime;
855
+ const responseInfo = {
856
+ status: this.status,
857
+ statusText: this.statusText
858
+ };
859
+ if (captureResponseBody && this.responseText) {
860
+ responseInfo.body = truncateBody(this.responseText, maxBodySize);
861
+ }
862
+ client.captureStateChange("api", requestInfo, responseInfo, {
863
+ actionType: "API_RESPONSE",
864
+ actionPayload: {
865
+ requestId,
866
+ url: info.url,
867
+ method: info.method,
868
+ status: this.status,
869
+ duration
870
+ },
871
+ function: "XMLHttpRequest"
872
+ });
873
+ });
874
+ return originalXHRSend.call(this, body);
875
+ };
876
+ xhrInstrumented = true;
877
+ return () => {
878
+ if (originalXHROpen) {
879
+ XMLHttpRequest.prototype.open = originalXHROpen;
880
+ originalXHROpen = null;
881
+ }
882
+ if (originalXHRSend) {
883
+ XMLHttpRequest.prototype.send = originalXHRSend;
884
+ originalXHRSend = null;
885
+ }
886
+ xhrInstrumented = false;
887
+ };
888
+ }
889
+ function truncateBody(body, maxSize) {
890
+ let str;
891
+ if (typeof body === "string") {
892
+ str = body;
893
+ } else if (body instanceof FormData) {
894
+ str = "[FormData]";
895
+ } else if (body instanceof Blob) {
896
+ str = `[Blob: ${body.size} bytes]`;
897
+ } else if (body instanceof ArrayBuffer) {
898
+ str = `[ArrayBuffer: ${body.byteLength} bytes]`;
899
+ } else {
900
+ try {
901
+ str = JSON.stringify(body);
902
+ } catch {
903
+ str = String(body);
904
+ }
905
+ }
906
+ if (str.length > maxSize) {
907
+ return str.slice(0, maxSize) + "... [truncated]";
908
+ }
909
+ return str;
910
+ }
911
+
912
+ // src/instrument/react.ts
913
+ var originalUseState = null;
914
+ var originalUseReducer = null;
915
+ var isInstrumented = false;
916
+ function instrumentReact(React, options = {}) {
917
+ if (isInstrumented) {
918
+ console.warn("[State Surgeon] React already instrumented");
919
+ return () => {
920
+ };
921
+ }
922
+ const client = options.client || getClient();
923
+ const captureComponentName = options.captureComponentName !== false;
924
+ originalUseState = React.useState;
925
+ originalUseReducer = React.useReducer;
926
+ React.useState = function(initialState) {
927
+ const [state, originalSetState] = originalUseState(initialState);
928
+ const instrumentedSetState = (newStateOrUpdater) => {
929
+ const previousState = deepClone(state);
930
+ const newState = typeof newStateOrUpdater === "function" ? newStateOrUpdater(state) : newStateOrUpdater;
931
+ client.captureStateChange("react", previousState, newState, {
932
+ actionType: "SET_STATE",
933
+ component: captureComponentName ? getComponentName() : void 0,
934
+ function: "useState"
935
+ });
936
+ return originalSetState(newStateOrUpdater);
937
+ };
938
+ return [state, instrumentedSetState];
939
+ };
940
+ React.useReducer = function(reducer, initialArg, init) {
941
+ const instrumentedReducer = ((state, action) => {
942
+ const previousState = deepClone(state);
943
+ const newState = reducer(state, action);
944
+ client.captureStateChange("react", previousState, newState, {
945
+ actionType: "DISPATCH",
946
+ actionPayload: action,
947
+ component: captureComponentName ? getComponentName() : void 0,
948
+ function: "useReducer"
949
+ });
950
+ return newState;
951
+ });
952
+ return originalUseReducer(instrumentedReducer, initialArg, init);
953
+ };
954
+ isInstrumented = true;
955
+ return () => {
956
+ if (originalUseState) {
957
+ React.useState = originalUseState;
958
+ }
959
+ if (originalUseReducer) {
960
+ React.useReducer = originalUseReducer;
961
+ }
962
+ isInstrumented = false;
963
+ originalUseState = null;
964
+ originalUseReducer = null;
965
+ };
966
+ }
967
+ function getComponentName() {
968
+ const stack = new Error().stack;
969
+ if (!stack) return void 0;
970
+ const lines = stack.split("\n");
971
+ for (const line of lines) {
972
+ if (line.includes("instrumentedSetState") || line.includes("instrumentedReducer") || line.includes("useState") || line.includes("useReducer") || line.includes("react-dom") || line.includes("react.development")) {
973
+ continue;
974
+ }
975
+ const match = line.match(/at\s+([A-Z][A-Za-z0-9_]*)/);
976
+ if (match) {
977
+ return match[1];
978
+ }
979
+ }
980
+ return void 0;
981
+ }
982
+
983
+ // src/instrument/redux.ts
984
+ function createReduxMiddleware(options = {}) {
985
+ const client = options.client || getClient();
986
+ const ignoreActions = new Set(options.ignoreActions || []);
987
+ const storeName = options.storeName || "redux";
988
+ return (storeAPI) => (next) => (action) => {
989
+ const actionType = action?.type;
990
+ if (actionType && ignoreActions.has(actionType)) {
991
+ return next(action);
992
+ }
993
+ const previousState = deepClone(storeAPI.getState());
994
+ const startTime = typeof performance !== "undefined" ? performance.now() : Date.now();
995
+ const result = next(action);
996
+ const nextState = deepClone(storeAPI.getState());
997
+ const duration = (typeof performance !== "undefined" ? performance.now() : Date.now()) - startTime;
998
+ const mutation = client.captureStateChange("redux", previousState, nextState, {
999
+ actionType: "DISPATCH",
1000
+ actionPayload: action,
1001
+ component: storeName,
1002
+ function: actionType || "dispatch"
1003
+ });
1004
+ mutation.duration = duration;
1005
+ return result;
1006
+ };
1007
+ }
1008
+ function instrumentReduxStore(store, options = {}) {
1009
+ const client = options.client || getClient();
1010
+ const ignoreActions = new Set(options.ignoreActions || []);
1011
+ const storeName = options.storeName || "redux";
1012
+ const originalDispatch = store.dispatch;
1013
+ store.dispatch = function instrumentedDispatch(action) {
1014
+ const actionType = action?.type;
1015
+ if (actionType && ignoreActions.has(actionType)) {
1016
+ return originalDispatch(action);
1017
+ }
1018
+ const previousState = deepClone(store.getState());
1019
+ const startTime = typeof performance !== "undefined" ? performance.now() : Date.now();
1020
+ const result = originalDispatch(action);
1021
+ const nextState = deepClone(store.getState());
1022
+ const duration = (typeof performance !== "undefined" ? performance.now() : Date.now()) - startTime;
1023
+ const mutation = client.captureStateChange("redux", previousState, nextState, {
1024
+ actionType: "DISPATCH",
1025
+ actionPayload: action,
1026
+ component: storeName,
1027
+ function: actionType || "dispatch"
1028
+ });
1029
+ mutation.duration = duration;
1030
+ return result;
1031
+ };
1032
+ return () => {
1033
+ store.dispatch = originalDispatch;
1034
+ };
1035
+ }
1036
+
1037
+ // src/instrument/zustand.ts
1038
+ function instrumentZustand(stateCreator, options = {}) {
1039
+ const client = options.client || getClient();
1040
+ const storeName = options.storeName || "zustand";
1041
+ return (set, get, api) => {
1042
+ const instrumentedSet = (partial, replace) => {
1043
+ const previousState = deepClone(get());
1044
+ set(partial, replace);
1045
+ const nextState = deepClone(get());
1046
+ client.captureStateChange("zustand", previousState, nextState, {
1047
+ actionType: "SET_STATE",
1048
+ actionPayload: typeof partial === "function" ? "updater function" : partial,
1049
+ component: storeName,
1050
+ function: "set"
1051
+ });
1052
+ };
1053
+ return stateCreator(instrumentedSet, get, api);
1054
+ };
1055
+ }
1056
+
1057
+ // src/recorder/index.ts
1058
+ var recorder_exports = {};
1059
+ __export(recorder_exports, {
1060
+ MutationStore: () => MutationStore,
1061
+ TimelineReconstructor: () => TimelineReconstructor,
1062
+ createAPIRoutes: () => createAPIRoutes,
1063
+ createRecorderServer: () => createRecorderServer
1064
+ });
1065
+ function createAPIRoutes(store, reconstructor) {
1066
+ const router = express.Router();
1067
+ router.get("/health", (_req, res) => {
1068
+ res.json({ status: "ok", timestamp: Date.now() });
1069
+ });
1070
+ router.get("/sessions", (req, res) => {
1071
+ try {
1072
+ const appId = req.query.appId;
1073
+ const sessions = store.getSessions(appId);
1074
+ res.json({ sessions });
1075
+ } catch (error) {
1076
+ res.status(500).json({ error: String(error) });
1077
+ }
1078
+ });
1079
+ router.get("/sessions/:sessionId", (req, res) => {
1080
+ try {
1081
+ const session = store.getSession(req.params.sessionId);
1082
+ if (!session) {
1083
+ return res.status(404).json({ error: "Session not found" });
1084
+ }
1085
+ res.json({ session });
1086
+ } catch (error) {
1087
+ res.status(500).json({ error: String(error) });
1088
+ }
1089
+ });
1090
+ router.delete("/sessions/:sessionId", (req, res) => {
1091
+ try {
1092
+ const deleted = store.deleteSession(req.params.sessionId);
1093
+ res.json({ deleted });
1094
+ } catch (error) {
1095
+ res.status(500).json({ error: String(error) });
1096
+ }
1097
+ });
1098
+ router.get("/sessions/:sessionId/timeline", (req, res) => {
1099
+ try {
1100
+ const timeline = store.getTimeline(req.params.sessionId);
1101
+ res.json({ timeline, count: timeline.length });
1102
+ } catch (error) {
1103
+ res.status(500).json({ error: String(error) });
1104
+ }
1105
+ });
1106
+ router.get("/mutations", (req, res) => {
1107
+ try {
1108
+ const options = {
1109
+ sessionId: req.query.sessionId,
1110
+ startTime: req.query.startTime ? Number(req.query.startTime) : void 0,
1111
+ endTime: req.query.endTime ? Number(req.query.endTime) : void 0,
1112
+ source: req.query.source,
1113
+ component: req.query.component,
1114
+ limit: req.query.limit ? Number(req.query.limit) : 100,
1115
+ offset: req.query.offset ? Number(req.query.offset) : 0,
1116
+ sortOrder: req.query.sortOrder || "asc"
1117
+ };
1118
+ const mutations = store.queryMutations(options);
1119
+ res.json({ mutations, count: mutations.length });
1120
+ } catch (error) {
1121
+ res.status(500).json({ error: String(error) });
1122
+ }
1123
+ });
1124
+ router.get("/mutations/:mutationId", (req, res) => {
1125
+ try {
1126
+ const mutation = store.getMutation(req.params.mutationId);
1127
+ if (!mutation) {
1128
+ return res.status(404).json({ error: "Mutation not found" });
1129
+ }
1130
+ res.json({ mutation });
1131
+ } catch (error) {
1132
+ res.status(500).json({ error: String(error) });
1133
+ }
1134
+ });
1135
+ router.get("/mutations/:mutationId/state", async (req, res) => {
1136
+ try {
1137
+ const state = await reconstructor.getStateAtMutation(req.params.mutationId);
1138
+ res.json({ state });
1139
+ } catch (error) {
1140
+ res.status(500).json({ error: String(error) });
1141
+ }
1142
+ });
1143
+ router.get("/mutations/:mutationId/chain", async (req, res) => {
1144
+ try {
1145
+ const maxDepth = req.query.maxDepth ? Number(req.query.maxDepth) : 100;
1146
+ const chain = await reconstructor.findCausalChain(req.params.mutationId, maxDepth);
1147
+ res.json({ chain });
1148
+ } catch (error) {
1149
+ res.status(500).json({ error: String(error) });
1150
+ }
1151
+ });
1152
+ router.get("/sessions/:sessionId/path", async (req, res) => {
1153
+ try {
1154
+ const path = req.query.path;
1155
+ if (!path) {
1156
+ return res.status(400).json({ error: "Path is required" });
1157
+ }
1158
+ const mutations = await reconstructor.findMutationsForPath(req.params.sessionId, path);
1159
+ res.json({ mutations, count: mutations.length });
1160
+ } catch (error) {
1161
+ res.status(500).json({ error: String(error) });
1162
+ }
1163
+ });
1164
+ router.get("/sessions/:sessionId/summary", async (req, res) => {
1165
+ try {
1166
+ const startTime = Number(req.query.startTime);
1167
+ const endTime = Number(req.query.endTime);
1168
+ if (isNaN(startTime) || isNaN(endTime)) {
1169
+ return res.status(400).json({ error: "startTime and endTime are required" });
1170
+ }
1171
+ const summary = await reconstructor.summarizeTimeRange(
1172
+ req.params.sessionId,
1173
+ startTime,
1174
+ endTime
1175
+ );
1176
+ res.json({ summary });
1177
+ } catch (error) {
1178
+ res.status(500).json({ error: String(error) });
1179
+ }
1180
+ });
1181
+ router.get("/stats", (_req, res) => {
1182
+ try {
1183
+ const stats = store.getStats();
1184
+ res.json({ stats });
1185
+ } catch (error) {
1186
+ res.status(500).json({ error: String(error) });
1187
+ }
1188
+ });
1189
+ router.delete("/clear", (_req, res) => {
1190
+ try {
1191
+ store.clear();
1192
+ reconstructor.clearCache();
1193
+ res.json({ success: true });
1194
+ } catch (error) {
1195
+ res.status(500).json({ error: String(error) });
1196
+ }
1197
+ });
1198
+ return router;
1199
+ }
1200
+
1201
+ // src/recorder/reconstructor.ts
1202
+ var TimelineReconstructor = class {
1203
+ constructor(store) {
1204
+ this.stateCache = /* @__PURE__ */ new Map();
1205
+ this.maxCacheSize = 1e3;
1206
+ this.store = store;
1207
+ }
1208
+ /**
1209
+ * Reconstructs the state at a specific mutation
1210
+ */
1211
+ async getStateAtMutation(mutationId) {
1212
+ const cached = this.stateCache.get(mutationId);
1213
+ if (cached !== void 0) {
1214
+ return deepClone(cached);
1215
+ }
1216
+ const targetMutation = this.store.getMutation(mutationId);
1217
+ if (!targetMutation) {
1218
+ throw new Error(`Mutation not found: ${mutationId}`);
1219
+ }
1220
+ const timeline = this.store.getTimeline(targetMutation.sessionId);
1221
+ const targetIndex = timeline.findIndex((m) => m.id === mutationId);
1222
+ if (targetIndex === -1) {
1223
+ throw new Error(`Mutation not in timeline: ${mutationId}`);
1224
+ }
1225
+ let state = {};
1226
+ for (let i = 0; i <= targetIndex; i++) {
1227
+ const mutation = timeline[i];
1228
+ state = mutation.nextState;
1229
+ this.cacheState(mutation.id, state);
1230
+ }
1231
+ return deepClone(state);
1232
+ }
1233
+ /**
1234
+ * Reconstructs the state at a specific timestamp
1235
+ */
1236
+ async getStateAtTime(sessionId, timestamp) {
1237
+ const timeline = this.store.getTimeline(sessionId);
1238
+ let lastMutation = null;
1239
+ for (const mutation of timeline) {
1240
+ if (mutation.timestamp <= timestamp) {
1241
+ lastMutation = mutation;
1242
+ } else {
1243
+ break;
1244
+ }
1245
+ }
1246
+ if (!lastMutation) {
1247
+ return {};
1248
+ }
1249
+ return this.getStateAtMutation(lastMutation.id);
1250
+ }
1251
+ /**
1252
+ * Finds the causal chain leading to a mutation
1253
+ */
1254
+ async findCausalChain(mutationId, maxDepth = 100) {
1255
+ const chain = [];
1256
+ let currentId = mutationId;
1257
+ let depth = 0;
1258
+ while (currentId && depth < maxDepth) {
1259
+ const mutation = this.store.getMutation(currentId);
1260
+ if (!mutation) break;
1261
+ chain.push(mutation);
1262
+ if (mutation.parentMutationId) {
1263
+ currentId = mutation.parentMutationId;
1264
+ } else if (mutation.causes && mutation.causes.length > 0) {
1265
+ currentId = mutation.causes[0];
1266
+ } else {
1267
+ break;
1268
+ }
1269
+ depth++;
1270
+ }
1271
+ return {
1272
+ mutations: chain.reverse(),
1273
+ rootCause: chain[0]
1274
+ };
1275
+ }
1276
+ /**
1277
+ * Binary search to find when state became invalid
1278
+ */
1279
+ async findStateCorruption(sessionId, validator) {
1280
+ const timeline = this.store.getTimeline(sessionId);
1281
+ if (timeline.length === 0) {
1282
+ return null;
1283
+ }
1284
+ let left = 0;
1285
+ let right = timeline.length - 1;
1286
+ let firstBad = null;
1287
+ while (left <= right) {
1288
+ const mid = Math.floor((left + right) / 2);
1289
+ const state = await this.getStateAtMutation(timeline[mid].id);
1290
+ const isValid = validator(state);
1291
+ if (isValid) {
1292
+ left = mid + 1;
1293
+ } else {
1294
+ firstBad = mid;
1295
+ right = mid - 1;
1296
+ }
1297
+ }
1298
+ return firstBad !== null ? timeline[firstBad] : null;
1299
+ }
1300
+ /**
1301
+ * Finds all mutations that touched a specific path
1302
+ */
1303
+ async findMutationsForPath(sessionId, path) {
1304
+ const timeline = this.store.getTimeline(sessionId);
1305
+ const result = [];
1306
+ for (const mutation of timeline) {
1307
+ if (mutation.diff) {
1308
+ for (const diff of mutation.diff) {
1309
+ if (diff.path === path || diff.path.startsWith(path + ".") || diff.path.startsWith(path + "[")) {
1310
+ result.push(mutation);
1311
+ break;
1312
+ }
1313
+ }
1314
+ } else {
1315
+ const prevValue = getValueAtPath(mutation.previousState, path);
1316
+ const nextValue = getValueAtPath(mutation.nextState, path);
1317
+ if (prevValue !== nextValue) {
1318
+ result.push(mutation);
1319
+ }
1320
+ }
1321
+ }
1322
+ return result;
1323
+ }
1324
+ /**
1325
+ * Compares two sessions to find divergence points
1326
+ */
1327
+ async compareSessions(sessionId1, sessionId2) {
1328
+ const timeline1 = this.store.getTimeline(sessionId1);
1329
+ const timeline2 = this.store.getTimeline(sessionId2);
1330
+ let divergencePoint;
1331
+ const minLength = Math.min(timeline1.length, timeline2.length);
1332
+ for (let i = 0; i < minLength; i++) {
1333
+ const state1 = await this.getStateAtMutation(timeline1[i].id);
1334
+ const state2 = await this.getStateAtMutation(timeline2[i].id);
1335
+ if (JSON.stringify(state1) !== JSON.stringify(state2)) {
1336
+ divergencePoint = i;
1337
+ break;
1338
+ }
1339
+ }
1340
+ const actions1 = new Set(timeline1.map((m) => `${m.actionType}:${JSON.stringify(m.actionPayload)}`));
1341
+ const actions2 = new Set(timeline2.map((m) => `${m.actionType}:${JSON.stringify(m.actionPayload)}`));
1342
+ const session1Only = timeline1.filter(
1343
+ (m) => !actions2.has(`${m.actionType}:${JSON.stringify(m.actionPayload)}`)
1344
+ );
1345
+ const session2Only = timeline2.filter(
1346
+ (m) => !actions1.has(`${m.actionType}:${JSON.stringify(m.actionPayload)}`)
1347
+ );
1348
+ return {
1349
+ divergencePoint,
1350
+ session1Only,
1351
+ session2Only
1352
+ };
1353
+ }
1354
+ /**
1355
+ * Generates a summary of what happened in a time range
1356
+ */
1357
+ async summarizeTimeRange(sessionId, startTime, endTime) {
1358
+ const mutations = this.store.queryMutations({
1359
+ sessionId,
1360
+ startTime,
1361
+ endTime
1362
+ });
1363
+ const componentBreakdown = {};
1364
+ const sourceBreakdown = {};
1365
+ const pathsSet = /* @__PURE__ */ new Set();
1366
+ for (const mutation of mutations) {
1367
+ const component = mutation.component || "unknown";
1368
+ componentBreakdown[component] = (componentBreakdown[component] || 0) + 1;
1369
+ sourceBreakdown[mutation.source] = (sourceBreakdown[mutation.source] || 0) + 1;
1370
+ if (mutation.diff) {
1371
+ for (const diff of mutation.diff) {
1372
+ pathsSet.add(diff.path);
1373
+ }
1374
+ }
1375
+ }
1376
+ return {
1377
+ mutationCount: mutations.length,
1378
+ componentBreakdown,
1379
+ sourceBreakdown,
1380
+ pathsChanged: Array.from(pathsSet)
1381
+ };
1382
+ }
1383
+ /**
1384
+ * Clears the state cache
1385
+ */
1386
+ clearCache() {
1387
+ this.stateCache.clear();
1388
+ }
1389
+ cacheState(mutationId, state) {
1390
+ if (this.stateCache.size >= this.maxCacheSize) {
1391
+ const firstKey = this.stateCache.keys().next().value;
1392
+ if (firstKey) {
1393
+ this.stateCache.delete(firstKey);
1394
+ }
1395
+ }
1396
+ this.stateCache.set(mutationId, deepClone(state));
1397
+ }
1398
+ };
1399
+
1400
+ // src/recorder/store.ts
1401
+ var MutationStore = class {
1402
+ constructor(options = {}) {
1403
+ this.sessions = /* @__PURE__ */ new Map();
1404
+ this.mutations = /* @__PURE__ */ new Map();
1405
+ this.totalMutationCount = 0;
1406
+ this.options = {
1407
+ maxMutationsPerSession: options.maxMutationsPerSession ?? 1e4,
1408
+ maxTotalMutations: options.maxTotalMutations ?? 1e5,
1409
+ sessionTimeout: options.sessionTimeout ?? 30 * 60 * 1e3,
1410
+ // 30 minutes
1411
+ debug: options.debug ?? false
1412
+ };
1413
+ }
1414
+ /**
1415
+ * Creates or updates a session
1416
+ */
1417
+ registerSession(sessionId, appId, metadata) {
1418
+ let session = this.sessions.get(sessionId);
1419
+ if (!session) {
1420
+ session = {
1421
+ id: sessionId,
1422
+ appId,
1423
+ startTime: /* @__PURE__ */ new Date(),
1424
+ metadata,
1425
+ mutationCount: 0
1426
+ };
1427
+ this.sessions.set(sessionId, session);
1428
+ this.mutations.set(sessionId, []);
1429
+ this.log("Session registered:", sessionId);
1430
+ }
1431
+ return session;
1432
+ }
1433
+ /**
1434
+ * Ends a session
1435
+ */
1436
+ endSession(sessionId) {
1437
+ const session = this.sessions.get(sessionId);
1438
+ if (session) {
1439
+ session.endTime = /* @__PURE__ */ new Date();
1440
+ this.log("Session ended:", sessionId);
1441
+ }
1442
+ }
1443
+ /**
1444
+ * Stores a mutation
1445
+ */
1446
+ storeMutation(mutation) {
1447
+ const sessionId = mutation.sessionId;
1448
+ if (!this.sessions.has(sessionId)) {
1449
+ this.registerSession(sessionId, "unknown");
1450
+ }
1451
+ let sessionMutations = this.mutations.get(sessionId);
1452
+ if (!sessionMutations) {
1453
+ sessionMutations = [];
1454
+ this.mutations.set(sessionId, sessionMutations);
1455
+ }
1456
+ if (!mutation.diff && mutation.previousState !== void 0 && mutation.nextState !== void 0) {
1457
+ mutation.diff = calculateDiff(mutation.previousState, mutation.nextState);
1458
+ }
1459
+ if (sessionMutations.length >= this.options.maxMutationsPerSession) {
1460
+ sessionMutations.shift();
1461
+ this.totalMutationCount--;
1462
+ }
1463
+ if (this.totalMutationCount >= this.options.maxTotalMutations) {
1464
+ this.evictOldestMutations();
1465
+ }
1466
+ sessionMutations.push(mutation);
1467
+ this.totalMutationCount++;
1468
+ const session = this.sessions.get(sessionId);
1469
+ session.mutationCount++;
1470
+ this.log("Mutation stored:", mutation.id);
1471
+ }
1472
+ /**
1473
+ * Stores multiple mutations at once
1474
+ */
1475
+ storeMutations(mutations) {
1476
+ for (const mutation of mutations) {
1477
+ this.storeMutation(mutation);
1478
+ }
1479
+ }
1480
+ /**
1481
+ * Gets a session by ID
1482
+ */
1483
+ getSession(sessionId) {
1484
+ return this.sessions.get(sessionId);
1485
+ }
1486
+ /**
1487
+ * Gets all sessions
1488
+ */
1489
+ getSessions(appId) {
1490
+ const sessions = Array.from(this.sessions.values());
1491
+ if (appId) {
1492
+ return sessions.filter((s) => s.appId === appId);
1493
+ }
1494
+ return sessions;
1495
+ }
1496
+ /**
1497
+ * Gets a specific mutation by ID
1498
+ */
1499
+ getMutation(mutationId) {
1500
+ for (const sessionMutations of this.mutations.values()) {
1501
+ const mutation = sessionMutations.find((m) => m.id === mutationId);
1502
+ if (mutation) return mutation;
1503
+ }
1504
+ return void 0;
1505
+ }
1506
+ /**
1507
+ * Queries mutations with filters
1508
+ */
1509
+ queryMutations(options = {}) {
1510
+ let results = [];
1511
+ if (options.sessionId) {
1512
+ const sessionMutations = this.mutations.get(options.sessionId);
1513
+ if (sessionMutations) {
1514
+ results = [...sessionMutations];
1515
+ }
1516
+ } else {
1517
+ for (const sessionMutations of this.mutations.values()) {
1518
+ results.push(...sessionMutations);
1519
+ }
1520
+ }
1521
+ if (options.startTime !== void 0) {
1522
+ results = results.filter((m) => m.timestamp >= options.startTime);
1523
+ }
1524
+ if (options.endTime !== void 0) {
1525
+ results = results.filter((m) => m.timestamp <= options.endTime);
1526
+ }
1527
+ if (options.source) {
1528
+ results = results.filter((m) => m.source === options.source);
1529
+ }
1530
+ if (options.component) {
1531
+ results = results.filter((m) => m.component === options.component);
1532
+ }
1533
+ results.sort((a, b) => {
1534
+ const order = options.sortOrder === "desc" ? -1 : 1;
1535
+ return (a.logicalClock - b.logicalClock) * order;
1536
+ });
1537
+ if (options.offset !== void 0) {
1538
+ results = results.slice(options.offset);
1539
+ }
1540
+ if (options.limit !== void 0) {
1541
+ results = results.slice(0, options.limit);
1542
+ }
1543
+ return results;
1544
+ }
1545
+ /**
1546
+ * Gets the timeline for a session
1547
+ */
1548
+ getTimeline(sessionId) {
1549
+ const mutations = this.mutations.get(sessionId) || [];
1550
+ return [...mutations].sort((a, b) => a.logicalClock - b.logicalClock);
1551
+ }
1552
+ /**
1553
+ * Deletes a session and its mutations
1554
+ */
1555
+ deleteSession(sessionId) {
1556
+ const sessionMutations = this.mutations.get(sessionId);
1557
+ if (sessionMutations) {
1558
+ this.totalMutationCount -= sessionMutations.length;
1559
+ }
1560
+ this.mutations.delete(sessionId);
1561
+ return this.sessions.delete(sessionId);
1562
+ }
1563
+ /**
1564
+ * Clears all data
1565
+ */
1566
+ clear() {
1567
+ this.sessions.clear();
1568
+ this.mutations.clear();
1569
+ this.totalMutationCount = 0;
1570
+ this.log("Store cleared");
1571
+ }
1572
+ /**
1573
+ * Gets store statistics
1574
+ */
1575
+ getStats() {
1576
+ const mutationsPerSession = {};
1577
+ for (const [sessionId, mutations] of this.mutations.entries()) {
1578
+ mutationsPerSession[sessionId] = mutations.length;
1579
+ }
1580
+ return {
1581
+ sessionCount: this.sessions.size,
1582
+ totalMutations: this.totalMutationCount,
1583
+ mutationsPerSession
1584
+ };
1585
+ }
1586
+ /**
1587
+ * Evicts oldest mutations when capacity is exceeded
1588
+ */
1589
+ evictOldestMutations() {
1590
+ let oldestSession = null;
1591
+ let oldestTime = Infinity;
1592
+ for (const [sessionId, session] of this.sessions.entries()) {
1593
+ if (session.startTime.getTime() < oldestTime) {
1594
+ oldestTime = session.startTime.getTime();
1595
+ oldestSession = sessionId;
1596
+ }
1597
+ }
1598
+ if (oldestSession) {
1599
+ const mutations = this.mutations.get(oldestSession);
1600
+ if (mutations && mutations.length > 0) {
1601
+ mutations.shift();
1602
+ this.totalMutationCount--;
1603
+ this.log("Evicted oldest mutation from:", oldestSession);
1604
+ }
1605
+ }
1606
+ }
1607
+ log(...args) {
1608
+ if (this.options.debug) {
1609
+ console.log("[MutationStore]", ...args);
1610
+ }
1611
+ }
1612
+ };
1613
+
1614
+ // src/recorder/server.ts
1615
+ function createRecorderServer(options = {}) {
1616
+ const port = options.port ?? 8080;
1617
+ const wsPort = options.wsPort ?? 8081;
1618
+ const apiPath = options.apiPath ?? "/api";
1619
+ const debug = options.debug ?? false;
1620
+ const store = new MutationStore(options.storeOptions);
1621
+ const reconstructor = new TimelineReconstructor(store);
1622
+ const app = express__default.default();
1623
+ app.use(express__default.default.json({ limit: "10mb" }));
1624
+ if (options.cors !== false) {
1625
+ app.use((_req, res, next) => {
1626
+ res.header("Access-Control-Allow-Origin", "*");
1627
+ res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
1628
+ res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
1629
+ next();
1630
+ });
1631
+ }
1632
+ app.use(apiPath, createAPIRoutes(store, reconstructor));
1633
+ app.get("/_surgeon", (_req, res) => {
1634
+ res.send(`
1635
+ <!DOCTYPE html>
1636
+ <html>
1637
+ <head>
1638
+ <title>State Surgeon Dashboard</title>
1639
+ <style>
1640
+ body { font-family: system-ui, sans-serif; padding: 2rem; background: #1a1a2e; color: #eee; }
1641
+ h1 { color: #00d9ff; }
1642
+ .stats { background: #16213e; padding: 1rem; border-radius: 8px; margin: 1rem 0; }
1643
+ pre { background: #0f0f23; padding: 1rem; border-radius: 4px; overflow: auto; }
1644
+ </style>
1645
+ </head>
1646
+ <body>
1647
+ <h1>\u{1F52C} State Surgeon Dashboard</h1>
1648
+ <p>Recorder is running. Connect your application to start capturing mutations.</p>
1649
+ <div class="stats">
1650
+ <h3>Quick Stats</h3>
1651
+ <div id="stats">Loading...</div>
1652
+ </div>
1653
+ <h3>Recent Sessions</h3>
1654
+ <pre id="sessions">Loading...</pre>
1655
+ <script>
1656
+ async function loadData() {
1657
+ try {
1658
+ const statsRes = await fetch('${apiPath}/stats');
1659
+ const stats = await statsRes.json();
1660
+ document.getElementById('stats').innerHTML =
1661
+ '<pre>' + JSON.stringify(stats, null, 2) + '</pre>';
1662
+
1663
+ const sessionsRes = await fetch('${apiPath}/sessions');
1664
+ const sessions = await sessionsRes.json();
1665
+ document.getElementById('sessions').textContent =
1666
+ JSON.stringify(sessions, null, 2);
1667
+ } catch (e) {
1668
+ document.getElementById('stats').textContent = 'Error loading stats';
1669
+ }
1670
+ }
1671
+ loadData();
1672
+ setInterval(loadData, 5000);
1673
+ </script>
1674
+ </body>
1675
+ </html>
1676
+ `);
1677
+ });
1678
+ const httpServer = app.listen(0);
1679
+ httpServer.close();
1680
+ let wss;
1681
+ const clients = /* @__PURE__ */ new Map();
1682
+ function log(...args) {
1683
+ if (debug) {
1684
+ console.log("[State Surgeon Recorder]", ...args);
1685
+ }
1686
+ }
1687
+ function handleMessage(ws, message) {
1688
+ log("Received message:", message.type);
1689
+ switch (message.type) {
1690
+ case "REGISTER_SESSION": {
1691
+ const payload = message.payload;
1692
+ store.registerSession(payload.sessionId, payload.appId, {
1693
+ userAgent: payload.userAgent,
1694
+ url: payload.url
1695
+ });
1696
+ clients.set(ws, { sessionId: payload.sessionId });
1697
+ ws.send(JSON.stringify({
1698
+ type: "SESSION_CONFIRMED",
1699
+ payload: { sessionId: payload.sessionId }
1700
+ }));
1701
+ log("Session registered:", payload.sessionId);
1702
+ break;
1703
+ }
1704
+ case "MUTATION_RECORDED": {
1705
+ const mutation = message.payload;
1706
+ store.storeMutation(mutation);
1707
+ ws.send(JSON.stringify({
1708
+ type: "MUTATION_ACKNOWLEDGED",
1709
+ payload: { mutationId: mutation.id, receivedAt: Date.now() }
1710
+ }));
1711
+ break;
1712
+ }
1713
+ case "MUTATION_BATCH": {
1714
+ const payload = message.payload;
1715
+ store.storeMutations(payload.mutations);
1716
+ ws.send(JSON.stringify({
1717
+ type: "BATCH_ACKNOWLEDGED",
1718
+ payload: { count: payload.mutations.length, receivedAt: Date.now() }
1719
+ }));
1720
+ log("Batch received:", payload.mutations.length, "mutations");
1721
+ break;
1722
+ }
1723
+ case "END_SESSION": {
1724
+ const clientInfo = clients.get(ws);
1725
+ if (clientInfo?.sessionId) {
1726
+ store.endSession(clientInfo.sessionId);
1727
+ log("Session ended:", clientInfo.sessionId);
1728
+ }
1729
+ break;
1730
+ }
1731
+ default:
1732
+ log("Unknown message type:", message.type);
1733
+ }
1734
+ }
1735
+ const server = {
1736
+ app,
1737
+ httpServer: null,
1738
+ wss: null,
1739
+ store,
1740
+ reconstructor,
1741
+ async start() {
1742
+ return new Promise((resolve) => {
1743
+ server.httpServer = app.listen(port, () => {
1744
+ log(`HTTP server listening on port ${port}`);
1745
+ });
1746
+ wss = new ws.WebSocketServer({ port: wsPort });
1747
+ server.wss = wss;
1748
+ wss.on("connection", (ws) => {
1749
+ log("Client connected");
1750
+ clients.set(ws, {});
1751
+ ws.on("message", (data) => {
1752
+ try {
1753
+ const message = JSON.parse(data.toString());
1754
+ handleMessage(ws, message);
1755
+ } catch (error) {
1756
+ log("Error parsing message:", error);
1757
+ }
1758
+ });
1759
+ ws.on("close", () => {
1760
+ const clientInfo = clients.get(ws);
1761
+ if (clientInfo?.sessionId) {
1762
+ store.endSession(clientInfo.sessionId);
1763
+ }
1764
+ clients.delete(ws);
1765
+ log("Client disconnected");
1766
+ });
1767
+ ws.on("error", (error) => {
1768
+ log("WebSocket error:", error);
1769
+ });
1770
+ });
1771
+ wss.on("listening", () => {
1772
+ log(`WebSocket server listening on port ${wsPort}`);
1773
+ resolve();
1774
+ });
1775
+ });
1776
+ },
1777
+ async stop() {
1778
+ return new Promise((resolve) => {
1779
+ for (const ws of clients.keys()) {
1780
+ ws.close();
1781
+ }
1782
+ clients.clear();
1783
+ if (wss) {
1784
+ wss.close(() => {
1785
+ log("WebSocket server closed");
1786
+ });
1787
+ }
1788
+ if (server.httpServer) {
1789
+ server.httpServer.close(() => {
1790
+ log("HTTP server closed");
1791
+ resolve();
1792
+ });
1793
+ } else {
1794
+ resolve();
1795
+ }
1796
+ });
1797
+ }
1798
+ };
1799
+ return server;
1800
+ }
1801
+
1802
+ exports.applyDiff = applyDiff;
1803
+ exports.calculateDiff = calculateDiff;
1804
+ exports.compress = compress;
1805
+ exports.createMutation = createMutation;
1806
+ exports.debounce = debounce;
1807
+ exports.decompress = decompress;
1808
+ exports.deepClone = deepClone;
1809
+ exports.deepEqual = deepEqual;
1810
+ exports.deepMerge = deepMerge;
1811
+ exports.formatBytes = formatBytes;
1812
+ exports.formatDuration = formatDuration;
1813
+ exports.generateSessionId = generateSessionId;
1814
+ exports.getLogicalClock = getLogicalClock;
1815
+ exports.getValueAtPath = getValueAtPath;
1816
+ exports.instrument = instrument_exports;
1817
+ exports.isBrowser = isBrowser;
1818
+ exports.isNode = isNode;
1819
+ exports.parseCallStack = parseCallStack;
1820
+ exports.recorder = recorder_exports;
1821
+ exports.resetLogicalClock = resetLogicalClock;
1822
+ exports.safeParse = safeParse;
1823
+ exports.safeStringify = safeStringify;
1824
+ exports.setValueAtPath = setValueAtPath;
1825
+ exports.simpleHash = simpleHash;
1826
+ exports.throttle = throttle;
1827
+ //# sourceMappingURL=index.js.map
1828
+ //# sourceMappingURL=index.js.map