@signaltree/enterprise 4.0.6 → 4.0.9
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/LICENSE +54 -0
- package/README.md +203 -0
- package/package.json +1 -1
- package/src/lib/diff-engine.spec.ts +384 -0
- package/src/lib/diff-engine.ts +351 -0
- package/src/lib/enterprise-enhancer.ts +136 -0
- package/src/lib/enterprise.spec.ts +7 -0
- package/src/lib/enterprise.ts +3 -0
- package/src/lib/path-index.spec.ts +290 -0
- package/src/lib/path-index.ts +320 -0
- package/src/lib/scheduler.ts +16 -0
- package/src/lib/thread-pools.ts +11 -0
- package/src/lib/update-engine.spec.ts +93 -0
- package/src/lib/update-engine.ts +399 -0
- package/src/test-setup.ts +6 -0
- package/src/types/signaltree-core.d.ts +4 -0
- package/src/index.js +0 -10
- package/src/index.js.map +0 -1
- package/src/lib/diff-engine.d.ts +0 -108
- package/src/lib/diff-engine.js +0 -236
- package/src/lib/diff-engine.js.map +0 -1
- package/src/lib/enterprise-enhancer.d.ts +0 -81
- package/src/lib/enterprise-enhancer.js +0 -78
- package/src/lib/enterprise-enhancer.js.map +0 -1
- package/src/lib/enterprise.d.ts +0 -1
- package/src/lib/enterprise.js +0 -7
- package/src/lib/enterprise.js.map +0 -1
- package/src/lib/path-index.d.ts +0 -119
- package/src/lib/path-index.js +0 -265
- package/src/lib/path-index.js.map +0 -1
- package/src/lib/scheduler.d.ts +0 -2
- package/src/lib/scheduler.js +0 -25
- package/src/lib/scheduler.js.map +0 -1
- package/src/lib/thread-pools.d.ts +0 -4
- package/src/lib/thread-pools.js +0 -14
- package/src/lib/thread-pools.js.map +0 -1
- package/src/lib/update-engine.d.ts +0 -115
- package/src/lib/update-engine.js +0 -287
- package/src/lib/update-engine.js.map +0 -1
- package/src/test-setup.d.ts +0 -1
- package/src/test-setup.js +0 -8
- package/src/test-setup.js.map +0 -1
- /package/src/{index.d.ts → index.ts} +0 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { signal } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
import { PathIndex } from './path-index';
|
|
4
|
+
|
|
5
|
+
import type { WritableSignal } from "@angular/core";
|
|
6
|
+
|
|
7
|
+
describe("PathIndex", () => {
|
|
8
|
+
let index: PathIndex;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
index = new PathIndex();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("set and get", () => {
|
|
15
|
+
it("should store and retrieve values by path", () => {
|
|
16
|
+
const testSignal = signal(42);
|
|
17
|
+
index.set(["user", "age"], testSignal);
|
|
18
|
+
|
|
19
|
+
const retrieved = index.get(["user", "age"]);
|
|
20
|
+
expect(retrieved).toBe(testSignal);
|
|
21
|
+
expect(retrieved?.()).toBe(42);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should handle nested paths", () => {
|
|
25
|
+
const nameSignal = signal("Alice");
|
|
26
|
+
const ageSignal = signal(30);
|
|
27
|
+
const citySignal = signal("NYC");
|
|
28
|
+
|
|
29
|
+
index.set(["user", "profile", "name"], nameSignal);
|
|
30
|
+
index.set(["user", "profile", "age"], ageSignal);
|
|
31
|
+
index.set(["user", "address", "city"], citySignal);
|
|
32
|
+
|
|
33
|
+
expect(index.get(["user", "profile", "name"])).toBe(nameSignal);
|
|
34
|
+
expect(index.get(["user", "profile", "age"])).toBe(ageSignal);
|
|
35
|
+
expect(index.get(["user", "address", "city"])).toBe(citySignal);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should handle array indices", () => {
|
|
39
|
+
const sig = signal("item");
|
|
40
|
+
index.set(["items", 0, "name"], sig);
|
|
41
|
+
|
|
42
|
+
const retrieved = index.get(["items", 0, "name"]);
|
|
43
|
+
expect(retrieved).toBe(sig);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should return null for non-existent paths", () => {
|
|
47
|
+
expect(index.get(["nonexistent", "path"])).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should handle empty paths", () => {
|
|
51
|
+
const sig = signal("root");
|
|
52
|
+
index.set([], sig);
|
|
53
|
+
|
|
54
|
+
expect(index.get([])).toBe(sig);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("has", () => {
|
|
59
|
+
it("should return true for existing paths", () => {
|
|
60
|
+
const sig = signal(1);
|
|
61
|
+
index.set(["test"], sig);
|
|
62
|
+
|
|
63
|
+
expect(index.has(["test"])).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should return false for non-existent paths", () => {
|
|
67
|
+
expect(index.has(["nonexistent"])).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("getByPrefix", () => {
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
index.set(["user", "name"], signal("Alice"));
|
|
74
|
+
index.set(["user", "age"], signal(30));
|
|
75
|
+
index.set(["user", "profile", "bio"], signal("Dev"));
|
|
76
|
+
index.set(["config", "theme"], signal("dark"));
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should get all values matching a prefix", () => {
|
|
80
|
+
const userValues = index.getByPrefix(["user"]);
|
|
81
|
+
|
|
82
|
+
expect(userValues.size).toBe(3);
|
|
83
|
+
expect(userValues.get("name")?.()).toBe("Alice");
|
|
84
|
+
expect(userValues.get("age")?.()).toBe(30);
|
|
85
|
+
expect(userValues.get("profile.bio")?.()).toBe("Dev");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should return empty map for non-existent prefix", () => {
|
|
89
|
+
const values = index.getByPrefix(["nonexistent"]);
|
|
90
|
+
expect(values.size).toBe(0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should handle nested prefixes", () => {
|
|
94
|
+
const profileValues = index.getByPrefix(["user", "profile"]);
|
|
95
|
+
|
|
96
|
+
expect(profileValues.size).toBe(1);
|
|
97
|
+
expect(profileValues.get("bio")?.()).toBe("Dev");
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("delete", () => {
|
|
102
|
+
it("should delete a value and return true", () => {
|
|
103
|
+
const sig = signal(1);
|
|
104
|
+
index.set(["test"], sig);
|
|
105
|
+
|
|
106
|
+
expect(index.delete(["test"])).toBe(true);
|
|
107
|
+
expect(index.has(["test"])).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should return false for non-existent paths", () => {
|
|
111
|
+
expect(index.delete(["nonexistent"])).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should clean up empty nodes", () => {
|
|
115
|
+
index.set(["user", "name"], signal("Alice"));
|
|
116
|
+
index.set(["user", "age"], signal(30));
|
|
117
|
+
|
|
118
|
+
index.delete(["user", "name"]);
|
|
119
|
+
|
|
120
|
+
// Age should still be accessible
|
|
121
|
+
expect(index.has(["user", "age"])).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("clear", () => {
|
|
126
|
+
it("should remove all entries", () => {
|
|
127
|
+
index.set(["a"], signal(1));
|
|
128
|
+
index.set(["b"], signal(2));
|
|
129
|
+
index.set(["c"], signal(3));
|
|
130
|
+
|
|
131
|
+
index.clear();
|
|
132
|
+
|
|
133
|
+
expect(index.has(["a"])).toBe(false);
|
|
134
|
+
expect(index.has(["b"])).toBe(false);
|
|
135
|
+
expect(index.has(["c"])).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("getStats", () => {
|
|
140
|
+
it("should track hits and misses", () => {
|
|
141
|
+
const sig = signal(1);
|
|
142
|
+
index.set(["test"], sig);
|
|
143
|
+
|
|
144
|
+
// Trigger a hit
|
|
145
|
+
index.get(["test"]);
|
|
146
|
+
|
|
147
|
+
// Trigger a miss
|
|
148
|
+
index.get(["nonexistent"]);
|
|
149
|
+
|
|
150
|
+
const stats = index.getStats();
|
|
151
|
+
expect(stats.hits).toBeGreaterThan(0);
|
|
152
|
+
expect(stats.misses).toBeGreaterThan(0);
|
|
153
|
+
expect(stats.hitRate).toBeGreaterThan(0);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("should track sets", () => {
|
|
157
|
+
index.set(["a"], signal(1));
|
|
158
|
+
index.set(["b"], signal(2));
|
|
159
|
+
|
|
160
|
+
const stats = index.getStats();
|
|
161
|
+
expect(stats.sets).toBe(2);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("buildFromTree", () => {
|
|
166
|
+
it("should index signals from a tree structure", () => {
|
|
167
|
+
const tree = {
|
|
168
|
+
user: {
|
|
169
|
+
name: signal("Alice"),
|
|
170
|
+
age: signal(30),
|
|
171
|
+
profile: {
|
|
172
|
+
bio: signal("Developer"),
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
config: signal("dark"),
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
index.buildFromTree(tree);
|
|
179
|
+
|
|
180
|
+
expect(index.has(["user", "name"])).toBe(true);
|
|
181
|
+
expect(index.has(["user", "age"])).toBe(true);
|
|
182
|
+
expect(index.has(["user", "profile", "bio"])).toBe(true);
|
|
183
|
+
expect(index.has(["config"])).toBe(true);
|
|
184
|
+
|
|
185
|
+
expect(index.get(["user", "name"])?.()).toBe("Alice");
|
|
186
|
+
expect(index.get(["config"])?.()).toBe("dark");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("should handle nested objects without signals", () => {
|
|
190
|
+
const tree = {
|
|
191
|
+
data: {
|
|
192
|
+
nested: {
|
|
193
|
+
value: signal(42),
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
index.buildFromTree(tree);
|
|
199
|
+
|
|
200
|
+
expect(index.has(["data", "nested", "value"])).toBe(true);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("should skip non-signal leaves", () => {
|
|
204
|
+
const tree = {
|
|
205
|
+
signal: signal(1),
|
|
206
|
+
notSignal: "plain value",
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
index.buildFromTree(tree);
|
|
210
|
+
|
|
211
|
+
expect(index.has(["signal"])).toBe(true);
|
|
212
|
+
expect(index.has(["notSignal"])).toBe(false);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("WeakRef behavior", () => {
|
|
217
|
+
it("should allow garbage collection of signals", () => {
|
|
218
|
+
const sig: WritableSignal<number> = signal(42);
|
|
219
|
+
index.set(["test"], sig);
|
|
220
|
+
|
|
221
|
+
// Signal should be retrievable
|
|
222
|
+
expect(index.get(["test"])).toBe(sig);
|
|
223
|
+
|
|
224
|
+
// Note: We can't actually trigger GC in tests, but the WeakRef
|
|
225
|
+
// allows it to happen in production
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("should clean up stale references on access", () => {
|
|
229
|
+
const sig: WritableSignal<number> = signal(42);
|
|
230
|
+
index.set(["test"], sig);
|
|
231
|
+
|
|
232
|
+
const stats1 = index.getStats();
|
|
233
|
+
const initialSets = stats1.sets;
|
|
234
|
+
|
|
235
|
+
// Access should maintain cache
|
|
236
|
+
index.get(["test"]);
|
|
237
|
+
|
|
238
|
+
const stats2 = index.getStats();
|
|
239
|
+
expect(stats2.sets).toBe(initialSets);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe("performance", () => {
|
|
244
|
+
it("should handle large numbers of paths efficiently", () => {
|
|
245
|
+
const start = performance.now();
|
|
246
|
+
|
|
247
|
+
// Index 1000 signals
|
|
248
|
+
for (let i = 0; i < 1000; i++) {
|
|
249
|
+
index.set(["items", i], signal(i));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const indexTime = performance.now() - start;
|
|
253
|
+
|
|
254
|
+
// Retrieval should be fast
|
|
255
|
+
const retrieveStart = performance.now();
|
|
256
|
+
for (let i = 0; i < 1000; i++) {
|
|
257
|
+
index.get(["items", i]);
|
|
258
|
+
}
|
|
259
|
+
const retrieveTime = performance.now() - retrieveStart;
|
|
260
|
+
|
|
261
|
+
// Both operations should be reasonably fast
|
|
262
|
+
expect(indexTime).toBeLessThan(100); // Indexing 1000 items < 100ms
|
|
263
|
+
expect(retrieveTime).toBeLessThan(50); // Retrieving 1000 items < 50ms
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("should have O(k) lookup time regardless of total size", () => {
|
|
267
|
+
// Add 100 signals
|
|
268
|
+
for (let i = 0; i < 100; i++) {
|
|
269
|
+
index.set(["items", i], signal(i));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const time100 = performance.now();
|
|
273
|
+
index.get(["items", 50]);
|
|
274
|
+
const duration100 = performance.now() - time100;
|
|
275
|
+
|
|
276
|
+
// Add 900 more signals (10x more data)
|
|
277
|
+
for (let i = 100; i < 1000; i++) {
|
|
278
|
+
index.set(["items", i], signal(i));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const time1000 = performance.now();
|
|
282
|
+
index.get(["items", 50]);
|
|
283
|
+
const duration1000 = performance.now() - time1000;
|
|
284
|
+
|
|
285
|
+
// Lookup time should not increase significantly with more data
|
|
286
|
+
// (allowing for some variance in measurement)
|
|
287
|
+
expect(duration1000).toBeLessThan(duration100 * 3);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
});
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { isSignal } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PathIndex - Fast signal lookup using Trie data structure
|
|
5
|
+
* @packageDocumentation
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { WritableSignal } from '@angular/core';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Path segment (string or number for array indices)
|
|
12
|
+
*/
|
|
13
|
+
export type PathSegment = string | number;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Path as array of segments
|
|
17
|
+
*/
|
|
18
|
+
export type Path = PathSegment[];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Node in the Trie structure
|
|
22
|
+
*/
|
|
23
|
+
class TrieNode<T> {
|
|
24
|
+
value: T | null = null;
|
|
25
|
+
children = new Map<string, TrieNode<T>>();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* PathIndex
|
|
30
|
+
*
|
|
31
|
+
* Fast signal lookup using a Trie (prefix tree) data structure.
|
|
32
|
+
* Provides O(k) lookup time where k is the path length, regardless of total signals.
|
|
33
|
+
*
|
|
34
|
+
* Features:
|
|
35
|
+
* - Trie-based indexing for O(k) lookup
|
|
36
|
+
* - WeakRef caching for memory efficiency
|
|
37
|
+
* - Automatic cleanup of stale references
|
|
38
|
+
* - Prefix matching for batch operations
|
|
39
|
+
* - Path normalization
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* const index = new PathIndex();
|
|
44
|
+
*
|
|
45
|
+
* // Index signals
|
|
46
|
+
* index.set(['user', 'name'], nameSignal);
|
|
47
|
+
* index.set(['user', 'email'], emailSignal);
|
|
48
|
+
*
|
|
49
|
+
* // Fast lookup
|
|
50
|
+
* const signal = index.get(['user', 'name']);
|
|
51
|
+
*
|
|
52
|
+
* // Prefix matching
|
|
53
|
+
* const userSignals = index.getByPrefix(['user']);
|
|
54
|
+
* // Returns: { name: nameSignal, email: emailSignal }
|
|
55
|
+
*
|
|
56
|
+
* // Check if path exists
|
|
57
|
+
* if (index.has(['user', 'name'])) {
|
|
58
|
+
* // ...
|
|
59
|
+
* }
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
63
|
+
export class PathIndex<T extends object = WritableSignal<any>> {
|
|
64
|
+
private root = new TrieNode<WeakRef<T>>();
|
|
65
|
+
private pathCache = new Map<string, WeakRef<T>>();
|
|
66
|
+
private stats = {
|
|
67
|
+
hits: 0,
|
|
68
|
+
misses: 0,
|
|
69
|
+
sets: 0,
|
|
70
|
+
cleanups: 0,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Set a value at the given path
|
|
75
|
+
*
|
|
76
|
+
* @param path - Path segments
|
|
77
|
+
* @param value - Value to store
|
|
78
|
+
*/
|
|
79
|
+
set(path: Path, signal: T): void {
|
|
80
|
+
const pathStr = this.pathToString(path);
|
|
81
|
+
const ref = new WeakRef(signal);
|
|
82
|
+
|
|
83
|
+
// Update trie
|
|
84
|
+
let node = this.root;
|
|
85
|
+
for (const segment of path) {
|
|
86
|
+
const key = String(segment);
|
|
87
|
+
if (!node.children.has(key)) {
|
|
88
|
+
node.children.set(key, new TrieNode<WeakRef<T>>());
|
|
89
|
+
}
|
|
90
|
+
const nextNode = node.children.get(key);
|
|
91
|
+
if (!nextNode) {
|
|
92
|
+
throw new Error(`Failed to get node for key: ${key}`);
|
|
93
|
+
}
|
|
94
|
+
node = nextNode;
|
|
95
|
+
}
|
|
96
|
+
node.value = ref;
|
|
97
|
+
|
|
98
|
+
// Update cache for fast string lookups
|
|
99
|
+
this.pathCache.set(pathStr, ref);
|
|
100
|
+
|
|
101
|
+
this.stats.sets++;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get value at the given path
|
|
106
|
+
*
|
|
107
|
+
* @param path - Path segments
|
|
108
|
+
* @returns Value if found and not GC'd, null otherwise
|
|
109
|
+
*/
|
|
110
|
+
get(path: Path): T | null {
|
|
111
|
+
const pathStr = this.pathToString(path);
|
|
112
|
+
|
|
113
|
+
// Try cache first
|
|
114
|
+
const cached = this.pathCache.get(pathStr);
|
|
115
|
+
if (cached) {
|
|
116
|
+
const value = cached.deref();
|
|
117
|
+
if (value) {
|
|
118
|
+
this.stats.hits++;
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
// Clean up dead reference
|
|
122
|
+
this.pathCache.delete(pathStr);
|
|
123
|
+
this.stats.cleanups++;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Try trie
|
|
127
|
+
let node: TrieNode<WeakRef<T>> | undefined = this.root;
|
|
128
|
+
for (const segment of path) {
|
|
129
|
+
const key = String(segment);
|
|
130
|
+
node = node.children.get(key);
|
|
131
|
+
if (!node) {
|
|
132
|
+
this.stats.misses++;
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (node.value) {
|
|
138
|
+
const value = node.value.deref();
|
|
139
|
+
if (value) {
|
|
140
|
+
// Re-cache for next time
|
|
141
|
+
this.pathCache.set(pathStr, node.value);
|
|
142
|
+
this.stats.hits++;
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
// Clean up dead reference
|
|
146
|
+
node.value = null;
|
|
147
|
+
this.stats.cleanups++;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this.stats.misses++;
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Check if path exists in index
|
|
156
|
+
*
|
|
157
|
+
* @param path - Path segments
|
|
158
|
+
* @returns True if path exists and value is not GC'd
|
|
159
|
+
*/
|
|
160
|
+
has(path: Path): boolean {
|
|
161
|
+
return this.get(path) !== null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get all values matching a prefix
|
|
166
|
+
*
|
|
167
|
+
* @param prefix - Path prefix
|
|
168
|
+
* @returns Map of relative paths to values
|
|
169
|
+
*/
|
|
170
|
+
getByPrefix(prefix: Path): Map<string, T> {
|
|
171
|
+
const results = new Map<string, T>();
|
|
172
|
+
|
|
173
|
+
// Find the node at prefix
|
|
174
|
+
let node: TrieNode<WeakRef<T>> | undefined = this.root;
|
|
175
|
+
for (const segment of prefix) {
|
|
176
|
+
const key = String(segment);
|
|
177
|
+
node = node.children.get(key);
|
|
178
|
+
if (!node) {
|
|
179
|
+
return results; // Empty map
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Collect all descendants
|
|
184
|
+
this.collectDescendants(node, [], results);
|
|
185
|
+
|
|
186
|
+
return results;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Delete value at path
|
|
191
|
+
*
|
|
192
|
+
* @param path - Path segments
|
|
193
|
+
* @returns True if deleted, false if not found
|
|
194
|
+
*/
|
|
195
|
+
delete(path: Path): boolean {
|
|
196
|
+
const pathStr = this.pathToString(path);
|
|
197
|
+
|
|
198
|
+
// Remove from cache
|
|
199
|
+
this.pathCache.delete(pathStr);
|
|
200
|
+
|
|
201
|
+
// Remove from trie
|
|
202
|
+
let node: TrieNode<WeakRef<T>> | undefined = this.root;
|
|
203
|
+
const nodes: TrieNode<WeakRef<T>>[] = [node];
|
|
204
|
+
|
|
205
|
+
for (const segment of path) {
|
|
206
|
+
const key = String(segment);
|
|
207
|
+
node = node.children.get(key);
|
|
208
|
+
if (!node) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
nodes.push(node);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Clear value
|
|
215
|
+
const hadValue = node.value !== null;
|
|
216
|
+
node.value = null;
|
|
217
|
+
|
|
218
|
+
// Clean up empty nodes (from leaf to root)
|
|
219
|
+
for (let i = nodes.length - 1; i > 0; i--) {
|
|
220
|
+
const current = nodes[i];
|
|
221
|
+
if (current.value === null && current.children.size === 0) {
|
|
222
|
+
const parent = nodes[i - 1];
|
|
223
|
+
const segment = path[i - 1];
|
|
224
|
+
parent.children.delete(String(segment));
|
|
225
|
+
} else {
|
|
226
|
+
break; // Stop if node has value or children
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return hadValue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Clear all entries
|
|
235
|
+
*/
|
|
236
|
+
clear(): void {
|
|
237
|
+
this.root = new TrieNode<WeakRef<T>>();
|
|
238
|
+
this.pathCache.clear();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Get statistics
|
|
243
|
+
*
|
|
244
|
+
* @returns Index statistics
|
|
245
|
+
*/
|
|
246
|
+
getStats(): {
|
|
247
|
+
hits: number;
|
|
248
|
+
misses: number;
|
|
249
|
+
sets: number;
|
|
250
|
+
cleanups: number;
|
|
251
|
+
hitRate: number;
|
|
252
|
+
cacheSize: number;
|
|
253
|
+
} {
|
|
254
|
+
const total = this.stats.hits + this.stats.misses;
|
|
255
|
+
const hitRate = total > 0 ? this.stats.hits / total : 0;
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
...this.stats,
|
|
259
|
+
hitRate,
|
|
260
|
+
cacheSize: this.pathCache.size,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Build index from a tree structure
|
|
266
|
+
*
|
|
267
|
+
* @param tree - Tree object to index
|
|
268
|
+
* @param path - Current path (for recursion)
|
|
269
|
+
*/
|
|
270
|
+
buildFromTree(tree: unknown, path: Path = []): void {
|
|
271
|
+
if (!tree) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Check if it's a signal using Angular's isSignal
|
|
276
|
+
if (isSignal(tree)) {
|
|
277
|
+
this.set(path, tree as T);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Only continue if it's an object (not a signal or primitive)
|
|
282
|
+
if (typeof tree !== 'object') {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Recursively index children
|
|
287
|
+
for (const [key, value] of Object.entries(tree)) {
|
|
288
|
+
this.buildFromTree(value, [...path, key]);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Convert path to string for caching
|
|
294
|
+
*/
|
|
295
|
+
private pathToString(path: Path): string {
|
|
296
|
+
return path.join('.');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Collect all descendant values recursively
|
|
301
|
+
*/
|
|
302
|
+
private collectDescendants(
|
|
303
|
+
node: TrieNode<WeakRef<T>>,
|
|
304
|
+
currentPath: PathSegment[],
|
|
305
|
+
results: Map<string, T>
|
|
306
|
+
): void {
|
|
307
|
+
// Add current node's value if it exists
|
|
308
|
+
if (node.value) {
|
|
309
|
+
const value = node.value.deref();
|
|
310
|
+
if (value) {
|
|
311
|
+
results.set(this.pathToString(currentPath), value);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Recursively collect children
|
|
316
|
+
for (const [key, child] of node.children) {
|
|
317
|
+
this.collectDescendants(child, [...currentPath, key], results);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type Task = () => void;
|
|
2
|
+
|
|
3
|
+
const q: Task[] = [];
|
|
4
|
+
|
|
5
|
+
export function postTask(t: Task) {
|
|
6
|
+
q.push(t);
|
|
7
|
+
if (q.length === 1) flush();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function flush() {
|
|
11
|
+
while (q.length) {
|
|
12
|
+
const t = q.shift()!;
|
|
13
|
+
try { t(); } catch (e) { console.error('[EnterpriseScheduler]', e); }
|
|
14
|
+
await Promise.resolve();
|
|
15
|
+
}
|
|
16
|
+
}
|