@signaltree/core 8.0.2 → 9.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.
@@ -0,0 +1 @@
1
+ export { createEditSession } from './lib/edit-session.js';
@@ -1,9 +1,10 @@
1
1
  import { copyTreeProperties } from '../utils/copy-tree-properties.js';
2
+ import { ENHANCER_META } from '../../lib/types.js';
2
3
 
3
4
  function batching(config = {}) {
4
5
  const enabled = config.enabled ?? true;
5
6
  const notificationDelayMs = config.notificationDelayMs ?? 0;
6
- return tree => {
7
+ const enhancerFn = tree => {
7
8
  if (!enabled) {
8
9
  const passthrough = {
9
10
  batch: fn => fn(),
@@ -200,8 +201,24 @@ function batching(config = {}) {
200
201
  });
201
202
  });
202
203
  };
204
+ if (typeof tree.registerCleanup === 'function') {
205
+ tree.registerCleanup(() => {
206
+ if (notificationTimeoutId !== undefined) {
207
+ clearTimeout(notificationTimeoutId);
208
+ notificationTimeoutId = undefined;
209
+ }
210
+ coalescedUpdates.clear();
211
+ });
212
+ }
203
213
  return enhancedTree;
204
214
  };
215
+ const meta = {
216
+ name: 'batching',
217
+ provides: ['batching']
218
+ };
219
+ enhancerFn.metadata = meta;
220
+ enhancerFn[ENHANCER_META] = meta;
221
+ return enhancerFn;
205
222
  }
206
223
  function highPerformanceBatching() {
207
224
  return batching({
@@ -209,22 +226,8 @@ function highPerformanceBatching() {
209
226
  notificationDelayMs: 0
210
227
  });
211
228
  }
212
- function batchingWithConfig(config = {}) {
213
- return batching(config);
214
- }
215
- function flushBatchedUpdates() {
216
- console.warn('[SignalTree] flushBatchedUpdates() is deprecated. Use tree.flushNotifications() instead.');
217
- }
218
- function hasPendingUpdates() {
219
- console.warn('[SignalTree] hasPendingUpdates() is deprecated. Use tree.hasPendingNotifications() instead.');
220
- return false;
221
- }
222
- function getBatchQueueSize() {
223
- console.warn('[SignalTree] getBatchQueueSize() is deprecated. Signal writes are now synchronous.');
224
- return 0;
225
- }
226
229
  Object.assign((config = {}) => batching(config), {
227
230
  highPerformance: highPerformanceBatching
228
231
  });
229
232
 
230
- export { batching, batchingWithConfig, flushBatchedUpdates, getBatchQueueSize, hasPendingUpdates, highPerformanceBatching };
233
+ export { batching, highPerformanceBatching };
@@ -2,6 +2,7 @@ import { signal } from '@angular/core';
2
2
  import { copyTreeProperties } from '../utils/copy-tree-properties.js';
3
3
  import { applyState, snapshotState } from '../../lib/utils.js';
4
4
  import { getPathNotifier } from '../../lib/path-notifier.js';
5
+ import { ENHANCER_META } from '../../lib/types.js';
5
6
 
6
7
  function createActivityTracker(options) {
7
8
  const modules = new Map();
@@ -698,7 +699,7 @@ function devTools(config = {}) {
698
699
  const pathExclude = toArray(excludePaths);
699
700
  const sendRateLimitMs = maxSendsPerSecond && maxSendsPerSecond > 0 ? Math.ceil(1000 / maxSendsPerSecond) : rateLimitMs ?? 0;
700
701
  const formatPathFn = formatPath ?? defaultFormatPath;
701
- return tree => {
702
+ const enhancerFn = tree => {
702
703
  if (!enabled) {
703
704
  const noopMethods = {
704
705
  connectDevTools() {},
@@ -1306,8 +1307,20 @@ function devTools(config = {}) {
1306
1307
  if (enableBrowserDevTools) {
1307
1308
  initBrowserDevTools();
1308
1309
  }
1310
+ if (typeof tree.registerCleanup === 'function') {
1311
+ tree.registerCleanup(() => {
1312
+ result.disconnectDevTools();
1313
+ });
1314
+ }
1309
1315
  return result;
1310
1316
  };
1317
+ const meta = {
1318
+ name: 'devTools',
1319
+ provides: ['devTools']
1320
+ };
1321
+ enhancerFn.metadata = meta;
1322
+ enhancerFn[ENHANCER_META] = meta;
1323
+ return enhancerFn;
1311
1324
  }
1312
1325
  function enableDevTools(treeName = 'SignalTree') {
1313
1326
  return devTools({
@@ -1,10 +1,11 @@
1
1
  import { effect, untracked } from '@angular/core';
2
+ import { ENHANCER_META } from '../../lib/types.js';
2
3
 
3
4
  function effects(config = {}) {
4
5
  const {
5
6
  enabled = true
6
7
  } = config;
7
- return tree => {
8
+ const enhancerFn = tree => {
8
9
  const cleanupFns = [];
9
10
  const methods = {
10
11
  effect(effectFn) {
@@ -53,6 +54,13 @@ function effects(config = {}) {
53
54
  };
54
55
  return Object.assign(tree, methods);
55
56
  };
57
+ const meta = {
58
+ name: 'effects',
59
+ provides: ['effects']
60
+ };
61
+ enhancerFn.metadata = meta;
62
+ enhancerFn[ENHANCER_META] = meta;
63
+ return enhancerFn;
56
64
  }
57
65
  function enableEffects() {
58
66
  return effects({
@@ -1,7 +1,8 @@
1
1
  import { computed } from '@angular/core';
2
2
  import { isNodeAccessor } from '../../lib/utils.js';
3
- import { deepEqual } from '../../shared/lib/deep-equal.js';
3
+ import { ENHANCER_META } from '../../lib/types.js';
4
4
  import { LRUCache } from '../../shared/lib/lru-cache.js';
5
+ import { deepEqual } from '../../shared/lib/deep-equal.js';
5
6
 
6
7
  function isDevMode() {
7
8
  if (typeof __DEV__ !== 'undefined') {
@@ -10,7 +11,6 @@ function isDevMode() {
10
11
  return false;
11
12
  }
12
13
  const MAX_CACHE_SIZE = 1000;
13
- const DEFAULT_TTL = 5 * 60 * 1000;
14
14
  const memoizationCache = new Map();
15
15
  function createMemoCacheStore(maxSize, enableLRU) {
16
16
  if (enableLRU) {
@@ -90,13 +90,6 @@ function clearCleanupInterval(tree) {
90
90
  } catch {}
91
91
  setCleanupInterval(tree);
92
92
  }
93
- function resetMemoizationCaches() {
94
- memoizationCache.forEach((cache, tree) => {
95
- cache.clear();
96
- clearCleanupInterval(tree);
97
- });
98
- memoizationCache.clear();
99
- }
100
93
  function shallowEqual(a, b) {
101
94
  if (a === b) return true;
102
95
  if (a == null || b == null) return false;
@@ -136,59 +129,6 @@ function getEqualityFn(strategy) {
136
129
  return deepEqual;
137
130
  }
138
131
  }
139
- function memoize(fn, keyFn, config = {}) {
140
- const maxSize = config.maxCacheSize ?? MAX_CACHE_SIZE;
141
- const ttl = config.ttl ?? DEFAULT_TTL;
142
- const equality = getEqualityFn(config.equality ?? 'shallow');
143
- const enableLRU = config.enableLRU ?? false;
144
- const cache = createMemoCacheStore(maxSize, enableLRU);
145
- const cleanExpiredEntries = () => {
146
- if (!ttl) return;
147
- const now = Date.now();
148
- cache.forEach((entry, key) => {
149
- if (entry.timestamp && now - entry.timestamp > ttl) {
150
- cache.delete(key);
151
- }
152
- });
153
- };
154
- return (...args) => {
155
- if (ttl && Math.random() < 0.01) {
156
- cleanExpiredEntries();
157
- }
158
- const key = keyFn ? keyFn(...args) : generateCacheKey(fn, args);
159
- const cached = cache.get(key);
160
- if (cached && (keyFn || equality(cached.deps, args))) {
161
- if (enableLRU) {
162
- cached.hitCount += 1;
163
- }
164
- return cached.value;
165
- }
166
- const result = fn(...args);
167
- cache.set(key, {
168
- value: result,
169
- deps: args,
170
- timestamp: Date.now(),
171
- hitCount: 1
172
- });
173
- return result;
174
- };
175
- }
176
- function memoizeShallow(fn, keyFn) {
177
- return memoize(fn, keyFn, {
178
- equality: 'shallow',
179
- enableLRU: false,
180
- ttl: undefined,
181
- maxCacheSize: 100
182
- });
183
- }
184
- function memoizeReference(fn, keyFn) {
185
- return memoize(fn, keyFn, {
186
- equality: 'reference',
187
- enableLRU: false,
188
- ttl: undefined,
189
- maxCacheSize: 50
190
- });
191
- }
192
132
  const MEMOIZATION_PRESETS = {
193
133
  selector: {
194
134
  equality: 'reference',
@@ -364,57 +304,22 @@ function memoization(config = {}) {
364
304
  const intervalId = setInterval(cleanup, ttl);
365
305
  setCleanupInterval(tree, intervalId);
366
306
  }
307
+ if (typeof tree.registerCleanup === 'function') {
308
+ tree.registerCleanup(() => {
309
+ clearCleanupInterval(tree);
310
+ cache.clear();
311
+ memoizationCache.delete(tree);
312
+ });
313
+ }
367
314
  return tree;
368
315
  };
369
- return enhancer;
370
- }
371
- function highPerformanceMemoization() {
372
- return memoization({
373
- enabled: true,
374
- maxCacheSize: 10000,
375
- ttl: 300000,
376
- equality: 'shallow',
377
- enableLRU: true
378
- });
379
- }
380
- function lightweightMemoization() {
381
- return memoization({
382
- enabled: true,
383
- maxCacheSize: 100,
384
- ttl: undefined,
385
- equality: 'reference',
386
- enableLRU: false
387
- });
388
- }
389
- function shallowMemoization() {
390
- return memoization({
391
- enabled: true,
392
- maxCacheSize: 1000,
393
- ttl: 60000,
394
- equality: 'shallow',
395
- enableLRU: true
396
- });
397
- }
398
- function clearAllCaches() {
399
- resetMemoizationCaches();
400
- }
401
- function getGlobalCacheStats() {
402
- let totalSize = 0;
403
- let totalHits = 0;
404
- let treeCount = 0;
405
- memoizationCache.forEach(cache => {
406
- treeCount++;
407
- totalSize += cache.size();
408
- cache.forEach(entry => {
409
- totalHits += entry.hitCount || 0;
410
- });
411
- });
412
- return {
413
- treeCount,
414
- totalSize,
415
- totalHits,
416
- averageCacheSize: treeCount > 0 ? totalSize / treeCount : 0
316
+ const meta = {
317
+ name: 'memoization',
318
+ provides: ['memoization']
417
319
  };
320
+ enhancer.metadata = meta;
321
+ enhancer[ENHANCER_META] = meta;
322
+ return enhancer;
418
323
  }
419
324
 
420
- export { MEMOIZATION_PRESETS, clearAllCaches, computedMemoization, deepStateMemoization, getGlobalCacheStats, highFrequencyMemoization, highPerformanceMemoization, lightweightMemoization, memoization, memoize, memoizeReference, memoizeShallow, selectorMemoization, shallowMemoization };
325
+ export { MEMOIZATION_PRESETS, computedMemoization, deepStateMemoization, highFrequencyMemoization, memoization, selectorMemoization };
@@ -1,4 +1,5 @@
1
1
  import { isSignal } from '@angular/core';
2
+ import { ENHANCER_META } from '../../lib/types.js';
2
3
  import { TYPE_MARKERS } from './constants.js';
3
4
 
4
5
  const DEFAULT_CONFIG = {
@@ -181,7 +182,7 @@ function resolveCircularReferences(obj, circularPaths) {
181
182
  }
182
183
  }
183
184
  function serialization(defaultConfig = {}) {
184
- return tree => {
185
+ const enhancerFn = tree => {
185
186
  const enhanced = tree;
186
187
  enhanced.toJSON = () => {
187
188
  return tree();
@@ -455,15 +456,15 @@ function serialization(defaultConfig = {}) {
455
456
  };
456
457
  return enhanced;
457
458
  };
459
+ const meta = {
460
+ name: 'serialization',
461
+ provides: ['serialization']
462
+ };
463
+ enhancerFn.metadata = meta;
464
+ enhancerFn[ENHANCER_META] = meta;
465
+ return enhancerFn;
458
466
  }
459
467
  Object.assign((defaultConfig = {}) => serialization(defaultConfig), {});
460
- function enableSerialization() {
461
- return serialization({
462
- includeMetadata: true,
463
- preserveTypes: true,
464
- handleCircular: true
465
- });
466
- }
467
468
  function persistence(config) {
468
469
  const {
469
470
  key,
@@ -477,7 +478,7 @@ function persistence(config) {
477
478
  throw new Error('No storage adapter available. Provide a storage adapter in the config.');
478
479
  }
479
480
  const storageAdapter = storage;
480
- return tree => {
481
+ const persistenceFn = tree => {
481
482
  const serializable = serialization(serializationConfig)(tree);
482
483
  const enhanced = serializable;
483
484
  let lastCacheKey = null;
@@ -553,8 +554,9 @@ function persistence(config) {
553
554
  });
554
555
  }, debounceMs);
555
556
  };
557
+ let unsubscribeAutoSave = null;
556
558
  try {
557
- tree.subscribe(() => {
559
+ unsubscribeAutoSave = tree.subscribe(() => {
558
560
  const currentState = JSON.stringify(tree());
559
561
  if (currentState !== previousState) {
560
562
  previousState = currentState;
@@ -575,6 +577,10 @@ function persistence(config) {
575
577
  }
576
578
  enhanced.__flushAutoSave = () => {
577
579
  pollingActive = false;
580
+ if (unsubscribeAutoSave) {
581
+ unsubscribeAutoSave();
582
+ unsubscribeAutoSave = null;
583
+ }
578
584
  if (saveTimeout) {
579
585
  clearTimeout(saveTimeout);
580
586
  saveTimeout = undefined;
@@ -582,9 +588,30 @@ function persistence(config) {
582
588
  }
583
589
  return Promise.resolve();
584
590
  };
591
+ if (typeof tree.registerCleanup === 'function') {
592
+ tree.registerCleanup(() => {
593
+ pollingActive = false;
594
+ if (unsubscribeAutoSave) {
595
+ unsubscribeAutoSave();
596
+ unsubscribeAutoSave = null;
597
+ }
598
+ if (saveTimeout) {
599
+ clearTimeout(saveTimeout);
600
+ saveTimeout = undefined;
601
+ }
602
+ });
603
+ }
585
604
  }
586
605
  return enhanced;
587
606
  };
607
+ const meta = {
608
+ name: 'persistence',
609
+ provides: ['persistence', 'serialization'],
610
+ requires: []
611
+ };
612
+ persistenceFn.metadata = meta;
613
+ persistenceFn[ENHANCER_META] = meta;
614
+ return persistenceFn;
588
615
  }
589
616
  Object.assign(cfg => persistence(cfg), {});
590
617
  function createStorageAdapter(getItem, setItem, removeItem) {
@@ -646,11 +673,5 @@ function createIndexedDBAdapter(dbName = 'SignalTreeDB', storeName = 'states') {
646
673
  }
647
674
  };
648
675
  }
649
- function applySerialization(tree) {
650
- return serialization()(tree);
651
- }
652
- function applyPersistence(tree, cfg) {
653
- return persistence(cfg)(tree);
654
- }
655
676
 
656
- export { applyPersistence, applySerialization, createIndexedDBAdapter, createStorageAdapter, enableSerialization, persistence, serialization };
677
+ export { createIndexedDBAdapter, createStorageAdapter, persistence, serialization };
@@ -1,5 +1,6 @@
1
1
  import { snapshotState } from '../../lib/utils.js';
2
2
  import { deepEqual, deepClone } from './utils.js';
3
+ import { ENHANCER_META } from '../../lib/types.js';
3
4
 
4
5
  class TimeTravelManager {
5
6
  tree;
@@ -130,7 +131,7 @@ function timeTravel(config = {}) {
130
131
  const {
131
132
  enabled = true
132
133
  } = config;
133
- return tree => {
134
+ const enhancerFn = tree => {
134
135
  if (!enabled) {
135
136
  const noopMethods = {
136
137
  undo() {},
@@ -255,13 +256,20 @@ function timeTravel(config = {}) {
255
256
  enhancedTree['canRedo'] = () => timeTravelManager.canRedo();
256
257
  enhancedTree['getCurrentIndex'] = () => timeTravelManager.getCurrentIndex();
257
258
  enhancedTree['__timeTravel'] = timeTravelManager;
259
+ if (typeof tree.registerCleanup === 'function') {
260
+ tree.registerCleanup(() => {
261
+ timeTravelManager.resetHistory();
262
+ });
263
+ }
258
264
  return enhancedTree;
259
265
  };
260
- }
261
- function enableTimeTravel() {
262
- return timeTravel({
263
- enabled: true
264
- });
266
+ const meta = {
267
+ name: 'timeTravel',
268
+ provides: ['timeTravel']
269
+ };
270
+ enhancerFn.metadata = meta;
271
+ enhancerFn[ENHANCER_META] = meta;
272
+ return enhancerFn;
265
273
  }
266
274
  function timeTravelHistory(maxHistorySize) {
267
275
  return timeTravel({
@@ -280,4 +288,4 @@ Object.assign((config = {}) => timeTravel(config), {
280
288
  history: timeTravelHistory
281
289
  });
282
290
 
283
- export { enableTimeTravel, timeTravel, timeTravelHistory };
291
+ export { timeTravel, timeTravelHistory };
package/dist/index.js CHANGED
@@ -6,22 +6,16 @@ export { LoadingState, isStatusMarker, status } from './lib/markers/status.js';
6
6
  export { clearStoragePrefix, createStorageKeys, isStoredMarker, stored } from './lib/markers/stored.js';
7
7
  export { FORM_MARKER, createFormSignal, form, isFormMarker, validators } from './lib/markers/form.js';
8
8
  export { registerMarkerProcessor } from './lib/internals/materialize-markers.js';
9
- export { composeEnhancers, createLazySignalTree, isAnySignal, isNodeAccessor, toWritableSignal } from './lib/utils.js';
10
- export { createEditSession } from './lib/edit-session.js';
9
+ export { composeEnhancers, isAnySignal, isNodeAccessor, toWritableSignal } from './lib/utils.js';
11
10
  export { getPathNotifier } from './lib/path-notifier.js';
12
- export { SecurityPresets, SecurityValidator } from './lib/security/security-validator.js';
13
11
  export { createEnhancer, resolveEnhancerOrder } from './enhancers/index.js';
14
- export { batching, batchingWithConfig, flushBatchedUpdates, getBatchQueueSize, hasPendingUpdates, highPerformanceBatching } from './enhancers/batching/batching.js';
15
- export { clearAllCaches, computedMemoization, deepStateMemoization, getGlobalCacheStats, highFrequencyMemoization, highPerformanceMemoization, lightweightMemoization, memoization, memoize, memoizeReference, memoizeShallow, selectorMemoization, shallowMemoization } from './enhancers/memoization/memoization.js';
16
- export { enableTimeTravel, timeTravel } from './enhancers/time-travel/time-travel.js';
17
- export { enableEntities, entities, highPerformanceEntities } from './enhancers/entities/entities.js';
18
- export { applyPersistence, applySerialization, createIndexedDBAdapter, createStorageAdapter, enableSerialization, persistence, serialization } from './enhancers/serialization/serialization.js';
19
- export { devTools, enableDevTools, fullDevTools, productionDevTools } from './enhancers/devtools/devtools.js';
20
- export { createAsyncOperation, trackAsync } from './lib/async-helpers.js';
21
- export { TREE_PRESETS, combinePresets, createPresetConfig, getAvailablePresets, validatePreset } from './enhancers/presets/lib/presets.js';
22
- export { SIGNAL_TREE_CONSTANTS, SIGNAL_TREE_MESSAGES } from './lib/constants.js';
12
+ export { batching } from './enhancers/batching/batching.js';
13
+ export { memoization } from './enhancers/memoization/memoization.js';
14
+ export { timeTravel } from './enhancers/time-travel/time-travel.js';
15
+ export { persistence, serialization } from './enhancers/serialization/serialization.js';
16
+ export { devTools } from './enhancers/devtools/devtools.js';
17
+ export { SIGNAL_TREE_CONSTANTS, SIGNAL_TREE_MESSAGES, isDev } from './lib/constants.js';
23
18
  export { entityMap } from './lib/markers/entity-map.js';
24
19
  export { deepEqual, deepEqual as equal } from './shared/lib/deep-equal.js';
25
20
  export { parsePath } from './shared/lib/parse-path.js';
26
21
  export { isBuiltInObject } from './shared/lib/is-built-in-object.js';
27
- export { createDevTree } from './lib/presets.js';
@@ -51,6 +51,7 @@ const PROD_MESSAGES = (() => {
51
51
  })();
52
52
  const _isProdByEnv = Boolean(typeof globalThis === 'object' && globalThis !== null && 'process' in globalThis && typeof globalThis.process === 'object' && 'env' in globalThis.process && globalThis.process.env.NODE_ENV === 'production');
53
53
  const _isDev = typeof ngDevMode !== 'undefined' ? Boolean(ngDevMode) : !_isProdByEnv;
54
+ const isDev = _isDev;
54
55
  const SIGNAL_TREE_MESSAGES = Object.freeze(_isDev ? DEV_MESSAGES : PROD_MESSAGES);
55
56
 
56
- export { SIGNAL_TREE_CONSTANTS, SIGNAL_TREE_MESSAGES };
57
+ export { SIGNAL_TREE_CONSTANTS, SIGNAL_TREE_MESSAGES, isDev };
@@ -16,5 +16,15 @@ function createDevTree(initialState, config = {}) {
16
16
  const enhanced = base.with(effects()).with(batching(config.batching)).with(memoization(config.memoization)).with(timeTravel(config.timeTravel)).with(devTools(config.devTools));
17
17
  return enhanced;
18
18
  }
19
+ function createProdTree(initialState, config = {}) {
20
+ const base = signalTree(initialState, config);
21
+ const enhanced = base.with(effects()).with(batching(config.batching)).with(memoization(config.memoization));
22
+ return enhanced;
23
+ }
24
+ function createMinimalTree(initialState, config = {}) {
25
+ const base = signalTree(initialState, config);
26
+ const enhanced = base.with(effects());
27
+ return enhanced;
28
+ }
19
29
 
20
- export { createDevTree };
30
+ export { createDevTree, createMinimalTree, createProdTree };
@@ -9,6 +9,7 @@ import { SignalMemoryManager } from './memory/memory-manager.js';
9
9
  import { getPathNotifier } from './path-notifier.js';
10
10
  import { SecurityValidator } from './security/security-validator.js';
11
11
  import { createLazySignalTree, unwrap } from './utils.js';
12
+ import { ENHANCER_META } from './types.js';
12
13
  import { deepEqual } from '../shared/lib/deep-equal.js';
13
14
  import { isBuiltInObject } from '../shared/lib/is-built-in-object.js';
14
15
 
@@ -228,6 +229,9 @@ function create(initialState, config) {
228
229
  }
229
230
  };
230
231
  tree[NODE_ACCESSOR_SYMBOL] = true;
232
+ const cleanupFns = [];
233
+ const destroyedSig = signal(false);
234
+ const appliedEnhancers = new Set();
231
235
  Object.defineProperty(tree, 'state', {
232
236
  value: signalState,
233
237
  enumerable: false,
@@ -243,6 +247,20 @@ function create(initialState, config) {
243
247
  if (typeof enhancer !== 'function') {
244
248
  throw new Error('Enhancer must be a function');
245
249
  }
250
+ const meta = enhancer[ENHANCER_META] ?? enhancer.metadata;
251
+ if (meta?.name) {
252
+ if (appliedEnhancers.has(meta.name)) {
253
+ throw new Error(`Enhancer "${meta.name}" has already been applied to this tree. ` + `Each enhancer can only be applied once.`);
254
+ }
255
+ if (meta.requires) {
256
+ for (const dep of meta.requires) {
257
+ if (!appliedEnhancers.has(dep)) {
258
+ throw new Error(`Enhancer "${meta.name}" requires "${dep}" to be applied first.`);
259
+ }
260
+ }
261
+ }
262
+ appliedEnhancers.add(meta.name);
263
+ }
246
264
  return enhancer(tree);
247
265
  },
248
266
  enumerable: false,
@@ -259,6 +277,14 @@ function create(initialState, config) {
259
277
  });
260
278
  Object.defineProperty(tree, 'destroy', {
261
279
  value: function () {
280
+ if (destroyedSig()) return;
281
+ destroyedSig.set(true);
282
+ for (const fn of cleanupFns) {
283
+ try {
284
+ fn();
285
+ } catch {}
286
+ }
287
+ cleanupFns.length = 0;
262
288
  if (memoryManager) {
263
289
  memoryManager.dispose();
264
290
  }
@@ -270,6 +296,22 @@ function create(initialState, config) {
270
296
  writable: true,
271
297
  configurable: true
272
298
  });
299
+ Object.defineProperty(tree, 'destroyed', {
300
+ value: destroyedSig.asReadonly(),
301
+ enumerable: false,
302
+ writable: false,
303
+ configurable: true
304
+ });
305
+ Object.defineProperty(tree, 'registerCleanup', {
306
+ value: function (fn) {
307
+ if (typeof fn === 'function') {
308
+ cleanupFns.push(fn);
309
+ }
310
+ },
311
+ enumerable: false,
312
+ writable: false,
313
+ configurable: true
314
+ });
273
315
  Object.defineProperty(tree, 'clearCache', {
274
316
  value: () => {},
275
317
  enumerable: false,
@@ -353,7 +395,7 @@ function createBuilder(baseTree) {
353
395
  const enhanced = baseTree.with(enhancer);
354
396
  const newBuilder = createBuilder(enhanced);
355
397
  for (const key of Object.keys(enhanced)) {
356
- if (key !== '$' && key !== 'state' && key !== 'with' && key !== 'bind' && key !== 'destroy' && key !== 'derived') {
398
+ if (key !== '$' && key !== 'state' && key !== 'with' && key !== 'bind' && key !== 'destroy' && key !== 'destroyed' && key !== 'registerCleanup' && key !== 'derived') {
357
399
  try {
358
400
  newBuilder[key] = enhanced[key];
359
401
  } catch {}
@@ -395,6 +437,22 @@ function createBuilder(baseTree) {
395
437
  configurable: true
396
438
  });
397
439
  }
440
+ if (baseTree.destroyed) {
441
+ Object.defineProperty(builder, 'destroyed', {
442
+ value: baseTree.destroyed,
443
+ enumerable: false,
444
+ writable: false,
445
+ configurable: true
446
+ });
447
+ }
448
+ if (typeof baseTree.registerCleanup === 'function') {
449
+ Object.defineProperty(builder, 'registerCleanup', {
450
+ value: baseTree.registerCleanup.bind(baseTree),
451
+ enumerable: false,
452
+ writable: false,
453
+ configurable: true
454
+ });
455
+ }
398
456
  Object.defineProperty(builder, 'derived', {
399
457
  value: function (factory) {
400
458
  if (isFinalized) {
@@ -0,0 +1,2 @@
1
+ export { TREE_PRESETS, combinePresets, createPresetConfig, getAvailablePresets, validatePreset } from './enhancers/presets/lib/presets.js';
2
+ export { createDevTree, createMinimalTree, createProdTree } from './lib/presets.js';
@@ -0,0 +1 @@
1
+ export { SecurityPresets, SecurityValidator } from './lib/security/security-validator.js';
@@ -0,0 +1 @@
1
+ export { createIndexedDBAdapter, createStorageAdapter } from './enhancers/serialization/serialization.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signaltree/core",
3
- "version": "8.0.2",
3
+ "version": "9.0.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",
@@ -14,6 +14,26 @@
14
14
  "import": "./dist/index.js",
15
15
  "default": "./dist/index.js"
16
16
  },
17
+ "./presets": {
18
+ "types": "./src/presets.d.ts",
19
+ "import": "./dist/presets.js",
20
+ "default": "./dist/presets.js"
21
+ },
22
+ "./security": {
23
+ "types": "./src/security.d.ts",
24
+ "import": "./dist/security.js",
25
+ "default": "./dist/security.js"
26
+ },
27
+ "./edit-session": {
28
+ "types": "./src/edit-session.d.ts",
29
+ "import": "./dist/edit-session.js",
30
+ "default": "./dist/edit-session.js"
31
+ },
32
+ "./storage": {
33
+ "types": "./src/storage.d.ts",
34
+ "import": "./dist/storage.js",
35
+ "default": "./dist/storage.js"
36
+ },
17
37
  "./package.json": "./package.json"
18
38
  },
19
39
  "peerDependencies": {
@@ -0,0 +1 @@
1
+ export { createEditSession, type EditSession, type UndoRedoHistory, } from './lib/edit-session';
package/src/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { signalTree } from './lib/signal-tree';
2
- export type { ISignalTree, SignalTree, SignalTreeBase, FullSignalTree, ProdSignalTree, TreeNode, CallableWritableSignal, AccessibleNode, NodeAccessor, Primitive, NotFn, TreeConfig, TreePreset, Enhancer, EnhancerMeta, EnhancerWithMeta, EntitySignal, EntityMapMarker, EntityConfig, MutationOptions, AddOptions, AddManyOptions, TimeTravelEntry, TimeTravelMethods, } from './lib/types';
2
+ export type { ISignalTree, SignalTree, SignalTreeBase, FullSignalTree, ProdSignalTree, TreeNode, CallableWritableSignal, AccessibleNode, NodeAccessor, Primitive, NotFn, TreeConfig, TreePreset, Enhancer, EnhancerMeta, EnhancerWithMeta, EntitySignal, EntityMapMarker, EntityConfig, MutationOptions, AddOptions, AddManyOptions, TimeTravelEntry, TimeTravelMethods, EnhancerCleanup, } from './lib/types';
3
3
  export { entityMap } from './lib/types';
4
4
  export type { ProcessDerived, DeepMergeTree, DerivedFactory, WithDerived, } from './lib/internals/derived-types';
5
5
  export { derivedFrom, externalDerived } from './lib/internals/derived-types';
@@ -9,19 +9,14 @@ export { status, isStatusMarker, LoadingState, type StatusMarker, type StatusSig
9
9
  export { stored, isStoredMarker, createStorageKeys, clearStoragePrefix, type StoredMarker, type StoredSignal, type StoredOptions, } from './lib/markers/stored';
10
10
  export { form, isFormMarker, createFormSignal, validators, FORM_MARKER, type FormMarker, type FormSignal, type FormConfig, type FormFields, type FormWizard, type WizardConfig, type WizardStepConfig, type Validator, type AsyncValidator, } from './lib/markers/form';
11
11
  export { registerMarkerProcessor } from './lib/internals/materialize-markers';
12
- export { equal, deepEqual, isNodeAccessor, isAnySignal, toWritableSignal, parsePath, composeEnhancers, isBuiltInObject, createLazySignalTree, } from './lib/utils';
13
- export { createEditSession, type EditSession, type UndoRedoHistory, } from './lib/edit-session';
12
+ export { equal, deepEqual, isNodeAccessor, isAnySignal, toWritableSignal, parsePath, composeEnhancers, isBuiltInObject, } from './lib/utils';
14
13
  export { getPathNotifier } from './lib/path-notifier';
15
- export { SecurityValidator, SecurityPresets, type SecurityEvent, type SecurityEventType, type SecurityValidatorConfig, } from './lib/security/security-validator';
16
14
  export { createEnhancer, resolveEnhancerOrder } from './enhancers/index';
17
15
  export { ENHANCER_META } from './lib/types';
18
- export { batching, batchingWithConfig, highPerformanceBatching, flushBatchedUpdates, hasPendingUpdates, getBatchQueueSize, } from './enhancers/batching/batching';
16
+ export { batching } from './enhancers/batching/batching';
19
17
  export type { BatchingConfig, BatchingMethods } from './lib/types';
20
- export { memoization, selectorMemoization, computedMemoization, deepStateMemoization, highFrequencyMemoization, highPerformanceMemoization, lightweightMemoization, shallowMemoization, memoize, memoizeShallow, memoizeReference, clearAllCaches, getGlobalCacheStats, } from './enhancers/memoization/memoization';
21
- export { timeTravel, enableTimeTravel, } from './enhancers/time-travel/time-travel';
22
- export { entities, enableEntities, highPerformanceEntities, } from './enhancers/entities/entities';
23
- export { serialization, enableSerialization, persistence, createStorageAdapter, createIndexedDBAdapter, applySerialization, applyPersistence, } from './enhancers/serialization/serialization';
24
- export { devTools, enableDevTools, fullDevTools, productionDevTools, } from './enhancers/devtools/devtools';
25
- export { createAsyncOperation, trackAsync } from './lib/async-helpers';
26
- export { TREE_PRESETS, createPresetConfig, validatePreset, getAvailablePresets, combinePresets, createDevTree, } from './enhancers/presets/lib/presets';
27
- export { SIGNAL_TREE_CONSTANTS, SIGNAL_TREE_MESSAGES } from './lib/constants';
18
+ export { memoization } from './enhancers/memoization/memoization';
19
+ export { timeTravel } from './enhancers/time-travel/time-travel';
20
+ export { serialization, persistence, } from './enhancers/serialization/serialization';
21
+ export { devTools } from './enhancers/devtools/devtools';
22
+ export { SIGNAL_TREE_CONSTANTS, SIGNAL_TREE_MESSAGES, isDev } from './lib/constants';
@@ -43,7 +43,10 @@ export interface ISignalTree<T> extends NodeAccessor<T> {
43
43
  with<TAdded>(enhancer: (tree: ISignalTree<T>) => ISignalTree<T> & TAdded): this & TAdded;
44
44
  bind(thisArg?: unknown): NodeAccessor<T>;
45
45
  destroy(): void;
46
+ readonly destroyed: Signal<boolean>;
47
+ registerCleanup(fn: EnhancerCleanup): void;
46
48
  }
49
+ export type EnhancerCleanup = () => void;
47
50
  export interface EffectsMethods<T> {
48
51
  effect(fn: (state: T) => void | (() => void)): () => void;
49
52
  subscribe(fn: (state: T) => void): () => void;
@@ -0,0 +1,2 @@
1
+ export { TREE_PRESETS, createPresetConfig, validatePreset, getAvailablePresets, combinePresets, createDevTree, createProdTree, createMinimalTree, } from './enhancers/presets/lib/presets';
2
+ export type { TreePreset } from './lib/types';
@@ -0,0 +1 @@
1
+ export { SecurityValidator, SecurityPresets, type SecurityEvent, type SecurityEventType, type SecurityValidatorConfig, } from './lib/security/security-validator';
@@ -0,0 +1,2 @@
1
+ export { createStorageAdapter, createIndexedDBAdapter, } from './enhancers/serialization/serialization';
2
+ export type { StorageAdapter } from './enhancers/serialization/serialization';
@@ -1,7 +0,0 @@
1
- function entities(config = {}) {
2
- throw new Error('entities() has been removed. Remove `.with(entities())` from your code; v7+ auto-processes EntityMap markers.');
3
- }
4
- const enableEntities = entities;
5
- const highPerformanceEntities = entities;
6
-
7
- export { enableEntities, entities, highPerformanceEntities };
@@ -1,77 +0,0 @@
1
- import { signal } from '@angular/core';
2
-
3
- function createAsyncOperation(name, operation) {
4
- return async tree => {
5
- if (typeof tree['batchUpdate'] === 'function') {
6
- const pendingPatch = {
7
- [`${name}_PENDING`]: true
8
- };
9
- tree['batchUpdate'](() => pendingPatch);
10
- } else if ('$' in tree) {
11
- try {
12
- tree['$'][`${name}_PENDING`]?.set?.(true);
13
- } catch (e) {
14
- }
15
- }
16
- try {
17
- const result = await operation();
18
- if (typeof tree['batchUpdate'] === 'function') {
19
- const resultPatch = {
20
- [`${name}_RESULT`]: result,
21
- [`${name}_PENDING`]: false
22
- };
23
- tree['batchUpdate'](() => resultPatch);
24
- } else if ('$' in tree) {
25
- try {
26
- tree['$'][`${name}_RESULT`]?.set?.(result);
27
- tree['$'][`${name}_PENDING`]?.set?.(false);
28
- } catch (e) {
29
- void e;
30
- }
31
- }
32
- return result;
33
- } catch (error) {
34
- if (typeof tree['batchUpdate'] === 'function') {
35
- const errorPatch = {
36
- [`${name}_ERROR`]: error,
37
- [`${name}_PENDING`]: false
38
- };
39
- tree['batchUpdate'](() => errorPatch);
40
- } else if ('$' in tree) {
41
- try {
42
- tree['$'][`${name}_ERROR`]?.set?.(error);
43
- tree['$'][`${name}_PENDING`]?.set?.(false);
44
- } catch (e) {
45
- }
46
- }
47
- throw error;
48
- }
49
- };
50
- }
51
- function trackAsync(operation) {
52
- const pending = signal(false);
53
- const error = signal(null);
54
- const result = signal(null);
55
- return {
56
- pending: pending.asReadonly(),
57
- error: error.asReadonly(),
58
- result: result.asReadonly(),
59
- execute: async () => {
60
- pending.set(true);
61
- error.set(null);
62
- try {
63
- const res = await operation();
64
- result.set(res);
65
- return res;
66
- } catch (e) {
67
- const err = e instanceof Error ? e : new Error(String(e));
68
- error.set(err);
69
- throw err;
70
- } finally {
71
- pending.set(false);
72
- }
73
- }
74
- };
75
- }
76
-
77
- export { createAsyncOperation, trackAsync };