@signaltree/core 4.2.1 → 5.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/README.md CHANGED
@@ -446,7 +446,7 @@ const activeUsers = computed(() => tree.$.users().filter((user) => user.active))
446
446
 
447
447
  ### 4) Manual async state management
448
448
 
449
- Core provides basic state updates. For advanced async helpers, use the built-in middleware functions (`createAsyncOperation`, `trackAsync`):
449
+ Core provides basic state updates. For advanced async helpers, use the built-in async helpers (`createAsyncOperation`, `trackAsync`):
450
450
 
451
451
  ```typescript
452
452
  const tree = signalTree({
@@ -519,12 +519,6 @@ All enhancers are exported directly from `@signaltree/core`:
519
519
  - `withDevTools()` - Redux DevTools integration
520
520
  - `withTimeTravel()` - Undo/redo functionality
521
521
 
522
- **Integration:**
523
-
524
- - `withMiddleware()` - State interceptors and logging
525
- - `createLoggingMiddleware()` - Built-in logging middleware
526
- - `createValidationMiddleware()` - Built-in validation middleware
527
-
528
522
  **Presets:**
529
523
 
530
524
  - `createDevTree()` - Pre-configured development setup
@@ -585,7 +579,7 @@ const tree = signalTree({
585
579
  user: null as User | null,
586
580
  preferences: { theme: 'light' },
587
581
  }).with(
588
- // withAsync removed — API integration patterns are now covered by middleware helpers
582
+ // withAsync removed — API integration patterns are now covered by async helpers
589
583
  withSerialization({
590
584
  // Auto-save to localStorage
591
585
  autoSave: true,
@@ -594,7 +588,7 @@ const tree = signalTree({
594
588
  withTimeTravel() // Undo/redo support
595
589
  );
596
590
 
597
- // For async operations, use manual async or middleware helpers
591
+ // For async operations, use manual async or async helpers
598
592
  async function fetchUser(id: string) {
599
593
  tree.$.loading.set(true);
600
594
  try {
@@ -895,7 +889,7 @@ appProducts.selectTotal(); // Count signal
895
889
  adminLogs.selectAll(); // All items signal
896
890
  adminReports.selectIds(); // ID array signal
897
891
 
898
- // For async operations, use manual async or middleware helpers
892
+ // For async operations, use manual async or async helpers
899
893
  async function fetchUsers() {
900
894
  tree.$.ui.loading.set(true);
901
895
  try {
@@ -924,7 +918,7 @@ const tree = signalTree({
924
918
  }).with(
925
919
  withBatching(), // Performance
926
920
  withEntities(), // Data management
927
- // withAsync removed — use middleware helpers for API integration
921
+ // withAsync removed — use async helpers for API integration
928
922
  withSerialization({
929
923
  // State persistence
930
924
  autoSave: true,
@@ -959,7 +953,7 @@ import { signalTree, withBatching, withEntities, withSerialization } from '@sign
959
953
  const tree = signalTree(initialState).with(
960
954
  withBatching(), // Performance optimization
961
955
  withEntities(), // Data management
962
- // withAsync removed — use middleware helpers for API integration
956
+ // withAsync removed — use async helpers for API integration
963
957
  withSerialization({
964
958
  // User preferences
965
959
  autoSave: true,
@@ -1029,7 +1023,7 @@ const tree2 = tree.with(withBatching());
1029
1023
  const tree3 = tree2.with(withEntities());
1030
1024
 
1031
1025
  // Phase 4: Add async for API integration
1032
- // withAsync removed — no explicit async enhancer; use middleware helpers instead
1026
+ // withAsync removed — no explicit async enhancer; use async helpers instead
1033
1027
 
1034
1028
  // Each phase is fully functional and production-ready
1035
1029
  ```
@@ -1316,7 +1310,7 @@ tree.destroy(); // Cleanup resources
1316
1310
 
1317
1311
  // Extended features (built into @signaltree/core)
1318
1312
  tree.entities<T>(key); // Entity helpers (use withEntities enhancer)
1319
- // For async operations, use manual async or middleware helpers like createAsyncOperation or trackAsync
1313
+ // For async operations, use manual async or async helpers like createAsyncOperation or trackAsync
1320
1314
  ```
1321
1315
 
1322
1316
  ## Extending with enhancers
@@ -1336,7 +1330,6 @@ All enhancers are included in `@signaltree/core`:
1336
1330
 
1337
1331
  - **withBatching()** - Batch multiple updates for better performance
1338
1332
  - **withMemoization()** - Intelligent caching & performance optimization
1339
- - **withMiddleware()** - Middleware system & state interceptors
1340
1333
  - **withEntities()** - Advanced entity management & CRUD operations
1341
1334
  - **withDevTools()** - Redux DevTools integration for debugging
1342
1335
  - **withTimeTravel()** - Undo/redo functionality & state history
@@ -1358,8 +1351,7 @@ Consider enhancers when you need:
1358
1351
 
1359
1352
  - ⚡ Performance optimization (withBatching, withMemoization)
1360
1353
  - 🐛 Advanced debugging (withDevTools, withTimeTravel)
1361
- - Entity management (withEntities)
1362
- - 🔌 Middleware patterns (withMiddleware)
1354
+ - 📦 Entity management (withEntities)
1363
1355
 
1364
1356
  Consider separate packages when you need:
1365
1357
 
@@ -1494,7 +1486,6 @@ All enhancers are now consolidated in the core package. The following features a
1494
1486
 
1495
1487
  ### Advanced Features
1496
1488
 
1497
- - **withMiddleware()** (+1.89KB gzipped) - Middleware system & state interceptors
1498
1489
  - **withEntities()** (+0.97KB gzipped) - Enhanced CRUD operations & entity management
1499
1490
 
1500
1491
  ### Development Tools
@@ -1,111 +1,66 @@
1
- import { computed } from '@angular/core';
2
- import { isAnySignal, isNodeAccessor } from '../../../lib/utils.js';
1
+ import { EntitySignalImpl } from '../../../lib/entity-signal.js';
2
+ import { getPathNotifier } from '../../../lib/path-notifier.js';
3
+ import { isNodeAccessor } from '../../../lib/utils.js';
3
4
 
4
- function resolveNestedSignal(tree, path) {
5
- const pathStr = String(path);
6
- if (!pathStr.includes('.')) {
7
- const signal = tree.state[pathStr];
8
- if (!signal) {
9
- throw new Error(`Entity key '${pathStr}' does not exist in the state. Available top-level keys: ${Object.keys(tree.state).join(', ')}`);
5
+ function isEntityMapMarker(value) {
6
+ return Boolean(value && typeof value === 'object' && value['__isEntityMap'] === true);
7
+ }
8
+ function isEntitySignal(value) {
9
+ return !!value && typeof value === 'object' && typeof value['addOne'] === 'function' && typeof value['all'] === 'function';
10
+ }
11
+ function materializeEntities(tree, notifier = getPathNotifier()) {
12
+ const registry = new Map();
13
+ const state = tree.state;
14
+ const visit = (parent, key, value, path) => {
15
+ const nextPath = [...path, key];
16
+ if (isEntityMapMarker(value)) {
17
+ const basePath = nextPath.join('.');
18
+ const config = value.__entityMapConfig ?? {};
19
+ const entitySignal = new EntitySignalImpl(config, notifier, basePath);
20
+ if (parent) {
21
+ try {
22
+ parent[key] = entitySignal;
23
+ } catch {}
24
+ }
25
+ try {
26
+ tree[key] = entitySignal;
27
+ } catch {}
28
+ registry.set(basePath, entitySignal);
29
+ return;
30
+ }
31
+ if (isNodeAccessor(value)) {
32
+ const nodeAsAny = value;
33
+ for (const childKey of Object.keys(nodeAsAny)) {
34
+ visit(nodeAsAny, childKey, nodeAsAny[childKey], nextPath);
35
+ }
36
+ return;
10
37
  }
11
- return signal;
38
+ if (value && typeof value === 'object') {
39
+ for (const childKey of Object.keys(value)) {
40
+ visit(value, childKey, value[childKey], nextPath);
41
+ }
42
+ }
43
+ };
44
+ for (const key of Object.keys(state)) {
45
+ visit(state, key, state[key], []);
12
46
  }
47
+ return registry;
48
+ }
49
+ function resolveEntitySignal(tree, registry, path) {
50
+ const pathStr = String(path);
51
+ const existing = registry.get(pathStr);
52
+ if (existing) return existing;
13
53
  const segments = pathStr.split('.');
14
54
  let current = tree.state;
15
- for (let i = 0; i < segments.length; i++) {
16
- const segment = segments[i];
55
+ for (const segment of segments) {
56
+ if (!current) break;
17
57
  current = current[segment];
18
- if (current === undefined) {
19
- const attemptedPath = segments.slice(0, i + 1).join('.');
20
- throw new Error(`Entity path '${pathStr}' is invalid: '${attemptedPath}' does not exist in the state`);
21
- }
22
58
  }
23
- if (isAnySignal(current)) {
59
+ if (isEntitySignal(current)) {
60
+ registry.set(pathStr, current);
24
61
  return current;
25
62
  }
26
- throw new Error(`Entity path '${pathStr}' does not resolve to a signal. Ensure all parent levels in the path are valid nested objects.`);
27
- }
28
- function createEntityHelpers(tree, entityKey) {
29
- const getEntitySignal = () => {
30
- const signal = resolveNestedSignal(tree, entityKey);
31
- if (!isAnySignal(signal)) {
32
- throw new Error(`Entity key '${String(entityKey)}' is not a signal. This should not happen with SignalTree.`);
33
- }
34
- const castSignal = signal;
35
- const value = castSignal();
36
- if (!Array.isArray(value)) {
37
- throw new Error(`Entity key '${String(entityKey)}' does not contain an array. Current type: ${typeof value}`);
38
- }
39
- return castSignal;
40
- };
41
- const setSignalValue = (signal, value) => {
42
- if (isNodeAccessor(signal)) {
43
- signal(value);
44
- } else {
45
- signal.set(value);
46
- }
47
- };
48
- return {
49
- add: entity => {
50
- const entitySignal = getEntitySignal();
51
- const currentEntities = entitySignal();
52
- if (currentEntities.some(e => e.id === entity.id)) {
53
- throw new Error(`Entity with id '${entity.id}' already exists`);
54
- }
55
- setSignalValue(entitySignal, [...currentEntities, entity]);
56
- },
57
- update: (id, updates) => {
58
- const entitySignal = getEntitySignal();
59
- const currentEntities = entitySignal();
60
- const updatedEntities = currentEntities.map(entity => entity.id === id ? {
61
- ...entity,
62
- ...updates
63
- } : entity);
64
- setSignalValue(entitySignal, updatedEntities);
65
- },
66
- remove: id => {
67
- const entitySignal = getEntitySignal();
68
- const currentEntities = entitySignal();
69
- const filteredEntities = currentEntities.filter(entity => entity.id !== id);
70
- setSignalValue(entitySignal, filteredEntities);
71
- },
72
- upsert: entity => {
73
- const entitySignal = getEntitySignal();
74
- const currentEntities = entitySignal();
75
- const index = currentEntities.findIndex(e => e.id === entity.id);
76
- if (index >= 0) {
77
- const updatedEntities = [...currentEntities];
78
- updatedEntities[index] = entity;
79
- setSignalValue(entitySignal, updatedEntities);
80
- } else {
81
- setSignalValue(entitySignal, [...currentEntities, entity]);
82
- }
83
- },
84
- selectById: id => {
85
- const entitySignal = getEntitySignal();
86
- return computed(() => entitySignal().find(entity => entity.id === id));
87
- },
88
- selectBy: predicate => {
89
- const entitySignal = getEntitySignal();
90
- return computed(() => entitySignal().filter(predicate));
91
- },
92
- selectIds: () => {
93
- const entitySignal = getEntitySignal();
94
- return computed(() => entitySignal().map(entity => entity.id));
95
- },
96
- selectAll: () => {
97
- const entitySignal = getEntitySignal();
98
- return entitySignal;
99
- },
100
- selectTotal: () => {
101
- const entitySignal = getEntitySignal();
102
- return computed(() => entitySignal().length);
103
- },
104
- clear: () => {
105
- const entitySignal = getEntitySignal();
106
- setSignalValue(entitySignal, []);
107
- }
108
- };
63
+ throw new Error(`Entity path '${pathStr}' is not configured. Define it with entityMap() in your initial state.`);
109
64
  }
110
65
  function withEntities(config = {}) {
111
66
  const {
@@ -115,9 +70,10 @@ function withEntities(config = {}) {
115
70
  if (!enabled) {
116
71
  return tree;
117
72
  }
73
+ const registry = materializeEntities(tree);
118
74
  const enhancedTree = Object.assign(tree, {
119
- entities(entityKey) {
120
- return createEntityHelpers(tree, entityKey);
75
+ entities(path) {
76
+ return resolveEntitySignal(tree, registry, path);
121
77
  }
122
78
  });
123
79
  return enhancedTree;
@@ -130,7 +86,8 @@ function enableEntities() {
130
86
  }
131
87
  function withHighPerformanceEntities() {
132
88
  return withEntities({
133
- enabled: true});
89
+ enabled: true
90
+ });
134
91
  }
135
92
 
136
93
  export { enableEntities, withEntities, withHighPerformanceEntities };
@@ -2,7 +2,6 @@ import { composeEnhancers } from '../../../lib/utils.js';
2
2
  import { withHighPerformanceBatching, withBatching } from '../../batching/lib/batching.js';
3
3
  import { withDevTools } from '../../devtools/lib/devtools.js';
4
4
  import { withHighPerformanceMemoization, withMemoization } from '../../memoization/lib/memoization.js';
5
- import { withMiddleware } from '../../middleware/lib/middleware.js';
6
5
  import { withTimeTravel } from '../../time-travel/lib/time-travel.js';
7
6
 
8
7
  const TREE_PRESETS = {
@@ -76,7 +75,7 @@ function combinePresets(presets, overrides = {}) {
76
75
  }
77
76
  function createDevTree(overrides = {}) {
78
77
  const config = createPresetConfig('development', overrides);
79
- const composed = composeEnhancers(withBatching(), withHighPerformanceBatching(), withMemoization(), withHighPerformanceMemoization(), withMiddleware(), withTimeTravel(), withDevTools({
78
+ const composed = composeEnhancers(withBatching(), withHighPerformanceBatching(), withMemoization(), withHighPerformanceMemoization(), withTimeTravel(), withDevTools({
80
79
  treeName: config.treeName ?? 'SignalTree Dev'
81
80
  }));
82
81
  return {
@@ -544,6 +544,8 @@ function withPersistence(config) {
544
544
  }
545
545
  if (autoSave) {
546
546
  let saveTimeout;
547
+ let previousState = JSON.stringify(tree());
548
+ let pollingActive = true;
547
549
  const triggerAutoSave = () => {
548
550
  if (saveTimeout) {
549
551
  clearTimeout(saveTimeout);
@@ -554,28 +556,30 @@ function withPersistence(config) {
554
556
  });
555
557
  }, debounceMs);
556
558
  };
557
- const watchSignals = (obj, path = '') => {
558
- if (!obj || typeof obj !== 'object') return;
559
- for (const [key, value] of Object.entries(obj)) {
560
- if (isSignal(value)) {
561
- const signal = value;
562
- let previousValue = signal();
563
- const checkForChanges = () => {
564
- const currentValue = signal();
565
- if (currentValue !== previousValue) {
566
- previousValue = currentValue;
567
- triggerAutoSave();
568
- }
569
- setTimeout(checkForChanges, 50);
570
- };
571
- setTimeout(checkForChanges, 0);
572
- } else if (value && typeof value === 'object') {
573
- watchSignals(value, path ? `${path}.${key}` : key);
559
+ let usingReactiveSubscription = false;
560
+ try {
561
+ tree.subscribe(() => {
562
+ const currentState = JSON.stringify(tree());
563
+ if (currentState !== previousState) {
564
+ previousState = currentState;
565
+ triggerAutoSave();
574
566
  }
575
- }
576
- };
577
- watchSignals(tree.state);
567
+ });
568
+ usingReactiveSubscription = true;
569
+ } catch {
570
+ const checkForChanges = () => {
571
+ if (!pollingActive) return;
572
+ const currentState = JSON.stringify(tree());
573
+ if (currentState !== previousState) {
574
+ previousState = currentState;
575
+ triggerAutoSave();
576
+ }
577
+ setTimeout(checkForChanges, 100);
578
+ };
579
+ setTimeout(checkForChanges, 0);
580
+ }
578
581
  enhanced.__flushAutoSave = () => {
582
+ pollingActive = false;
579
583
  if (saveTimeout) {
580
584
  clearTimeout(saveTimeout);
581
585
  saveTimeout = undefined;
package/dist/index.js CHANGED
@@ -1,16 +1,15 @@
1
1
  export { signalTree } from './lib/signal-tree.js';
2
+ export { ENHANCER_META, entityMap } from './lib/types.js';
2
3
  export { composeEnhancers, createLazySignalTree, isAnySignal, isNodeAccessor, toWritableSignal } from './lib/utils.js';
3
4
  export { SecurityPresets, SecurityValidator } from './lib/security/security-validator.js';
4
5
  export { createEnhancer, resolveEnhancerOrder } from './enhancers/index.js';
5
- export { ENHANCER_META } from './lib/types.js';
6
6
  export { flushBatchedUpdates, getBatchQueueSize, hasPendingUpdates, withBatching, withHighPerformanceBatching } from './enhancers/batching/lib/batching.js';
7
7
  export { cleanupMemoizationCache, clearAllCaches, getGlobalCacheStats, memoize, memoizeReference, memoizeShallow, withComputedMemoization, withDeepStateMemoization, withHighFrequencyMemoization, withHighPerformanceMemoization, withLightweightMemoization, withMemoization, withSelectorMemoization, withShallowMemoization } from './enhancers/memoization/lib/memoization.js';
8
8
  export { enableTimeTravel, getTimeTravel, withTimeTravel } from './enhancers/time-travel/lib/time-travel.js';
9
9
  export { enableEntities, withEntities, withHighPerformanceEntities } from './enhancers/entities/lib/entities.js';
10
10
  export { applyPersistence, applySerialization, createIndexedDBAdapter, createStorageAdapter, enableSerialization, withPersistence, withSerialization } from './enhancers/serialization/lib/serialization.js';
11
11
  export { enableDevTools, withDevTools, withFullDevTools, withProductionDevTools } from './enhancers/devtools/lib/devtools.js';
12
- export { createLoggingMiddleware, createValidationMiddleware, withMiddleware } from './enhancers/middleware/lib/middleware.js';
13
- export { createAsyncOperation, trackAsync } from './enhancers/middleware/lib/async-helpers.js';
12
+ export { createAsyncOperation, trackAsync } from './lib/async-helpers.js';
14
13
  export { TREE_PRESETS, combinePresets, createDevTree, createPresetConfig, getAvailablePresets, validatePreset } from './enhancers/presets/lib/presets.js';
15
14
  export { computedEnhancer, createComputed } from './enhancers/computed/lib/computed.js';
16
15
  export { SIGNAL_TREE_CONSTANTS, SIGNAL_TREE_MESSAGES } from './lib/constants.js';
@@ -0,0 +1,280 @@
1
+ import { signal, computed } from '@angular/core';
2
+
3
+ class EntitySignalImpl {
4
+ pathNotifier;
5
+ basePath;
6
+ storage = new Map();
7
+ allSignal;
8
+ countSignal;
9
+ idsSignal;
10
+ mapSignal;
11
+ nodeCache = new Map();
12
+ selectId;
13
+ tapHandlers = [];
14
+ interceptHandlers = [];
15
+ constructor(config, pathNotifier, basePath) {
16
+ this.pathNotifier = pathNotifier;
17
+ this.basePath = basePath;
18
+ this.selectId = config.selectId ?? (entity => entity['id']);
19
+ this.allSignal = signal([]);
20
+ this.countSignal = signal(0);
21
+ this.idsSignal = signal([]);
22
+ this.mapSignal = signal(new Map());
23
+ return new Proxy(this, {
24
+ get: (target, prop) => {
25
+ if (typeof prop === 'string' && !isNaN(Number(prop))) {
26
+ return target.byId(Number(prop));
27
+ }
28
+ return target[prop];
29
+ }
30
+ });
31
+ }
32
+ byId(id) {
33
+ const entity = this.storage.get(id);
34
+ if (!entity) return undefined;
35
+ return this.getOrCreateNode(id, entity);
36
+ }
37
+ byIdOrFail(id) {
38
+ const node = this.byId(id);
39
+ if (!node) {
40
+ throw new Error(`Entity with id ${String(id)} not found`);
41
+ }
42
+ return node;
43
+ }
44
+ all() {
45
+ return this.allSignal;
46
+ }
47
+ count() {
48
+ return this.countSignal;
49
+ }
50
+ ids() {
51
+ return this.idsSignal;
52
+ }
53
+ map() {
54
+ return this.mapSignal;
55
+ }
56
+ has(id) {
57
+ return computed(() => this.storage.has(id));
58
+ }
59
+ isEmpty() {
60
+ return computed(() => this.storage.size === 0);
61
+ }
62
+ where(predicate) {
63
+ return computed(() => {
64
+ const result = [];
65
+ for (const entity of this.storage.values()) {
66
+ if (predicate(entity)) {
67
+ result.push(entity);
68
+ }
69
+ }
70
+ return result;
71
+ });
72
+ }
73
+ find(predicate) {
74
+ return computed(() => {
75
+ for (const entity of this.storage.values()) {
76
+ if (predicate(entity)) {
77
+ return entity;
78
+ }
79
+ }
80
+ return undefined;
81
+ });
82
+ }
83
+ addOne(entity, opts) {
84
+ const id = opts?.selectId?.(entity) ?? this.selectId(entity);
85
+ if (this.storage.has(id)) {
86
+ throw new Error(`Entity with id ${String(id)} already exists`);
87
+ }
88
+ let transformedEntity = entity;
89
+ for (const handler of this.interceptHandlers) {
90
+ const ctx = {
91
+ block: reason => {
92
+ throw new Error(`Cannot add entity: ${reason || 'blocked by interceptor'}`);
93
+ },
94
+ transform: value => {
95
+ transformedEntity = value;
96
+ },
97
+ blocked: false,
98
+ blockReason: undefined
99
+ };
100
+ handler.onAdd?.(entity, ctx);
101
+ }
102
+ this.storage.set(id, transformedEntity);
103
+ this.nodeCache.delete(id);
104
+ this.updateSignals();
105
+ this.pathNotifier.notify(`${this.basePath}.${String(id)}`, transformedEntity, undefined);
106
+ for (const handler of this.tapHandlers) {
107
+ handler.onAdd?.(transformedEntity, id);
108
+ }
109
+ return id;
110
+ }
111
+ addMany(entities, opts) {
112
+ const ids = [];
113
+ for (const entity of entities) {
114
+ ids.push(this.addOne(entity, opts));
115
+ }
116
+ return ids;
117
+ }
118
+ updateOne(id, changes) {
119
+ const entity = this.storage.get(id);
120
+ if (!entity) {
121
+ throw new Error(`Entity with id ${String(id)} not found`);
122
+ }
123
+ const prev = entity;
124
+ let transformedChanges = changes;
125
+ for (const handler of this.interceptHandlers) {
126
+ const ctx = {
127
+ block: reason => {
128
+ throw new Error(`Cannot update entity: ${reason || 'blocked by interceptor'}`);
129
+ },
130
+ transform: value => {
131
+ transformedChanges = value;
132
+ },
133
+ blocked: false,
134
+ blockReason: undefined
135
+ };
136
+ handler.onUpdate?.(id, changes, ctx);
137
+ }
138
+ const finalUpdated = {
139
+ ...entity,
140
+ ...transformedChanges
141
+ };
142
+ this.storage.set(id, finalUpdated);
143
+ this.nodeCache.delete(id);
144
+ this.updateSignals();
145
+ this.pathNotifier.notify(`${this.basePath}.${String(id)}`, finalUpdated, prev);
146
+ for (const handler of this.tapHandlers) {
147
+ handler.onUpdate?.(id, transformedChanges, finalUpdated);
148
+ }
149
+ }
150
+ updateMany(ids, changes) {
151
+ for (const id of ids) {
152
+ this.updateOne(id, changes);
153
+ }
154
+ }
155
+ updateWhere(predicate, changes) {
156
+ let count = 0;
157
+ for (const [id, entity] of this.storage) {
158
+ if (predicate(entity)) {
159
+ this.updateOne(id, changes);
160
+ count++;
161
+ }
162
+ }
163
+ return count;
164
+ }
165
+ removeOne(id) {
166
+ const entity = this.storage.get(id);
167
+ if (!entity) {
168
+ throw new Error(`Entity with id ${String(id)} not found`);
169
+ }
170
+ for (const handler of this.interceptHandlers) {
171
+ const ctx = {
172
+ block: reason => {
173
+ throw new Error(`Cannot remove entity: ${reason || 'blocked by interceptor'}`);
174
+ },
175
+ transform: () => {},
176
+ blocked: false,
177
+ blockReason: undefined
178
+ };
179
+ handler.onRemove?.(id, entity, ctx);
180
+ }
181
+ this.storage.delete(id);
182
+ this.nodeCache.delete(id);
183
+ this.updateSignals();
184
+ this.pathNotifier.notify(`${this.basePath}.${String(id)}`, undefined, entity);
185
+ for (const handler of this.tapHandlers) {
186
+ handler.onRemove?.(id, entity);
187
+ }
188
+ }
189
+ removeMany(ids) {
190
+ for (const id of ids) {
191
+ this.removeOne(id);
192
+ }
193
+ }
194
+ removeWhere(predicate) {
195
+ const idsToRemove = [];
196
+ for (const [id, entity] of this.storage) {
197
+ if (predicate(entity)) {
198
+ idsToRemove.push(id);
199
+ }
200
+ }
201
+ let count = 0;
202
+ for (const id of idsToRemove) {
203
+ this.removeOne(id);
204
+ count++;
205
+ }
206
+ return count;
207
+ }
208
+ upsertOne(entity, opts) {
209
+ const id = opts?.selectId?.(entity) ?? this.selectId(entity);
210
+ if (this.storage.has(id)) {
211
+ this.updateOne(id, entity);
212
+ } else {
213
+ this.addOne(entity, opts);
214
+ }
215
+ return id;
216
+ }
217
+ upsertMany(entities, opts) {
218
+ return entities.map(e => this.upsertOne(e, opts));
219
+ }
220
+ clear() {
221
+ this.storage.clear();
222
+ this.nodeCache.clear();
223
+ this.updateSignals();
224
+ }
225
+ removeAll() {
226
+ this.clear();
227
+ }
228
+ setAll(entities, opts) {
229
+ this.clear();
230
+ this.addMany(entities, opts);
231
+ }
232
+ tap(handlers) {
233
+ this.tapHandlers.push(handlers);
234
+ return () => {
235
+ const idx = this.tapHandlers.indexOf(handlers);
236
+ if (idx > -1) this.tapHandlers.splice(idx, 1);
237
+ };
238
+ }
239
+ intercept(handlers) {
240
+ this.interceptHandlers.push(handlers);
241
+ return () => {
242
+ const idx = this.interceptHandlers.indexOf(handlers);
243
+ if (idx > -1) this.interceptHandlers.splice(idx, 1);
244
+ };
245
+ }
246
+ updateSignals() {
247
+ const entities = Array.from(this.storage.values());
248
+ const ids = Array.from(this.storage.keys());
249
+ const map = new Map(this.storage);
250
+ this.allSignal.set(entities);
251
+ this.countSignal.set(entities.length);
252
+ this.idsSignal.set(ids);
253
+ this.mapSignal.set(map);
254
+ }
255
+ getOrCreateNode(id, entity) {
256
+ let node = this.nodeCache.get(id);
257
+ if (!node) {
258
+ node = this.createEntityNode(id, entity);
259
+ this.nodeCache.set(id, node);
260
+ }
261
+ return node;
262
+ }
263
+ createEntityNode(id, entity) {
264
+ const node = () => this.storage.get(id);
265
+ for (const key of Object.keys(entity)) {
266
+ Object.defineProperty(node, key, {
267
+ get: () => {
268
+ const current = this.storage.get(id);
269
+ const value = current?.[key];
270
+ return () => value;
271
+ },
272
+ enumerable: true,
273
+ configurable: true
274
+ });
275
+ }
276
+ return node;
277
+ }
278
+ }
279
+
280
+ export { EntitySignalImpl };