@signaltree/enterprise 4.0.9 → 4.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,290 +0,0 @@
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
- });
@@ -1,320 +0,0 @@
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
- }
@@ -1,16 +0,0 @@
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
- }
@@ -1,11 +0,0 @@
1
- export interface WorkerPool {
2
- run<T>(fn: (...args: any[]) => T, ...args: any[]): Promise<T>;
3
- }
4
-
5
- export function createMockPool(): WorkerPool {
6
- return {
7
- async run(fn, ...args) {
8
- return fn(...args);
9
- }
10
- };
11
- }