state-surgeon 1.1.0 → 2.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.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { v4 } from 'uuid';
2
- import express, { Router } from 'express';
2
+ import express from 'express';
3
3
  import { WebSocketServer } from 'ws';
4
4
 
5
5
  var __defProp = Object.defineProperty;
@@ -14,6 +14,320 @@ var __export = (target, all) => {
14
14
  __defProp(target, name, { get: all[name], enumerable: true });
15
15
  };
16
16
 
17
+ // src/core/analyzer.ts
18
+ var StateAnalyzer = class {
19
+ constructor() {
20
+ this.invariants = [];
21
+ this.updateTimestamps = /* @__PURE__ */ new Map();
22
+ }
23
+ /**
24
+ * Register custom invariant rules
25
+ */
26
+ addInvariant(invariant) {
27
+ this.invariants.push(invariant);
28
+ }
29
+ /**
30
+ * Analyze a single mutation for issues
31
+ */
32
+ analyzeMutation(mutation, index, timeline) {
33
+ const issues = [];
34
+ if (mutation.diff) {
35
+ for (const diff of mutation.diff) {
36
+ if (diff.operation === "REMOVE") {
37
+ issues.push({
38
+ id: `issue_${mutation.id}_${diff.path}_loss`,
39
+ category: "state-loss",
40
+ severity: "critical",
41
+ title: `State field removed: ${diff.path}`,
42
+ description: `The field "${diff.path}" was removed from state. This may indicate an overwrite instead of a merge.`,
43
+ mutationId: mutation.id,
44
+ mutationIndex: index,
45
+ path: diff.path,
46
+ previousValue: diff.oldValue,
47
+ currentValue: void 0,
48
+ suggestion: "Use spread operator to preserve existing fields: setState(prev => ({ ...prev, newField }))",
49
+ timestamp: mutation.timestamp
50
+ });
51
+ }
52
+ if (diff.operation !== "REMOVE") {
53
+ const invalidIssue = this.checkInvalidValue(diff, mutation, index);
54
+ if (invalidIssue) {
55
+ issues.push(invalidIssue);
56
+ }
57
+ }
58
+ if (diff.operation === "UPDATE") {
59
+ const typeIssue = this.checkTypeChange(diff, mutation, index);
60
+ if (typeIssue) {
61
+ issues.push(typeIssue);
62
+ }
63
+ }
64
+ }
65
+ }
66
+ if (this.isNoOpUpdate(mutation)) {
67
+ issues.push({
68
+ id: `issue_${mutation.id}_noop`,
69
+ category: "no-op-update",
70
+ severity: "info",
71
+ title: "Redundant state update",
72
+ description: "This mutation did not change any values. Consider memoizing or adding conditions.",
73
+ mutationId: mutation.id,
74
+ mutationIndex: index,
75
+ suggestion: "Add a condition before updating: if (newValue !== currentValue) setState(newValue)",
76
+ timestamp: mutation.timestamp
77
+ });
78
+ }
79
+ const excessiveIssue = this.checkExcessiveUpdates(mutation, index);
80
+ if (excessiveIssue) {
81
+ issues.push(excessiveIssue);
82
+ }
83
+ for (const invariant of this.invariants) {
84
+ const violation = this.checkInvariant(invariant, mutation, index);
85
+ if (violation) {
86
+ issues.push(violation);
87
+ }
88
+ }
89
+ return issues;
90
+ }
91
+ /**
92
+ * Check for invalid values (NaN, unexpected undefined/null)
93
+ */
94
+ checkInvalidValue(diff, mutation, index) {
95
+ const value = diff.newValue;
96
+ if (typeof value === "number" && isNaN(value)) {
97
+ return {
98
+ id: `issue_${mutation.id}_${diff.path}_nan`,
99
+ category: "invalid-value",
100
+ severity: "critical",
101
+ title: `NaN value at: ${diff.path}`,
102
+ description: `The value at "${diff.path}" became NaN. This usually indicates a calculation with undefined/null.`,
103
+ mutationId: mutation.id,
104
+ mutationIndex: index,
105
+ path: diff.path,
106
+ previousValue: diff.oldValue,
107
+ currentValue: value,
108
+ suggestion: "Check for undefined/null values before calculation. Use default values: (value ?? 0)",
109
+ timestamp: mutation.timestamp
110
+ };
111
+ }
112
+ if (value === void 0 && diff.oldValue !== void 0 && diff.operation === "UPDATE") {
113
+ return {
114
+ id: `issue_${mutation.id}_${diff.path}_undefined`,
115
+ category: "invalid-value",
116
+ severity: "critical",
117
+ title: `Value became undefined: ${diff.path}`,
118
+ description: `The field "${diff.path}" changed from a defined value to undefined.`,
119
+ mutationId: mutation.id,
120
+ mutationIndex: index,
121
+ path: diff.path,
122
+ previousValue: diff.oldValue,
123
+ currentValue: void 0,
124
+ suggestion: "Ensure the value is always defined or explicitly handle undefined cases.",
125
+ timestamp: mutation.timestamp
126
+ };
127
+ }
128
+ return null;
129
+ }
130
+ /**
131
+ * Check for unexpected type changes
132
+ */
133
+ checkTypeChange(diff, mutation, index) {
134
+ if (diff.oldValue === void 0 || diff.oldValue === null) return null;
135
+ if (diff.newValue === void 0 || diff.newValue === null) return null;
136
+ const oldType = typeof diff.oldValue;
137
+ const newType = typeof diff.newValue;
138
+ if (oldType !== newType) {
139
+ return {
140
+ id: `issue_${mutation.id}_${diff.path}_type`,
141
+ category: "type-change",
142
+ severity: "warning",
143
+ title: `Type changed: ${diff.path}`,
144
+ description: `The type of "${diff.path}" changed from ${oldType} to ${newType}.`,
145
+ mutationId: mutation.id,
146
+ mutationIndex: index,
147
+ path: diff.path,
148
+ previousValue: diff.oldValue,
149
+ currentValue: diff.newValue,
150
+ suggestion: "Ensure consistent types. Use TypeScript or runtime validation.",
151
+ timestamp: mutation.timestamp
152
+ };
153
+ }
154
+ return null;
155
+ }
156
+ /**
157
+ * Check if mutation is a no-op (no actual changes)
158
+ */
159
+ isNoOpUpdate(mutation) {
160
+ if (!mutation.diff || mutation.diff.length === 0) {
161
+ return true;
162
+ }
163
+ return mutation.diff.every((d) => {
164
+ if (d.operation !== "UPDATE") return false;
165
+ return JSON.stringify(d.oldValue) === JSON.stringify(d.newValue);
166
+ });
167
+ }
168
+ /**
169
+ * Check for excessive updates in short time period
170
+ */
171
+ checkExcessiveUpdates(mutation, index) {
172
+ const key = mutation.component || mutation.source;
173
+ const timestamps = this.updateTimestamps.get(key) || [];
174
+ timestamps.push(mutation.timestamp);
175
+ const cutoff = mutation.timestamp - 100;
176
+ const recentTimestamps = timestamps.filter((t) => t >= cutoff);
177
+ this.updateTimestamps.set(key, recentTimestamps);
178
+ if (recentTimestamps.length > 5) {
179
+ return {
180
+ id: `issue_${mutation.id}_excessive`,
181
+ category: "excessive-updates",
182
+ severity: "warning",
183
+ title: `Excessive updates from: ${key}`,
184
+ description: `${recentTimestamps.length} mutations in 100ms from "${key}". This may cause performance issues.`,
185
+ mutationId: mutation.id,
186
+ mutationIndex: index,
187
+ suggestion: "Use debouncing, batching, or memoization to reduce update frequency.",
188
+ timestamp: mutation.timestamp
189
+ };
190
+ }
191
+ return null;
192
+ }
193
+ /**
194
+ * Check custom invariant rule
195
+ */
196
+ checkInvariant(invariant, mutation, index) {
197
+ if (!mutation.nextState) return null;
198
+ const value = getValueAtPath(mutation.nextState, invariant.path);
199
+ try {
200
+ const isValid = invariant.rule(value);
201
+ if (!isValid) {
202
+ return {
203
+ id: `issue_${mutation.id}_invariant_${invariant.name}`,
204
+ category: "broken-invariant",
205
+ severity: "critical",
206
+ title: `Invariant violated: ${invariant.name}`,
207
+ description: invariant.message,
208
+ mutationId: mutation.id,
209
+ mutationIndex: index,
210
+ path: invariant.path,
211
+ currentValue: value,
212
+ suggestion: `Ensure the invariant "${invariant.name}" is always maintained.`,
213
+ timestamp: mutation.timestamp
214
+ };
215
+ }
216
+ } catch (e) {
217
+ return null;
218
+ }
219
+ return null;
220
+ }
221
+ /**
222
+ * Analyze entire timeline for issues
223
+ */
224
+ analyzeTimeline(timeline) {
225
+ const allIssues = [];
226
+ this.updateTimestamps.clear();
227
+ for (let i = 0; i < timeline.length; i++) {
228
+ const issues = this.analyzeMutation(timeline[i], i, timeline);
229
+ allIssues.push(...issues);
230
+ }
231
+ return allIssues;
232
+ }
233
+ /**
234
+ * Build dependency graph showing which components touch which state paths
235
+ */
236
+ buildDependencyGraph(timeline) {
237
+ const nodes = /* @__PURE__ */ new Map();
238
+ for (const mutation of timeline) {
239
+ if (!mutation.diff) continue;
240
+ const component = mutation.component || mutation.source || "unknown";
241
+ for (const diff of mutation.diff) {
242
+ const existing = nodes.get(diff.path);
243
+ if (existing) {
244
+ existing.components.add(component);
245
+ existing.mutationCount++;
246
+ existing.lastMutationId = mutation.id;
247
+ } else {
248
+ nodes.set(diff.path, {
249
+ path: diff.path,
250
+ components: /* @__PURE__ */ new Set([component]),
251
+ mutationCount: 1,
252
+ lastMutationId: mutation.id
253
+ });
254
+ }
255
+ }
256
+ }
257
+ const couplings = [];
258
+ for (const [path, node] of nodes) {
259
+ if (node.components.size > 1) {
260
+ couplings.push({
261
+ path,
262
+ components: Array.from(node.components),
263
+ severity: node.components.size > 2 ? "critical" : "warning"
264
+ });
265
+ }
266
+ }
267
+ return { nodes, couplings };
268
+ }
269
+ /**
270
+ * Find causal chain - which mutations caused downstream effects
271
+ */
272
+ findCausalChain(mutationId, timeline) {
273
+ const rootIndex = timeline.findIndex((m) => m.id === mutationId);
274
+ if (rootIndex === -1) return null;
275
+ const root = timeline[rootIndex];
276
+ const effects = [];
277
+ if (!root.diff) return { rootMutation: root, effects };
278
+ const changedPaths = new Set(root.diff.map((d) => d.path));
279
+ for (let i = rootIndex + 1; i < timeline.length; i++) {
280
+ const mutation = timeline[i];
281
+ if (!mutation.diff) continue;
282
+ for (const diff of mutation.diff) {
283
+ for (const changedPath of changedPaths) {
284
+ if (diff.path.startsWith(changedPath) || changedPath.startsWith(diff.path)) {
285
+ effects.push({
286
+ mutation,
287
+ causedBy: root.id,
288
+ reason: `Uses path "${diff.path}" which was affected by "${changedPath}"`
289
+ });
290
+ break;
291
+ }
292
+ }
293
+ }
294
+ }
295
+ return { rootMutation: root, effects };
296
+ }
297
+ /**
298
+ * Find the first mutation that corrupted state
299
+ * Uses binary search for efficiency
300
+ */
301
+ findCorruptionPoint(timeline, validator) {
302
+ let left = 0;
303
+ let right = timeline.length - 1;
304
+ let result = null;
305
+ while (left <= right) {
306
+ const mid = Math.floor((left + right) / 2);
307
+ const mutation = timeline[mid];
308
+ if (mutation.nextState && !validator(mutation.nextState)) {
309
+ result = { mutation, index: mid };
310
+ right = mid - 1;
311
+ } else {
312
+ left = mid + 1;
313
+ }
314
+ }
315
+ return result;
316
+ }
317
+ };
318
+ function getValueAtPath(obj, path) {
319
+ if (!path) return obj;
320
+ const parts = path.split(".");
321
+ let current = obj;
322
+ for (const part of parts) {
323
+ if (current === null || current === void 0) return void 0;
324
+ if (typeof current !== "object") return void 0;
325
+ current = current[part];
326
+ }
327
+ return current;
328
+ }
329
+ var analyzer = new StateAnalyzer();
330
+
17
331
  // src/core/diff.ts
18
332
  function deepClone(obj) {
19
333
  if (obj === null || typeof obj !== "object") {
@@ -114,7 +428,7 @@ function applyDiff(state, diffs) {
114
428
  }
115
429
  return result;
116
430
  }
117
- function getValueAtPath(obj, path) {
431
+ function getValueAtPath2(obj, path) {
118
432
  if (!path || path === "root") return obj;
119
433
  const parts = parsePath(path);
120
434
  let current = obj;
@@ -1056,6 +1370,8 @@ __export(recorder_exports, {
1056
1370
  createAPIRoutes: () => createAPIRoutes,
1057
1371
  createRecorderServer: () => createRecorderServer
1058
1372
  });
1373
+
1374
+ // src/recorder/api.ts
1059
1375
  function createAPIRoutes(store, reconstructor) {
1060
1376
  const router = Router();
1061
1377
  router.get("/health", (_req, res) => {
@@ -1306,8 +1622,8 @@ var TimelineReconstructor = class {
1306
1622
  }
1307
1623
  }
1308
1624
  } else {
1309
- const prevValue = getValueAtPath(mutation.previousState, path);
1310
- const nextValue = getValueAtPath(mutation.nextState, path);
1625
+ const prevValue = getValueAtPath2(mutation.previousState, path);
1626
+ const nextValue = getValueAtPath2(mutation.nextState, path);
1311
1627
  if (prevValue !== nextValue) {
1312
1628
  result.push(mutation);
1313
1629
  }
@@ -1793,6 +2109,6 @@ function createRecorderServer(options = {}) {
1793
2109
  return server;
1794
2110
  }
1795
2111
 
1796
- export { applyDiff, calculateDiff, compress, createMutation, debounce, decompress, deepClone, deepEqual, deepMerge, formatBytes, formatDuration, generateSessionId, getLogicalClock, getValueAtPath, instrument_exports as instrument, isBrowser, isNode, parseCallStack, recorder_exports as recorder, resetLogicalClock, safeParse, safeStringify, setValueAtPath, simpleHash, throttle };
2112
+ export { StateAnalyzer, analyzer, applyDiff, calculateDiff, compress, createMutation, debounce, decompress, deepClone, deepEqual, deepMerge, formatBytes, formatDuration, generateSessionId, getLogicalClock, getValueAtPath2 as getValueAtPath, instrument_exports as instrument, isBrowser, isNode, parseCallStack, recorder_exports as recorder, resetLogicalClock, safeParse, safeStringify, setValueAtPath, simpleHash, throttle };
1797
2113
  //# sourceMappingURL=index.mjs.map
1798
2114
  //# sourceMappingURL=index.mjs.map