@signaltree/core 6.2.3 → 6.3.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 +40 -0
- package/dist/enhancers/time-travel/time-travel.js +38 -2
- package/dist/lib/entity-signal.js +454 -468
- package/dist/lib/path-notifier.js +72 -0
- package/dist/lib/signal-tree.js +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -148,6 +148,46 @@ $.users.setAll(usersFromApi); // Replace all
|
|
|
148
148
|
const user = entityMap()[123]; // Requires intermediate object
|
|
149
149
|
```
|
|
150
150
|
|
|
151
|
+
### Notification Batching
|
|
152
|
+
|
|
153
|
+
SignalTree automatically batches _notification delivery_ to subscribers and change detection to the end of the current microtask. This prevents render thrashing when multiple values are updated together and preserves immediate read-after-write semantics (values update synchronously, notifications are deferred).
|
|
154
|
+
|
|
155
|
+
**Example**
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
// Multiple updates in the same microtask are coalesced into a single notification
|
|
159
|
+
tree.$.form.name.set('Alice');
|
|
160
|
+
tree.$.form.email.set('alice@example.com');
|
|
161
|
+
tree.$.form.submitted.set(true);
|
|
162
|
+
// → Subscribers are notified once at the end of the microtask with final values
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Testing**
|
|
166
|
+
|
|
167
|
+
When tests need synchronous notification delivery, use `flushSync()`:
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { getPathNotifier } from '@signaltree/core';
|
|
171
|
+
|
|
172
|
+
it('updates state', () => {
|
|
173
|
+
tree.$.count.set(5);
|
|
174
|
+
getPathNotifier().flushSync();
|
|
175
|
+
expect(subscriber).toHaveBeenCalledWith(5, 0);
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Alternatively, await a microtask (`await Promise.resolve()`) to allow the automatic flush to occur.
|
|
180
|
+
|
|
181
|
+
**Opting out**
|
|
182
|
+
|
|
183
|
+
To disable automatic microtask batching for a specific tree instance:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
const tree = signalTree(initialState, { batching: false });
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Use this only for rare cases that truly require synchronous notifications (most apps should keep batching enabled).
|
|
190
|
+
|
|
151
191
|
## Quick start
|
|
152
192
|
|
|
153
193
|
### Installation
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { snapshotState } from '../../lib/utils.js';
|
|
2
|
-
import {
|
|
2
|
+
import { deepEqual, deepClone } from './utils.js';
|
|
3
3
|
|
|
4
4
|
class TimeTravelManager {
|
|
5
5
|
tree;
|
|
@@ -24,7 +24,7 @@ class TimeTravelManager {
|
|
|
24
24
|
};
|
|
25
25
|
this.addEntry('INIT', this.tree());
|
|
26
26
|
}
|
|
27
|
-
addEntry(action, state, payload) {
|
|
27
|
+
addEntry(action, state, payload, provisional = false) {
|
|
28
28
|
if (this.currentIndex < this.history.length - 1) {
|
|
29
29
|
this.history = this.history.slice(0, this.currentIndex + 1);
|
|
30
30
|
}
|
|
@@ -43,6 +43,12 @@ class TimeTravelManager {
|
|
|
43
43
|
payload
|
|
44
44
|
})
|
|
45
45
|
};
|
|
46
|
+
if (provisional) entry.__provisional = true;
|
|
47
|
+
const last = this.history[this.history.length - 1];
|
|
48
|
+
if (last && deepEqual(last.state, entry.state)) {
|
|
49
|
+
if (last.__provisional) delete last.__provisional;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
46
52
|
this.history.push(entry);
|
|
47
53
|
this.currentIndex = this.history.length - 1;
|
|
48
54
|
if (this.history.length > this.maxHistorySize) {
|
|
@@ -59,6 +65,20 @@ class TimeTravelManager {
|
|
|
59
65
|
this.restoreState(entry.state);
|
|
60
66
|
return true;
|
|
61
67
|
}
|
|
68
|
+
finalizeProvisional(state) {
|
|
69
|
+
const last = this.history[this.history.length - 1];
|
|
70
|
+
if (last && last.__provisional) {
|
|
71
|
+
if (deepEqual(last.state, state)) {
|
|
72
|
+
delete last.__provisional;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
last.state = deepClone(state);
|
|
76
|
+
last.timestamp = Date.now();
|
|
77
|
+
delete last.__provisional;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.addEntry('update', state);
|
|
81
|
+
}
|
|
62
82
|
redo() {
|
|
63
83
|
if (!this.canRedo()) {
|
|
64
84
|
return false;
|
|
@@ -143,6 +163,22 @@ function timeTravel(config = {}) {
|
|
|
143
163
|
isRestoring = false;
|
|
144
164
|
}
|
|
145
165
|
});
|
|
166
|
+
try {
|
|
167
|
+
const req = globalThis['require'];
|
|
168
|
+
if (typeof req === 'function') {
|
|
169
|
+
const {
|
|
170
|
+
getPathNotifier
|
|
171
|
+
} = req('../../lib/path-notifier');
|
|
172
|
+
const notifier = getPathNotifier();
|
|
173
|
+
if (notifier && typeof notifier.onFlush === 'function') {
|
|
174
|
+
notifier.onFlush(() => {
|
|
175
|
+
if (isRestoring) return;
|
|
176
|
+
const afterState = originalTreeCall();
|
|
177
|
+
timeTravelManager.addEntry('batch', afterState);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch {}
|
|
146
182
|
const enhancedTree = function (...args) {
|
|
147
183
|
if (args.length === 0) {
|
|
148
184
|
return originalTreeCall();
|
|
@@ -1,120 +1,101 @@
|
|
|
1
1
|
import { signal, computed } from '@angular/core';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
this.idsSignal = signal([]);
|
|
22
|
-
this.mapSignal = signal(new Map());
|
|
3
|
+
function createEntitySignal(config, pathNotifier, basePath) {
|
|
4
|
+
const storage = new Map();
|
|
5
|
+
const allSignal = signal([]);
|
|
6
|
+
const countSignal = signal(0);
|
|
7
|
+
const idsSignal = signal([]);
|
|
8
|
+
const mapSignal = signal(new Map());
|
|
9
|
+
const nodeCache = new Map();
|
|
10
|
+
const selectId = config.selectId ?? (entity => entity['id']);
|
|
11
|
+
const tapHandlers = [];
|
|
12
|
+
const interceptHandlers = [];
|
|
13
|
+
function updateSignals() {
|
|
14
|
+
const entities = Array.from(storage.values());
|
|
15
|
+
const ids = Array.from(storage.keys());
|
|
16
|
+
const map = new Map(storage);
|
|
17
|
+
allSignal.set(entities);
|
|
18
|
+
countSignal.set(entities.length);
|
|
19
|
+
idsSignal.set(ids);
|
|
20
|
+
mapSignal.set(map);
|
|
23
21
|
}
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
function createEntityNode(id, entity) {
|
|
23
|
+
const node = () => storage.get(id);
|
|
24
|
+
for (const key of Object.keys(entity)) {
|
|
25
|
+
Object.defineProperty(node, key, {
|
|
26
|
+
get: () => {
|
|
27
|
+
const current = storage.get(id);
|
|
28
|
+
const value = current?.[key];
|
|
29
|
+
return () => value;
|
|
30
|
+
},
|
|
31
|
+
enumerable: true,
|
|
32
|
+
configurable: true
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return node;
|
|
28
36
|
}
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
function getOrCreateNode(id, entity) {
|
|
38
|
+
let node = nodeCache.get(id);
|
|
31
39
|
if (!node) {
|
|
32
|
-
|
|
40
|
+
node = createEntityNode(id, entity);
|
|
41
|
+
nodeCache.set(id, node);
|
|
33
42
|
}
|
|
34
43
|
return node;
|
|
35
44
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
has(id) {
|
|
49
|
-
return computed(() => this.storage.has(id));
|
|
50
|
-
}
|
|
51
|
-
get isEmpty() {
|
|
52
|
-
return computed(() => this.storage.size === 0);
|
|
53
|
-
}
|
|
54
|
-
where(predicate) {
|
|
55
|
-
return computed(() => {
|
|
56
|
-
const result = [];
|
|
57
|
-
for (const entity of this.storage.values()) {
|
|
58
|
-
if (predicate(entity)) {
|
|
59
|
-
result.push(entity);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return result;
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
find(predicate) {
|
|
66
|
-
return computed(() => {
|
|
67
|
-
for (const entity of this.storage.values()) {
|
|
68
|
-
if (predicate(entity)) {
|
|
69
|
-
return entity;
|
|
70
|
-
}
|
|
45
|
+
const whereCache = new WeakMap();
|
|
46
|
+
const findCache = new WeakMap();
|
|
47
|
+
const api = {
|
|
48
|
+
byId(id) {
|
|
49
|
+
const entity = storage.get(id);
|
|
50
|
+
if (!entity) return undefined;
|
|
51
|
+
return getOrCreateNode(id, entity);
|
|
52
|
+
},
|
|
53
|
+
byIdOrFail(id) {
|
|
54
|
+
const node = api.byId(id);
|
|
55
|
+
if (!node) {
|
|
56
|
+
throw new Error(`Entity with id ${String(id)} not found`);
|
|
71
57
|
}
|
|
72
|
-
return
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
58
|
+
return node;
|
|
59
|
+
},
|
|
60
|
+
get all() {
|
|
61
|
+
return allSignal;
|
|
62
|
+
},
|
|
63
|
+
get count() {
|
|
64
|
+
return countSignal;
|
|
65
|
+
},
|
|
66
|
+
get ids() {
|
|
67
|
+
return idsSignal;
|
|
68
|
+
},
|
|
69
|
+
get map() {
|
|
70
|
+
return mapSignal;
|
|
71
|
+
},
|
|
72
|
+
has(id) {
|
|
73
|
+
return computed(() => mapSignal().has(id));
|
|
74
|
+
},
|
|
75
|
+
get isEmpty() {
|
|
76
|
+
return computed(() => countSignal() === 0);
|
|
77
|
+
},
|
|
78
|
+
where(predicate) {
|
|
79
|
+
const cached = whereCache.get(predicate);
|
|
80
|
+
if (cached) return cached;
|
|
81
|
+
const s = computed(() => allSignal().filter(predicate));
|
|
82
|
+
whereCache.set(predicate, s);
|
|
83
|
+
return s;
|
|
84
|
+
},
|
|
85
|
+
find(predicate) {
|
|
86
|
+
const cached = findCache.get(predicate);
|
|
87
|
+
if (cached) return cached;
|
|
88
|
+
const s = computed(() => allSignal().find(predicate));
|
|
89
|
+
findCache.set(predicate, s);
|
|
90
|
+
return s;
|
|
91
|
+
},
|
|
92
|
+
addOne(entity, opts) {
|
|
93
|
+
const id = opts?.selectId?.(entity) ?? selectId(entity);
|
|
94
|
+
if (storage.has(id)) {
|
|
108
95
|
throw new Error(`Entity with id ${String(id)} already exists`);
|
|
109
96
|
}
|
|
110
|
-
idsToAdd.push(id);
|
|
111
|
-
}
|
|
112
|
-
const addedEntities = [];
|
|
113
|
-
for (let i = 0; i < entities.length; i++) {
|
|
114
|
-
const entity = entities[i];
|
|
115
|
-
const id = idsToAdd[i];
|
|
116
97
|
let transformedEntity = entity;
|
|
117
|
-
for (const handler of
|
|
98
|
+
for (const handler of interceptHandlers) {
|
|
118
99
|
const ctx = {
|
|
119
100
|
block: reason => {
|
|
120
101
|
throw new Error(`Cannot add entity: ${reason || 'blocked by interceptor'}`);
|
|
@@ -127,73 +108,74 @@ class EntitySignalImpl {
|
|
|
127
108
|
};
|
|
128
109
|
handler.onAdd?.(entity, ctx);
|
|
129
110
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
111
|
+
storage.set(id, transformedEntity);
|
|
112
|
+
nodeCache.delete(id);
|
|
113
|
+
updateSignals();
|
|
114
|
+
pathNotifier.notify(`${basePath}.${String(id)}`, transformedEntity, undefined);
|
|
115
|
+
for (const handler of tapHandlers) {
|
|
116
|
+
handler.onAdd?.(transformedEntity, id);
|
|
117
|
+
}
|
|
118
|
+
return id;
|
|
119
|
+
},
|
|
120
|
+
addMany(entities, opts) {
|
|
121
|
+
const idsToAdd = [];
|
|
122
|
+
for (const entity of entities) {
|
|
123
|
+
const id = opts?.selectId?.(entity) ?? selectId(entity);
|
|
124
|
+
if (storage.has(id)) {
|
|
125
|
+
throw new Error(`Entity with id ${String(id)} already exists`);
|
|
126
|
+
}
|
|
127
|
+
idsToAdd.push(id);
|
|
128
|
+
}
|
|
129
|
+
const addedEntities = [];
|
|
130
|
+
for (let i = 0; i < entities.length; i++) {
|
|
131
|
+
const entity = entities[i];
|
|
132
|
+
const id = idsToAdd[i];
|
|
133
|
+
let transformedEntity = entity;
|
|
134
|
+
for (const handler of interceptHandlers) {
|
|
135
|
+
const ctx = {
|
|
136
|
+
block: reason => {
|
|
137
|
+
throw new Error(`Cannot add entity: ${reason || 'blocked by interceptor'}`);
|
|
138
|
+
},
|
|
139
|
+
transform: value => {
|
|
140
|
+
transformedEntity = value;
|
|
141
|
+
},
|
|
142
|
+
blocked: false,
|
|
143
|
+
blockReason: undefined
|
|
144
|
+
};
|
|
145
|
+
handler.onAdd?.(entity, ctx);
|
|
146
|
+
}
|
|
147
|
+
storage.set(id, transformedEntity);
|
|
148
|
+
nodeCache.delete(id);
|
|
149
|
+
addedEntities.push({
|
|
150
|
+
id,
|
|
151
|
+
entity: transformedEntity
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
updateSignals();
|
|
155
|
+
for (const {
|
|
133
156
|
id,
|
|
134
|
-
entity
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
this.updateSignals();
|
|
138
|
-
for (const {
|
|
139
|
-
id,
|
|
140
|
-
entity
|
|
141
|
-
} of addedEntities) {
|
|
142
|
-
this.pathNotifier.notify(`${this.basePath}.${String(id)}`, entity, undefined);
|
|
143
|
-
}
|
|
144
|
-
for (const {
|
|
145
|
-
id,
|
|
146
|
-
entity
|
|
147
|
-
} of addedEntities) {
|
|
148
|
-
for (const handler of this.tapHandlers) {
|
|
149
|
-
handler.onAdd?.(entity, id);
|
|
157
|
+
entity
|
|
158
|
+
} of addedEntities) {
|
|
159
|
+
pathNotifier.notify(`${basePath}.${String(id)}`, entity, undefined);
|
|
150
160
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
block: reason => {
|
|
164
|
-
throw new Error(`Cannot update entity: ${reason || 'blocked by interceptor'}`);
|
|
165
|
-
},
|
|
166
|
-
transform: value => {
|
|
167
|
-
transformedChanges = value;
|
|
168
|
-
},
|
|
169
|
-
blocked: false,
|
|
170
|
-
blockReason: undefined
|
|
171
|
-
};
|
|
172
|
-
handler.onUpdate?.(id, changes, ctx);
|
|
173
|
-
}
|
|
174
|
-
const finalUpdated = {
|
|
175
|
-
...entity,
|
|
176
|
-
...transformedChanges
|
|
177
|
-
};
|
|
178
|
-
this.storage.set(id, finalUpdated);
|
|
179
|
-
this.nodeCache.delete(id);
|
|
180
|
-
this.updateSignals();
|
|
181
|
-
this.pathNotifier.notify(`${this.basePath}.${String(id)}`, finalUpdated, prev);
|
|
182
|
-
for (const handler of this.tapHandlers) {
|
|
183
|
-
handler.onUpdate?.(id, transformedChanges, finalUpdated);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
updateMany(ids, changes) {
|
|
187
|
-
if (ids.length === 0) return;
|
|
188
|
-
const updatedEntities = [];
|
|
189
|
-
for (const id of ids) {
|
|
190
|
-
const entity = this.storage.get(id);
|
|
161
|
+
for (const {
|
|
162
|
+
id,
|
|
163
|
+
entity
|
|
164
|
+
} of addedEntities) {
|
|
165
|
+
for (const handler of tapHandlers) {
|
|
166
|
+
handler.onAdd?.(entity, id);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return idsToAdd;
|
|
170
|
+
},
|
|
171
|
+
updateOne(id, changes) {
|
|
172
|
+
const entity = storage.get(id);
|
|
191
173
|
if (!entity) {
|
|
192
174
|
throw new Error(`Entity with id ${String(id)} not found`);
|
|
193
175
|
}
|
|
194
176
|
const prev = entity;
|
|
195
177
|
let transformedChanges = changes;
|
|
196
|
-
for (const handler of
|
|
178
|
+
for (const handler of interceptHandlers) {
|
|
197
179
|
const ctx = {
|
|
198
180
|
block: reason => {
|
|
199
181
|
throw new Error(`Cannot update entity: ${reason || 'blocked by interceptor'}`);
|
|
@@ -210,78 +192,86 @@ class EntitySignalImpl {
|
|
|
210
192
|
...entity,
|
|
211
193
|
...transformedChanges
|
|
212
194
|
};
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
195
|
+
storage.set(id, finalUpdated);
|
|
196
|
+
nodeCache.delete(id);
|
|
197
|
+
updateSignals();
|
|
198
|
+
pathNotifier.notify(`${basePath}.${String(id)}`, finalUpdated, prev);
|
|
199
|
+
for (const handler of tapHandlers) {
|
|
200
|
+
handler.onUpdate?.(id, transformedChanges, finalUpdated);
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
updateMany(ids, changes) {
|
|
204
|
+
if (ids.length === 0) return;
|
|
205
|
+
const updatedEntities = [];
|
|
206
|
+
for (const id of ids) {
|
|
207
|
+
const entity = storage.get(id);
|
|
208
|
+
if (!entity) {
|
|
209
|
+
throw new Error(`Entity with id ${String(id)} not found`);
|
|
210
|
+
}
|
|
211
|
+
const prev = entity;
|
|
212
|
+
let transformedChanges = changes;
|
|
213
|
+
for (const handler of interceptHandlers) {
|
|
214
|
+
const ctx = {
|
|
215
|
+
block: reason => {
|
|
216
|
+
throw new Error(`Cannot update entity: ${reason || 'blocked by interceptor'}`);
|
|
217
|
+
},
|
|
218
|
+
transform: value => {
|
|
219
|
+
transformedChanges = value;
|
|
220
|
+
},
|
|
221
|
+
blocked: false,
|
|
222
|
+
blockReason: undefined
|
|
223
|
+
};
|
|
224
|
+
handler.onUpdate?.(id, changes, ctx);
|
|
225
|
+
}
|
|
226
|
+
const finalUpdated = {
|
|
227
|
+
...entity,
|
|
228
|
+
...transformedChanges
|
|
229
|
+
};
|
|
230
|
+
storage.set(id, finalUpdated);
|
|
231
|
+
nodeCache.delete(id);
|
|
232
|
+
updatedEntities.push({
|
|
233
|
+
id,
|
|
234
|
+
prev,
|
|
235
|
+
finalUpdated,
|
|
236
|
+
transformedChanges
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
updateSignals();
|
|
240
|
+
for (const {
|
|
216
241
|
id,
|
|
217
242
|
prev,
|
|
218
|
-
finalUpdated
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
this.updateSignals();
|
|
223
|
-
for (const {
|
|
224
|
-
id,
|
|
225
|
-
prev,
|
|
226
|
-
finalUpdated
|
|
227
|
-
} of updatedEntities) {
|
|
228
|
-
this.pathNotifier.notify(`${this.basePath}.${String(id)}`, finalUpdated, prev);
|
|
229
|
-
}
|
|
230
|
-
for (const {
|
|
231
|
-
id,
|
|
232
|
-
transformedChanges,
|
|
233
|
-
finalUpdated
|
|
234
|
-
} of updatedEntities) {
|
|
235
|
-
for (const handler of this.tapHandlers) {
|
|
236
|
-
handler.onUpdate?.(id, transformedChanges, finalUpdated);
|
|
243
|
+
finalUpdated
|
|
244
|
+
} of updatedEntities) {
|
|
245
|
+
pathNotifier.notify(`${basePath}.${String(id)}`, finalUpdated, prev);
|
|
237
246
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
247
|
+
for (const {
|
|
248
|
+
id,
|
|
249
|
+
transformedChanges,
|
|
250
|
+
finalUpdated
|
|
251
|
+
} of updatedEntities) {
|
|
252
|
+
for (const handler of tapHandlers) {
|
|
253
|
+
handler.onUpdate?.(id, transformedChanges, finalUpdated);
|
|
254
|
+
}
|
|
245
255
|
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
},
|
|
262
|
-
transform: () => {},
|
|
263
|
-
blocked: false,
|
|
264
|
-
blockReason: undefined
|
|
265
|
-
};
|
|
266
|
-
handler.onRemove?.(id, entity, ctx);
|
|
267
|
-
}
|
|
268
|
-
this.storage.delete(id);
|
|
269
|
-
this.nodeCache.delete(id);
|
|
270
|
-
this.updateSignals();
|
|
271
|
-
this.pathNotifier.notify(`${this.basePath}.${String(id)}`, undefined, entity);
|
|
272
|
-
for (const handler of this.tapHandlers) {
|
|
273
|
-
handler.onRemove?.(id, entity);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
removeMany(ids) {
|
|
277
|
-
if (ids.length === 0) return;
|
|
278
|
-
const entitiesToRemove = [];
|
|
279
|
-
for (const id of ids) {
|
|
280
|
-
const entity = this.storage.get(id);
|
|
256
|
+
},
|
|
257
|
+
updateWhere(predicate, changes) {
|
|
258
|
+
const idsToUpdate = [];
|
|
259
|
+
for (const [id, entity] of storage) {
|
|
260
|
+
if (predicate(entity)) {
|
|
261
|
+
idsToUpdate.push(id);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (idsToUpdate.length > 0) {
|
|
265
|
+
api.updateMany(idsToUpdate, changes);
|
|
266
|
+
}
|
|
267
|
+
return idsToUpdate.length;
|
|
268
|
+
},
|
|
269
|
+
removeOne(id) {
|
|
270
|
+
const entity = storage.get(id);
|
|
281
271
|
if (!entity) {
|
|
282
272
|
throw new Error(`Entity with id ${String(id)} not found`);
|
|
283
273
|
}
|
|
284
|
-
for (const handler of
|
|
274
|
+
for (const handler of interceptHandlers) {
|
|
285
275
|
const ctx = {
|
|
286
276
|
block: reason => {
|
|
287
277
|
throw new Error(`Cannot remove entity: ${reason || 'blocked by interceptor'}`);
|
|
@@ -292,267 +282,263 @@ class EntitySignalImpl {
|
|
|
292
282
|
};
|
|
293
283
|
handler.onRemove?.(id, entity, ctx);
|
|
294
284
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
for (const {
|
|
301
|
-
id
|
|
302
|
-
} of entitiesToRemove) {
|
|
303
|
-
this.storage.delete(id);
|
|
304
|
-
this.nodeCache.delete(id);
|
|
305
|
-
}
|
|
306
|
-
this.updateSignals();
|
|
307
|
-
for (const {
|
|
308
|
-
id,
|
|
309
|
-
entity
|
|
310
|
-
} of entitiesToRemove) {
|
|
311
|
-
this.pathNotifier.notify(`${this.basePath}.${String(id)}`, undefined, entity);
|
|
312
|
-
}
|
|
313
|
-
for (const {
|
|
314
|
-
id,
|
|
315
|
-
entity
|
|
316
|
-
} of entitiesToRemove) {
|
|
317
|
-
for (const handler of this.tapHandlers) {
|
|
285
|
+
storage.delete(id);
|
|
286
|
+
nodeCache.delete(id);
|
|
287
|
+
updateSignals();
|
|
288
|
+
pathNotifier.notify(`${basePath}.${String(id)}`, undefined, entity);
|
|
289
|
+
for (const handler of tapHandlers) {
|
|
318
290
|
handler.onRemove?.(id, entity);
|
|
319
291
|
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
return id;
|
|
342
|
-
}
|
|
343
|
-
upsertMany(entities, opts) {
|
|
344
|
-
if (entities.length === 0) return [];
|
|
345
|
-
const toAdd = [];
|
|
346
|
-
const toUpdate = [];
|
|
347
|
-
for (const entity of entities) {
|
|
348
|
-
const id = opts?.selectId?.(entity) ?? this.selectId(entity);
|
|
349
|
-
if (this.storage.has(id)) {
|
|
350
|
-
toUpdate.push({
|
|
351
|
-
entity,
|
|
292
|
+
},
|
|
293
|
+
removeMany(ids) {
|
|
294
|
+
if (ids.length === 0) return;
|
|
295
|
+
const entitiesToRemove = [];
|
|
296
|
+
for (const id of ids) {
|
|
297
|
+
const entity = storage.get(id);
|
|
298
|
+
if (!entity) {
|
|
299
|
+
throw new Error(`Entity with id ${String(id)} not found`);
|
|
300
|
+
}
|
|
301
|
+
for (const handler of interceptHandlers) {
|
|
302
|
+
const ctx = {
|
|
303
|
+
block: reason => {
|
|
304
|
+
throw new Error(`Cannot remove entity: ${reason || 'blocked by interceptor'}`);
|
|
305
|
+
},
|
|
306
|
+
transform: () => {},
|
|
307
|
+
blocked: false,
|
|
308
|
+
blockReason: undefined
|
|
309
|
+
};
|
|
310
|
+
handler.onRemove?.(id, entity, ctx);
|
|
311
|
+
}
|
|
312
|
+
entitiesToRemove.push({
|
|
352
313
|
id,
|
|
353
|
-
|
|
314
|
+
entity
|
|
354
315
|
});
|
|
316
|
+
}
|
|
317
|
+
for (const {
|
|
318
|
+
id
|
|
319
|
+
} of entitiesToRemove) {
|
|
320
|
+
storage.delete(id);
|
|
321
|
+
nodeCache.delete(id);
|
|
322
|
+
}
|
|
323
|
+
updateSignals();
|
|
324
|
+
for (const {
|
|
325
|
+
id,
|
|
326
|
+
entity
|
|
327
|
+
} of entitiesToRemove) {
|
|
328
|
+
pathNotifier.notify(`${basePath}.${String(id)}`, undefined, entity);
|
|
329
|
+
}
|
|
330
|
+
for (const {
|
|
331
|
+
id,
|
|
332
|
+
entity
|
|
333
|
+
} of entitiesToRemove) {
|
|
334
|
+
for (const handler of tapHandlers) {
|
|
335
|
+
handler.onRemove?.(id, entity);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
removeWhere(predicate) {
|
|
340
|
+
const idsToRemove = [];
|
|
341
|
+
for (const [id, entity] of storage) {
|
|
342
|
+
if (predicate(entity)) {
|
|
343
|
+
idsToRemove.push(id);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (idsToRemove.length > 0) {
|
|
347
|
+
api.removeMany(idsToRemove);
|
|
348
|
+
}
|
|
349
|
+
return idsToRemove.length;
|
|
350
|
+
},
|
|
351
|
+
upsertOne(entity, opts) {
|
|
352
|
+
const id = opts?.selectId?.(entity) ?? selectId(entity);
|
|
353
|
+
if (storage.has(id)) {
|
|
354
|
+
api.updateOne(id, entity);
|
|
355
355
|
} else {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
356
|
+
api.addOne(entity, opts);
|
|
357
|
+
}
|
|
358
|
+
return id;
|
|
359
|
+
},
|
|
360
|
+
upsertMany(entities, opts) {
|
|
361
|
+
if (entities.length === 0) return [];
|
|
362
|
+
const toAdd = [];
|
|
363
|
+
const toUpdate = [];
|
|
364
|
+
for (const entity of entities) {
|
|
365
|
+
const id = opts?.selectId?.(entity) ?? selectId(entity);
|
|
366
|
+
const existing = storage.get(id);
|
|
367
|
+
if (existing !== undefined) {
|
|
368
|
+
toUpdate.push({
|
|
369
|
+
entity,
|
|
370
|
+
id,
|
|
371
|
+
prev: existing
|
|
372
|
+
});
|
|
373
|
+
} else {
|
|
374
|
+
toAdd.push({
|
|
375
|
+
entity,
|
|
376
|
+
id
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const addedEntities = [];
|
|
381
|
+
for (const {
|
|
382
|
+
entity,
|
|
383
|
+
id
|
|
384
|
+
} of toAdd) {
|
|
385
|
+
let transformedEntity = entity;
|
|
386
|
+
for (const handler of interceptHandlers) {
|
|
387
|
+
const ctx = {
|
|
388
|
+
block: reason => {
|
|
389
|
+
throw new Error(`Cannot add entity: ${reason || 'blocked by interceptor'}`);
|
|
390
|
+
},
|
|
391
|
+
transform: value => {
|
|
392
|
+
transformedEntity = value;
|
|
393
|
+
},
|
|
394
|
+
blocked: false,
|
|
395
|
+
blockReason: undefined
|
|
396
|
+
};
|
|
397
|
+
handler.onAdd?.(entity, ctx);
|
|
398
|
+
}
|
|
399
|
+
storage.set(id, transformedEntity);
|
|
400
|
+
nodeCache.delete(id);
|
|
401
|
+
addedEntities.push({
|
|
402
|
+
id,
|
|
403
|
+
entity: transformedEntity
|
|
359
404
|
});
|
|
360
405
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
406
|
+
const updatedEntities = [];
|
|
407
|
+
for (const {
|
|
408
|
+
entity,
|
|
409
|
+
id,
|
|
410
|
+
prev
|
|
411
|
+
} of toUpdate) {
|
|
412
|
+
let transformedChanges = entity;
|
|
413
|
+
for (const handler of interceptHandlers) {
|
|
414
|
+
const ctx = {
|
|
415
|
+
block: reason => {
|
|
416
|
+
throw new Error(`Cannot update entity: ${reason || 'blocked by interceptor'}`);
|
|
417
|
+
},
|
|
418
|
+
transform: value => {
|
|
419
|
+
transformedChanges = value;
|
|
420
|
+
},
|
|
421
|
+
blocked: false,
|
|
422
|
+
blockReason: undefined
|
|
423
|
+
};
|
|
424
|
+
handler.onUpdate?.(id, entity, ctx);
|
|
425
|
+
}
|
|
426
|
+
const finalUpdated = {
|
|
427
|
+
...prev,
|
|
428
|
+
...transformedChanges
|
|
378
429
|
};
|
|
379
|
-
|
|
430
|
+
storage.set(id, finalUpdated);
|
|
431
|
+
nodeCache.delete(id);
|
|
432
|
+
updatedEntities.push({
|
|
433
|
+
id,
|
|
434
|
+
prev,
|
|
435
|
+
finalUpdated,
|
|
436
|
+
transformedChanges
|
|
437
|
+
});
|
|
380
438
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
addedEntities.push({
|
|
439
|
+
updateSignals();
|
|
440
|
+
for (const {
|
|
384
441
|
id,
|
|
385
|
-
entity
|
|
386
|
-
})
|
|
387
|
-
|
|
388
|
-
const updatedEntities = [];
|
|
389
|
-
for (const {
|
|
390
|
-
entity,
|
|
391
|
-
id,
|
|
392
|
-
prev
|
|
393
|
-
} of toUpdate) {
|
|
394
|
-
let transformedChanges = entity;
|
|
395
|
-
for (const handler of this.interceptHandlers) {
|
|
396
|
-
const ctx = {
|
|
397
|
-
block: reason => {
|
|
398
|
-
throw new Error(`Cannot update entity: ${reason || 'blocked by interceptor'}`);
|
|
399
|
-
},
|
|
400
|
-
transform: value => {
|
|
401
|
-
transformedChanges = value;
|
|
402
|
-
},
|
|
403
|
-
blocked: false,
|
|
404
|
-
blockReason: undefined
|
|
405
|
-
};
|
|
406
|
-
handler.onUpdate?.(id, entity, ctx);
|
|
442
|
+
entity
|
|
443
|
+
} of addedEntities) {
|
|
444
|
+
pathNotifier.notify(`${basePath}.${String(id)}`, entity, undefined);
|
|
407
445
|
}
|
|
408
|
-
const
|
|
409
|
-
...prev,
|
|
410
|
-
...transformedChanges
|
|
411
|
-
};
|
|
412
|
-
this.storage.set(id, finalUpdated);
|
|
413
|
-
this.nodeCache.delete(id);
|
|
414
|
-
updatedEntities.push({
|
|
446
|
+
for (const {
|
|
415
447
|
id,
|
|
416
448
|
prev,
|
|
417
|
-
finalUpdated
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
this.updateSignals();
|
|
422
|
-
for (const {
|
|
423
|
-
id,
|
|
424
|
-
entity
|
|
425
|
-
} of addedEntities) {
|
|
426
|
-
this.pathNotifier.notify(`${this.basePath}.${String(id)}`, entity, undefined);
|
|
427
|
-
}
|
|
428
|
-
for (const {
|
|
429
|
-
id,
|
|
430
|
-
prev,
|
|
431
|
-
finalUpdated
|
|
432
|
-
} of updatedEntities) {
|
|
433
|
-
this.pathNotifier.notify(`${this.basePath}.${String(id)}`, finalUpdated, prev);
|
|
434
|
-
}
|
|
435
|
-
for (const {
|
|
436
|
-
id,
|
|
437
|
-
entity
|
|
438
|
-
} of addedEntities) {
|
|
439
|
-
for (const handler of this.tapHandlers) {
|
|
440
|
-
handler.onAdd?.(entity, id);
|
|
449
|
+
finalUpdated
|
|
450
|
+
} of updatedEntities) {
|
|
451
|
+
pathNotifier.notify(`${basePath}.${String(id)}`, finalUpdated, prev);
|
|
441
452
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
handler.onUpdate?.(id, transformedChanges, finalUpdated);
|
|
453
|
+
for (const {
|
|
454
|
+
id,
|
|
455
|
+
entity
|
|
456
|
+
} of addedEntities) {
|
|
457
|
+
for (const handler of tapHandlers) {
|
|
458
|
+
handler.onAdd?.(entity, id);
|
|
459
|
+
}
|
|
450
460
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
removeAll() {
|
|
460
|
-
this.clear();
|
|
461
|
-
}
|
|
462
|
-
setAll(entities, opts) {
|
|
463
|
-
this.storage.clear();
|
|
464
|
-
this.nodeCache.clear();
|
|
465
|
-
const addedIds = [];
|
|
466
|
-
for (const entity of entities) {
|
|
467
|
-
const id = opts?.selectId?.(entity) ?? this.selectId(entity);
|
|
468
|
-
let transformedEntity = entity;
|
|
469
|
-
for (const handler of this.interceptHandlers) {
|
|
470
|
-
const ctx = {
|
|
471
|
-
block: reason => {
|
|
472
|
-
throw new Error(`Cannot add entity: ${reason || 'blocked by interceptor'}`);
|
|
473
|
-
},
|
|
474
|
-
transform: value => {
|
|
475
|
-
transformedEntity = value;
|
|
476
|
-
},
|
|
477
|
-
blocked: false,
|
|
478
|
-
blockReason: undefined
|
|
479
|
-
};
|
|
480
|
-
handler.onAdd?.(entity, ctx);
|
|
461
|
+
for (const {
|
|
462
|
+
id,
|
|
463
|
+
transformedChanges,
|
|
464
|
+
finalUpdated
|
|
465
|
+
} of updatedEntities) {
|
|
466
|
+
for (const handler of tapHandlers) {
|
|
467
|
+
handler.onUpdate?.(id, transformedChanges, finalUpdated);
|
|
468
|
+
}
|
|
481
469
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
470
|
+
return [...toAdd.map(a => a.id), ...toUpdate.map(u => u.id)];
|
|
471
|
+
},
|
|
472
|
+
clear() {
|
|
473
|
+
storage.clear();
|
|
474
|
+
nodeCache.clear();
|
|
475
|
+
updateSignals();
|
|
476
|
+
},
|
|
477
|
+
removeAll() {
|
|
478
|
+
api.clear();
|
|
479
|
+
},
|
|
480
|
+
setAll(entities, opts) {
|
|
481
|
+
storage.clear();
|
|
482
|
+
nodeCache.clear();
|
|
483
|
+
const addedIds = [];
|
|
484
|
+
for (const entity of entities) {
|
|
485
|
+
const id = opts?.selectId?.(entity) ?? selectId(entity);
|
|
486
|
+
let transformedEntity = entity;
|
|
487
|
+
for (const handler of interceptHandlers) {
|
|
488
|
+
const ctx = {
|
|
489
|
+
block: reason => {
|
|
490
|
+
throw new Error(`Cannot add entity: ${reason || 'blocked by interceptor'}`);
|
|
491
|
+
},
|
|
492
|
+
transform: value => {
|
|
493
|
+
transformedEntity = value;
|
|
494
|
+
},
|
|
495
|
+
blocked: false,
|
|
496
|
+
blockReason: undefined
|
|
497
|
+
};
|
|
498
|
+
handler.onAdd?.(entity, ctx);
|
|
499
|
+
}
|
|
500
|
+
storage.set(id, transformedEntity);
|
|
501
|
+
addedIds.push(id);
|
|
496
502
|
}
|
|
503
|
+
updateSignals();
|
|
504
|
+
for (let i = 0; i < addedIds.length; i++) {
|
|
505
|
+
const id = addedIds[i];
|
|
506
|
+
const entity = storage.get(id);
|
|
507
|
+
pathNotifier.notify(`${basePath}.${String(id)}`, entity, undefined);
|
|
508
|
+
}
|
|
509
|
+
for (let i = 0; i < addedIds.length; i++) {
|
|
510
|
+
const id = addedIds[i];
|
|
511
|
+
const entity = storage.get(id);
|
|
512
|
+
if (entity) {
|
|
513
|
+
for (const handler of tapHandlers) {
|
|
514
|
+
handler.onAdd?.(entity, id);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
},
|
|
519
|
+
tap(handlers) {
|
|
520
|
+
tapHandlers.push(handlers);
|
|
521
|
+
return () => {
|
|
522
|
+
const idx = tapHandlers.indexOf(handlers);
|
|
523
|
+
if (idx > -1) tapHandlers.splice(idx, 1);
|
|
524
|
+
};
|
|
525
|
+
},
|
|
526
|
+
intercept(handlers) {
|
|
527
|
+
interceptHandlers.push(handlers);
|
|
528
|
+
return () => {
|
|
529
|
+
const idx = interceptHandlers.indexOf(handlers);
|
|
530
|
+
if (idx > -1) interceptHandlers.splice(idx, 1);
|
|
531
|
+
};
|
|
497
532
|
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
this.tapHandlers.push(handlers);
|
|
501
|
-
return () => {
|
|
502
|
-
const idx = this.tapHandlers.indexOf(handlers);
|
|
503
|
-
if (idx > -1) this.tapHandlers.splice(idx, 1);
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
intercept(handlers) {
|
|
507
|
-
this.interceptHandlers.push(handlers);
|
|
508
|
-
return () => {
|
|
509
|
-
const idx = this.interceptHandlers.indexOf(handlers);
|
|
510
|
-
if (idx > -1) this.interceptHandlers.splice(idx, 1);
|
|
511
|
-
};
|
|
512
|
-
}
|
|
513
|
-
updateSignals() {
|
|
514
|
-
const entities = Array.from(this.storage.values());
|
|
515
|
-
const ids = Array.from(this.storage.keys());
|
|
516
|
-
const map = new Map(this.storage);
|
|
517
|
-
this.allSignal.set(entities);
|
|
518
|
-
this.countSignal.set(entities.length);
|
|
519
|
-
this.idsSignal.set(ids);
|
|
520
|
-
this.mapSignal.set(map);
|
|
521
|
-
}
|
|
522
|
-
getOrCreateNode(id, entity) {
|
|
523
|
-
let node = this.nodeCache.get(id);
|
|
524
|
-
if (!node) {
|
|
525
|
-
node = this.createEntityNode(id, entity);
|
|
526
|
-
this.nodeCache.set(id, node);
|
|
527
|
-
}
|
|
528
|
-
return node;
|
|
529
|
-
}
|
|
530
|
-
createEntityNode(id, entity) {
|
|
531
|
-
const node = () => this.storage.get(id);
|
|
532
|
-
for (const key of Object.keys(entity)) {
|
|
533
|
-
Object.defineProperty(node, key, {
|
|
534
|
-
get: () => {
|
|
535
|
-
const current = this.storage.get(id);
|
|
536
|
-
const value = current?.[key];
|
|
537
|
-
return () => value;
|
|
538
|
-
},
|
|
539
|
-
enumerable: true,
|
|
540
|
-
configurable: true
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
return node;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
function createEntitySignal(config, pathNotifier, basePath) {
|
|
547
|
-
const impl = new EntitySignalImpl(config, pathNotifier, basePath);
|
|
548
|
-
return new Proxy(impl, {
|
|
533
|
+
};
|
|
534
|
+
return new Proxy(api, {
|
|
549
535
|
get: (target, prop) => {
|
|
550
536
|
if (typeof prop === 'string' && !isNaN(Number(prop))) {
|
|
551
|
-
return
|
|
537
|
+
return api.byId(Number(prop));
|
|
552
538
|
}
|
|
553
539
|
return target[prop];
|
|
554
540
|
}
|
|
555
541
|
});
|
|
556
542
|
}
|
|
557
543
|
|
|
558
|
-
export {
|
|
544
|
+
export { createEntitySignal };
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
class PathNotifier {
|
|
2
2
|
subscribers = new Map();
|
|
3
3
|
interceptors = new Map();
|
|
4
|
+
batchingEnabled = true;
|
|
5
|
+
pendingFlush = false;
|
|
6
|
+
pending = new Map();
|
|
7
|
+
firstValues = new Map();
|
|
8
|
+
flushCallbacks = new Set();
|
|
9
|
+
constructor(options) {
|
|
10
|
+
if (options && options.batching === false) this.batchingEnabled = false;
|
|
11
|
+
}
|
|
12
|
+
setBatchingEnabled(enabled) {
|
|
13
|
+
this.batchingEnabled = enabled;
|
|
14
|
+
}
|
|
15
|
+
isBatchingEnabled() {
|
|
16
|
+
return this.batchingEnabled;
|
|
17
|
+
}
|
|
4
18
|
subscribe(pattern, handler) {
|
|
5
19
|
if (!this.subscribers.has(pattern)) {
|
|
6
20
|
this.subscribers.set(pattern, new Set());
|
|
@@ -34,6 +48,26 @@ class PathNotifier {
|
|
|
34
48
|
};
|
|
35
49
|
}
|
|
36
50
|
notify(path, value, prev) {
|
|
51
|
+
if (!this.batchingEnabled) {
|
|
52
|
+
return this._runNotify(path, value, prev);
|
|
53
|
+
}
|
|
54
|
+
if (!this.pending.has(path)) {
|
|
55
|
+
this.firstValues.set(path, prev);
|
|
56
|
+
}
|
|
57
|
+
this.pending.set(path, {
|
|
58
|
+
newValue: value,
|
|
59
|
+
oldValue: this.firstValues.get(path)
|
|
60
|
+
});
|
|
61
|
+
if (!this.pendingFlush) {
|
|
62
|
+
this.pendingFlush = true;
|
|
63
|
+
queueMicrotask(() => this.flush());
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
blocked: false,
|
|
67
|
+
value
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
_runNotify(path, value, prev) {
|
|
37
71
|
let blocked = false;
|
|
38
72
|
let transformed = value;
|
|
39
73
|
for (const [pattern, interceptorSet] of this.interceptors) {
|
|
@@ -67,6 +101,41 @@ class PathNotifier {
|
|
|
67
101
|
value: transformed
|
|
68
102
|
};
|
|
69
103
|
}
|
|
104
|
+
flush() {
|
|
105
|
+
const toNotify = new Map(this.pending);
|
|
106
|
+
this.pending.clear();
|
|
107
|
+
this.firstValues.clear();
|
|
108
|
+
this.pendingFlush = false;
|
|
109
|
+
for (const [path, {
|
|
110
|
+
newValue,
|
|
111
|
+
oldValue
|
|
112
|
+
}] of toNotify) {
|
|
113
|
+
if (newValue === oldValue) continue;
|
|
114
|
+
const res = this._runNotify(path, newValue, oldValue);
|
|
115
|
+
if (res.blocked) ;
|
|
116
|
+
}
|
|
117
|
+
for (const cb of Array.from(this.flushCallbacks)) {
|
|
118
|
+
try {
|
|
119
|
+
cb();
|
|
120
|
+
} catch {}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
flushSync() {
|
|
124
|
+
while (this.pending.size > 0 || this.pendingFlush) {
|
|
125
|
+
if (this.pendingFlush && this.pending.size === 0) {
|
|
126
|
+
this.pendingFlush = false;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
this.flush();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
onFlush(callback) {
|
|
133
|
+
this.flushCallbacks.add(callback);
|
|
134
|
+
return () => this.flushCallbacks.delete(callback);
|
|
135
|
+
}
|
|
136
|
+
hasPending() {
|
|
137
|
+
return this.pending.size > 0;
|
|
138
|
+
}
|
|
70
139
|
matches(pattern, path) {
|
|
71
140
|
if (pattern === '**') return true;
|
|
72
141
|
if (pattern === path) return true;
|
|
@@ -79,6 +148,9 @@ class PathNotifier {
|
|
|
79
148
|
clear() {
|
|
80
149
|
this.subscribers.clear();
|
|
81
150
|
this.interceptors.clear();
|
|
151
|
+
this.pending.clear();
|
|
152
|
+
this.firstValues.clear();
|
|
153
|
+
this.pendingFlush = false;
|
|
82
154
|
}
|
|
83
155
|
getSubscriberCount() {
|
|
84
156
|
let count = 0;
|
package/dist/lib/signal-tree.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { signal, isSignal } from '@angular/core';
|
|
2
2
|
import { SIGNAL_TREE_MESSAGES, SIGNAL_TREE_CONSTANTS } from './constants.js';
|
|
3
3
|
import { SignalMemoryManager } from './memory/memory-manager.js';
|
|
4
|
+
import { getPathNotifier } from './path-notifier.js';
|
|
4
5
|
import { SecurityValidator } from './security/security-validator.js';
|
|
5
6
|
import { createLazySignalTree, unwrap } from './utils.js';
|
|
6
7
|
import { deepEqual } from '../deep-equal.js';
|
|
@@ -172,6 +173,9 @@ function create(initialState, config) {
|
|
|
172
173
|
const useLazy = shouldUseLazy(initialState, config, estimatedSize);
|
|
173
174
|
let signalState;
|
|
174
175
|
let memoryManager;
|
|
176
|
+
try {
|
|
177
|
+
getPathNotifier().setBatchingEnabled(Boolean(config.batchUpdates !== false));
|
|
178
|
+
} catch {}
|
|
175
179
|
if (useLazy && typeof initialState === 'object') {
|
|
176
180
|
try {
|
|
177
181
|
memoryManager = new SignalMemoryManager();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signaltree/core",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.3.0",
|
|
4
4
|
"description": "Lightweight, type-safe signal-based state management for Angular. Core package providing hierarchical signal trees, basic entity management, and async actions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|