@signaltree/core 7.3.0 → 7.6.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.
@@ -1,5 +1,7 @@
1
- import { signal } from '@angular/core';
1
+ import { effect, signal } from '@angular/core';
2
2
  import { copyTreeProperties } from '../utils/copy-tree-properties.js';
3
+ import { getPathNotifier } from '../../lib/path-notifier.js';
4
+ import { snapshotState, applyState } from '../../lib/utils.js';
3
5
 
4
6
  function createActivityTracker() {
5
7
  const modules = new Map();
@@ -130,16 +132,149 @@ function createModularMetrics() {
130
132
  }
131
133
  };
132
134
  }
135
+ function toArray(value) {
136
+ if (!value) return [];
137
+ return Array.isArray(value) ? value : [value];
138
+ }
139
+ function matchesPattern(pattern, path) {
140
+ if (pattern === '**') return true;
141
+ if (pattern === path) return true;
142
+ if (pattern.endsWith('.*')) {
143
+ const prefix = pattern.slice(0, -2);
144
+ return path.startsWith(prefix + '.');
145
+ }
146
+ return false;
147
+ }
148
+ function defaultFormatPath(path) {
149
+ const segments = path.split('.');
150
+ let formatted = '';
151
+ for (const segment of segments) {
152
+ if (!segment) continue;
153
+ if (/^\d+$/.test(segment)) {
154
+ formatted += `[${segment}]`;
155
+ } else {
156
+ formatted += formatted ? `.${segment}` : segment;
157
+ }
158
+ }
159
+ return formatted || path;
160
+ }
161
+ function computeChangedPaths(prev, next, maxDepth, maxArrayLength, path = '', depth = 0, output = []) {
162
+ if (prev === next) return output;
163
+ if (depth >= maxDepth) {
164
+ if (path) output.push(path);
165
+ return output;
166
+ }
167
+ if (prev === null || next === null || prev === undefined || next === undefined) {
168
+ if (path) output.push(path);
169
+ return output;
170
+ }
171
+ const prevType = typeof prev;
172
+ const nextType = typeof next;
173
+ if (prevType !== 'object' || nextType !== 'object') {
174
+ if (path) output.push(path);
175
+ return output;
176
+ }
177
+ if (Array.isArray(prev) && Array.isArray(next)) {
178
+ if (prev.length !== next.length) {
179
+ if (path) output.push(path);
180
+ return output;
181
+ }
182
+ if (prev.length > maxArrayLength) {
183
+ if (path) output.push(path);
184
+ return output;
185
+ }
186
+ for (let i = 0; i < prev.length; i += 1) {
187
+ computeChangedPaths(prev[i], next[i], maxDepth, maxArrayLength, path ? `${path}.${i}` : `${i}`, depth + 1, output);
188
+ }
189
+ return output;
190
+ }
191
+ const prevObj = prev;
192
+ const nextObj = next;
193
+ const keys = new Set([...Object.keys(prevObj), ...Object.keys(nextObj)]);
194
+ if (keys.size === 0) {
195
+ if (path) output.push(path);
196
+ return output;
197
+ }
198
+ for (const key of keys) {
199
+ computeChangedPaths(prevObj[key], nextObj[key], maxDepth, maxArrayLength, path ? `${path}.${key}` : key, depth + 1, output);
200
+ }
201
+ return output;
202
+ }
203
+ function sanitizeState(value, options, depth = 0, seen = new WeakSet()) {
204
+ const {
205
+ maxDepth,
206
+ maxArrayLength,
207
+ maxStringLength
208
+ } = options;
209
+ if (value === null || value === undefined) return value;
210
+ if (typeof value === 'string') {
211
+ if (value.length > maxStringLength) {
212
+ return `${value.slice(0, maxStringLength)}…`;
213
+ }
214
+ return value;
215
+ }
216
+ if (typeof value === 'number' || typeof value === 'boolean') return value;
217
+ if (typeof value === 'bigint') return `${value.toString()}n`;
218
+ if (typeof value === 'symbol') return String(value);
219
+ if (typeof value === 'function') return '[Function]';
220
+ if (depth >= maxDepth) return '[MaxDepth]';
221
+ if (value instanceof Date) return value.toISOString();
222
+ if (value instanceof RegExp) return value.toString();
223
+ if (value instanceof Error) {
224
+ return {
225
+ name: value.name,
226
+ message: value.message,
227
+ stack: value.stack
228
+ };
229
+ }
230
+ if (typeof value === 'object') {
231
+ const obj = value;
232
+ if (seen.has(obj)) return '[Circular]';
233
+ seen.add(obj);
234
+ if (value instanceof Map) {
235
+ const entries = Array.from(value.entries()).slice(0, maxArrayLength);
236
+ return entries.map(([k, v]) => [sanitizeState(k, options, depth + 1, seen), sanitizeState(v, options, depth + 1, seen)]);
237
+ }
238
+ if (value instanceof Set) {
239
+ const values = Array.from(value.values()).slice(0, maxArrayLength);
240
+ return values.map(v => sanitizeState(v, options, depth + 1, seen));
241
+ }
242
+ if (Array.isArray(value)) {
243
+ const list = value.slice(0, maxArrayLength);
244
+ return list.map(item => sanitizeState(item, options, depth + 1, seen));
245
+ }
246
+ const result = {};
247
+ for (const [key, val] of Object.entries(obj)) {
248
+ result[key] = sanitizeState(val, options, depth + 1, seen);
249
+ }
250
+ return result;
251
+ }
252
+ return value;
253
+ }
133
254
  function devTools(config = {}) {
134
255
  const {
135
256
  enabled = true,
136
257
  treeName = 'SignalTree',
137
258
  name,
138
259
  enableBrowserDevTools = true,
260
+ enableTimeTravel = true,
139
261
  enableLogging = true,
140
- performanceThreshold = 16
262
+ performanceThreshold = 16,
263
+ includePaths,
264
+ excludePaths,
265
+ formatPath,
266
+ rateLimitMs,
267
+ maxSendsPerSecond,
268
+ maxDepth = 10,
269
+ maxArrayLength = 50,
270
+ maxStringLength = 2000,
271
+ serialize
141
272
  } = config;
142
273
  const displayName = name ?? treeName;
274
+ const pathInclude = toArray(includePaths);
275
+ const pathExclude = toArray(excludePaths);
276
+ const sendRateLimitMs = maxSendsPerSecond && maxSendsPerSecond > 0 ? Math.ceil(1000 / maxSendsPerSecond) : rateLimitMs ?? 0;
277
+ const formatPathFn = formatPath ?? defaultFormatPath;
143
278
  return tree => {
144
279
  if (!enabled) {
145
280
  const noopMethods = {
@@ -152,9 +287,227 @@ function devTools(config = {}) {
152
287
  const logger = enableLogging ? createCompositionLogger() : createNoopLogger();
153
288
  const metrics = createModularMetrics();
154
289
  const compositionHistory = [];
290
+ const compositionChain = [];
291
+ const trackComposition = modules => {
292
+ compositionHistory.push({
293
+ timestamp: new Date(),
294
+ chain: [...modules]
295
+ });
296
+ metrics.updateMetrics({
297
+ compositionChain: modules
298
+ });
299
+ logger.logComposition(modules, 'with');
300
+ };
155
301
  const activeProfiles = new Map();
156
302
  let browserDevTools = null;
303
+ let isConnected = false;
304
+ let isApplyingExternalState = false;
305
+ let unsubscribeDevTools = null;
306
+ let unsubscribeNotifier = null;
307
+ let unsubscribeFlush = null;
308
+ let pendingPaths = [];
309
+ let effectRef = null;
310
+ let effectPrimed = false;
311
+ let sendScheduled = false;
312
+ let pendingAction = null;
313
+ let pendingExplicitAction = false;
314
+ let pendingSource;
315
+ let pendingDuration;
316
+ let lastSnapshot = undefined;
317
+ let lastSendAt = 0;
318
+ let sendTimer = null;
319
+ const isPathAllowed = path => {
320
+ if (pathInclude.length > 0) {
321
+ const matched = pathInclude.some(pattern => matchesPattern(pattern, path));
322
+ if (!matched) return false;
323
+ }
324
+ if (pathExclude.length > 0) {
325
+ const blocked = pathExclude.some(pattern => matchesPattern(pattern, path));
326
+ if (blocked) return false;
327
+ }
328
+ return true;
329
+ };
330
+ const readSnapshot = () => {
331
+ try {
332
+ if ('$' in tree) {
333
+ return snapshotState(tree.$);
334
+ }
335
+ } catch {}
336
+ return originalTreeCall();
337
+ };
338
+ const buildSerializedState = rawState => {
339
+ if (serialize) {
340
+ try {
341
+ return serialize(rawState);
342
+ } catch {}
343
+ }
344
+ return sanitizeState(rawState, {
345
+ maxDepth,
346
+ maxArrayLength,
347
+ maxStringLength
348
+ });
349
+ };
350
+ const buildAction = (type, payload, meta) => ({
351
+ type,
352
+ ...(payload !== undefined && {
353
+ payload
354
+ }),
355
+ ...(meta && {
356
+ meta: meta
357
+ })
358
+ });
359
+ const flushSend = () => {
360
+ sendScheduled = false;
361
+ if (!browserDevTools || isApplyingExternalState) return;
362
+ const rawSnapshot = readSnapshot();
363
+ const currentSnapshot = rawSnapshot ?? {};
364
+ const sanitized = buildSerializedState(currentSnapshot);
365
+ const defaultPaths = lastSnapshot === undefined ? [] : computeChangedPaths(lastSnapshot, currentSnapshot, maxDepth, maxArrayLength);
366
+ const mergedPaths = Array.from(new Set([...pendingPaths, ...defaultPaths.filter(path => path && isPathAllowed(path))]));
367
+ const formattedPaths = mergedPaths.map(path => formatPathFn(path));
368
+ if (pathInclude.length > 0 && formattedPaths.length === 0 && !pendingExplicitAction) {
369
+ pendingAction = null;
370
+ pendingExplicitAction = false;
371
+ pendingSource = undefined;
372
+ pendingDuration = undefined;
373
+ pendingPaths = [];
374
+ lastSnapshot = currentSnapshot;
375
+ return;
376
+ }
377
+ const effectiveAction = pendingExplicitAction ? pendingAction : formattedPaths.length === 1 ? buildAction(`SignalTree/${formattedPaths[0]}`, formattedPaths[0]) : formattedPaths.length > 1 ? buildAction('SignalTree/batch', formattedPaths) : buildAction('SignalTree/update');
378
+ const actionMeta = {
379
+ timestamp: Date.now(),
380
+ ...(pendingSource && {
381
+ source: pendingSource
382
+ }),
383
+ ...(pendingDuration !== undefined && {
384
+ duration: pendingDuration,
385
+ slow: pendingDuration > performanceThreshold
386
+ }),
387
+ ...(formattedPaths.length > 0 && {
388
+ paths: formattedPaths
389
+ })
390
+ };
391
+ const actionToSend = buildAction(effectiveAction?.type ?? 'SignalTree/update', effectiveAction?.payload, actionMeta);
392
+ try {
393
+ browserDevTools.send(actionToSend, sanitized);
394
+ } catch {} finally {
395
+ pendingAction = null;
396
+ pendingExplicitAction = false;
397
+ pendingSource = undefined;
398
+ pendingDuration = undefined;
399
+ pendingPaths = [];
400
+ lastSnapshot = currentSnapshot;
401
+ lastSendAt = Date.now();
402
+ }
403
+ };
404
+ const scheduleSend = (action, meta) => {
405
+ if (isApplyingExternalState) return;
406
+ if (action !== undefined) {
407
+ pendingAction = action;
408
+ pendingExplicitAction = true;
409
+ }
410
+ if (meta?.source) {
411
+ if (!pendingSource) {
412
+ pendingSource = meta.source;
413
+ } else if (pendingSource !== meta.source) {
414
+ pendingSource = 'mixed';
415
+ }
416
+ }
417
+ if (meta?.duration !== undefined) {
418
+ pendingDuration = pendingDuration === undefined ? meta.duration : Math.max(pendingDuration, meta.duration);
419
+ }
420
+ if (!browserDevTools) return;
421
+ if (sendScheduled) return;
422
+ sendScheduled = true;
423
+ queueMicrotask(() => {
424
+ if (!browserDevTools) {
425
+ sendScheduled = false;
426
+ return;
427
+ }
428
+ const now = Date.now();
429
+ const waitMs = Math.max(0, sendRateLimitMs - (now - lastSendAt));
430
+ if (waitMs > 0) {
431
+ if (sendTimer) return;
432
+ sendTimer = setTimeout(() => {
433
+ sendTimer = null;
434
+ flushSend();
435
+ }, waitMs);
436
+ return;
437
+ }
438
+ flushSend();
439
+ });
440
+ };
441
+ const parseDevToolsState = state => {
442
+ if (typeof state === 'string') {
443
+ try {
444
+ return JSON.parse(state);
445
+ } catch {
446
+ return undefined;
447
+ }
448
+ }
449
+ return state;
450
+ };
451
+ const applyExternalState = state => {
452
+ if (state === undefined || state === null) return;
453
+ isApplyingExternalState = true;
454
+ try {
455
+ if ('$' in tree) {
456
+ applyState(tree.$, state);
457
+ } else {
458
+ originalTreeCall(state);
459
+ }
460
+ } finally {
461
+ isApplyingExternalState = false;
462
+ lastSnapshot = readSnapshot();
463
+ pendingPaths = [];
464
+ }
465
+ };
466
+ const handleDevToolsMessage = message => {
467
+ if (!enableTimeTravel) return;
468
+ if (!message || typeof message !== 'object') return;
469
+ const msg = message;
470
+ if (msg.type !== 'DISPATCH' || !msg.payload?.type) return;
471
+ const actionType = msg.payload.type;
472
+ if (actionType === 'JUMP_TO_STATE' || actionType === 'JUMP_TO_ACTION') {
473
+ const nextState = parseDevToolsState(msg.state);
474
+ applyExternalState(nextState);
475
+ return;
476
+ }
477
+ if (actionType === 'ROLLBACK') {
478
+ const nextState = parseDevToolsState(msg.state);
479
+ applyExternalState(nextState);
480
+ if (browserDevTools) {
481
+ const rawSnapshot = readSnapshot();
482
+ const sanitized = buildSerializedState(rawSnapshot);
483
+ browserDevTools.send('@@INIT', sanitized);
484
+ }
485
+ return;
486
+ }
487
+ if (actionType === 'COMMIT') {
488
+ if (browserDevTools) {
489
+ const rawSnapshot = readSnapshot();
490
+ const sanitized = buildSerializedState(rawSnapshot);
491
+ browserDevTools.send('@@INIT', sanitized);
492
+ }
493
+ return;
494
+ }
495
+ if (actionType === 'IMPORT_STATE') {
496
+ const lifted = msg.payload.nextLiftedState;
497
+ const computedStates = lifted?.computedStates ?? [];
498
+ const index = lifted?.currentStateIndex ?? computedStates.length - 1;
499
+ const entry = computedStates[index];
500
+ const nextState = parseDevToolsState(entry?.state);
501
+ applyExternalState(nextState);
502
+ if (browserDevTools) {
503
+ const rawSnapshot = readSnapshot();
504
+ const sanitized = buildSerializedState(rawSnapshot);
505
+ browserDevTools.send('@@INIT', sanitized);
506
+ }
507
+ }
508
+ };
157
509
  const initBrowserDevTools = () => {
510
+ if (isConnected) return;
158
511
  if (!enableBrowserDevTools || typeof window === 'undefined' || !('__REDUX_DEVTOOLS_EXTENSION__' in window)) {
159
512
  return;
160
513
  }
@@ -169,9 +522,20 @@ function devTools(config = {}) {
169
522
  }
170
523
  });
171
524
  browserDevTools = {
172
- send: connection.send
525
+ send: connection.send,
526
+ subscribe: connection.subscribe
173
527
  };
174
- browserDevTools.send('@@INIT', tree());
528
+ if (browserDevTools.subscribe && !unsubscribeDevTools) {
529
+ browserDevTools.subscribe(handleDevToolsMessage);
530
+ unsubscribeDevTools = () => {
531
+ browserDevTools?.subscribe?.(() => void 0);
532
+ };
533
+ }
534
+ const rawSnapshot = readSnapshot();
535
+ const sanitized = buildSerializedState(rawSnapshot);
536
+ browserDevTools.send('@@INIT', sanitized);
537
+ lastSnapshot = rawSnapshot;
538
+ isConnected = true;
175
539
  console.log(`🔗 Connected to Redux DevTools as "${displayName}"`);
176
540
  } catch (e) {
177
541
  console.warn('[SignalTree] Failed to connect to Redux DevTools:', e);
@@ -193,13 +557,16 @@ function devTools(config = {}) {
193
557
  }
194
558
  }
195
559
  const duration = performance.now() - startTime;
196
- const newState = originalTreeCall();
560
+ originalTreeCall();
197
561
  metrics.trackModuleUpdate('core', duration);
198
562
  if (duration > performanceThreshold) {
199
563
  logger.logPerformanceWarning('core', 'update', duration, performanceThreshold);
200
564
  }
201
565
  if (browserDevTools) {
202
- browserDevTools.send('UPDATE', newState);
566
+ scheduleSend(undefined, {
567
+ source: 'tree.update',
568
+ duration
569
+ });
203
570
  }
204
571
  return result;
205
572
  };
@@ -210,6 +577,15 @@ function devTools(config = {}) {
210
577
  if (typeof enhancer !== 'function') {
211
578
  throw new Error('Enhancer must be a function');
212
579
  }
580
+ const enhancerName = enhancer.name || 'anonymousEnhancer';
581
+ compositionChain.push(enhancerName);
582
+ trackComposition([...compositionChain]);
583
+ scheduleSend(buildAction('SignalTree/with', {
584
+ enhancer: enhancerName,
585
+ chain: [...compositionChain]
586
+ }), {
587
+ source: 'composition'
588
+ });
213
589
  return enhancer(enhancedTree);
214
590
  },
215
591
  writable: false,
@@ -234,16 +610,7 @@ function devTools(config = {}) {
234
610
  activityTracker,
235
611
  logger,
236
612
  metrics: metrics.signal,
237
- trackComposition: modules => {
238
- compositionHistory.push({
239
- timestamp: new Date(),
240
- chain: [...modules]
241
- });
242
- metrics.updateMetrics({
243
- compositionChain: modules
244
- });
245
- logger.logComposition(modules, 'with');
246
- },
613
+ trackComposition,
247
614
  startModuleProfiling: module => {
248
615
  const profileId = `${module}_${Date.now()}`;
249
616
  activeProfiles.set(profileId, {
@@ -262,8 +629,14 @@ function devTools(config = {}) {
262
629
  }
263
630
  },
264
631
  connectDevTools: name => {
632
+ if (!browserDevTools || !isConnected) {
633
+ initBrowserDevTools();
634
+ }
265
635
  if (browserDevTools) {
266
- browserDevTools.send('@@INIT', originalTreeCall());
636
+ const rawSnapshot = readSnapshot();
637
+ const sanitized = buildSerializedState(rawSnapshot);
638
+ browserDevTools.send('@@INIT', sanitized);
639
+ lastSnapshot = rawSnapshot;
267
640
  console.log(`🔗 Connected to Redux DevTools as "${name}"`);
268
641
  }
269
642
  },
@@ -280,9 +653,65 @@ function devTools(config = {}) {
280
653
  },
281
654
  disconnectDevTools() {
282
655
  browserDevTools = null;
656
+ isConnected = false;
657
+ if (unsubscribeNotifier) {
658
+ unsubscribeNotifier();
659
+ unsubscribeNotifier = null;
660
+ }
661
+ if (unsubscribeFlush) {
662
+ unsubscribeFlush();
663
+ unsubscribeFlush = null;
664
+ }
665
+ if (unsubscribeDevTools) {
666
+ unsubscribeDevTools();
667
+ unsubscribeDevTools = null;
668
+ }
669
+ if (effectRef) {
670
+ effectRef.destroy();
671
+ effectRef = null;
672
+ }
673
+ if (sendTimer) {
674
+ clearTimeout(sendTimer);
675
+ sendTimer = null;
676
+ }
677
+ pendingPaths = [];
678
+ sendScheduled = false;
679
+ pendingAction = null;
680
+ pendingExplicitAction = false;
681
+ pendingSource = undefined;
682
+ pendingDuration = undefined;
683
+ lastSnapshot = undefined;
283
684
  }
284
685
  };
285
686
  enhancedTree['__devTools'] = devToolsInterface;
687
+ try {
688
+ initBrowserDevTools();
689
+ const notifier = getPathNotifier();
690
+ unsubscribeNotifier = notifier.subscribe('**', (_value, _prev, path) => {
691
+ if (!isPathAllowed(path)) return;
692
+ pendingPaths.push(path);
693
+ });
694
+ unsubscribeFlush = notifier.onFlush(() => {
695
+ if (!browserDevTools) {
696
+ pendingPaths = [];
697
+ return;
698
+ }
699
+ if (pendingPaths.length === 0) return;
700
+ scheduleSend(undefined, {
701
+ source: 'path-notifier'
702
+ });
703
+ });
704
+ effectRef = effect(() => {
705
+ void originalTreeCall();
706
+ if (!effectPrimed) {
707
+ effectPrimed = true;
708
+ return;
709
+ }
710
+ scheduleSend(undefined, {
711
+ source: 'signal'
712
+ });
713
+ });
714
+ } catch {}
286
715
  return Object.assign(enhancedTree, methods);
287
716
  };
288
717
  }
@@ -20,13 +20,11 @@ function createEntitySignal(config, pathNotifier, basePath) {
20
20
  mapSignal.set(map);
21
21
  }
22
22
  function createEntityNode(id, entity) {
23
- const node = () => storage.get(id);
23
+ const node = () => mapSignal().get(id);
24
24
  for (const key of Object.keys(entity)) {
25
25
  Object.defineProperty(node, key, {
26
26
  get: () => {
27
- const current = storage.get(id);
28
- const value = current?.[key];
29
- return () => value;
27
+ return () => mapSignal().get(id)?.[key];
30
28
  },
31
29
  enumerable: true,
32
30
  configurable: true
@@ -46,7 +44,8 @@ function createEntitySignal(config, pathNotifier, basePath) {
46
44
  const findCache = new WeakMap();
47
45
  const api = {
48
46
  byId(id) {
49
- const entity = storage.get(id);
47
+ const map = mapSignal();
48
+ const entity = map.get(id);
50
49
  if (!entity) return undefined;
51
50
  return getOrCreateNode(id, entity);
52
51
  },
@@ -359,9 +359,6 @@ function createBuilder(baseTree) {
359
359
  } catch {}
360
360
  }
361
361
  }
362
- for (const factory of derivedQueue) {
363
- newBuilder.derived(factory);
364
- }
365
362
  return newBuilder;
366
363
  },
367
364
  enumerable: false,
package/dist/lib/utils.js CHANGED
@@ -260,5 +260,40 @@ function unwrap(node) {
260
260
  function snapshotState(state) {
261
261
  return unwrap(state);
262
262
  }
263
+ function applyState(stateNode, snapshot) {
264
+ if (snapshot === null || snapshot === undefined) return;
265
+ if (typeof snapshot !== 'object') return;
266
+ for (const key of Object.keys(snapshot)) {
267
+ const val = snapshot[key];
268
+ const target = stateNode[key];
269
+ if (isNodeAccessor(target)) {
270
+ if (val && typeof val === 'object') {
271
+ try {
272
+ applyState(target, val);
273
+ } catch {
274
+ try {
275
+ target(val);
276
+ } catch {}
277
+ }
278
+ } else {
279
+ try {
280
+ target(val);
281
+ } catch {}
282
+ }
283
+ } else if (isSignal(target)) {
284
+ try {
285
+ target.set?.(val);
286
+ } catch {
287
+ try {
288
+ target(val);
289
+ } catch {}
290
+ }
291
+ } else {
292
+ try {
293
+ stateNode[key] = val;
294
+ } catch {}
295
+ }
296
+ }
297
+ }
263
298
 
264
- export { composeEnhancers, createLazySignalTree, isAnySignal, isBuiltInObject, isEntityMapMarker, isNodeAccessor, snapshotState, toWritableSignal, unwrap };
299
+ export { applyState, composeEnhancers, createLazySignalTree, isAnySignal, isBuiltInObject, isEntityMapMarker, isNodeAccessor, snapshotState, toWritableSignal, unwrap };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signaltree/core",
3
- "version": "7.3.0",
3
+ "version": "7.6.0",
4
4
  "description": "Reactive JSON for Angular. JSON branches, reactive leaves. No actions. No reducers. No selectors.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -246,11 +246,21 @@ export interface DevToolsConfig {
246
246
  enableBrowserDevTools?: boolean;
247
247
  enableLogging?: boolean;
248
248
  performanceThreshold?: number;
249
+ enableTimeTravel?: boolean;
249
250
  name?: string;
250
251
  treeName?: string;
251
252
  enabled?: boolean;
252
253
  logActions?: boolean;
253
254
  maxAge?: number;
255
+ rateLimitMs?: number;
256
+ maxSendsPerSecond?: number;
257
+ includePaths?: string[];
258
+ excludePaths?: string[];
259
+ formatPath?: (path: string) => string;
260
+ maxDepth?: number;
261
+ maxArrayLength?: number;
262
+ maxStringLength?: number;
263
+ serialize?: (state: unknown) => unknown;
254
264
  features?: {
255
265
  jump?: boolean;
256
266
  skip?: boolean;