@signaltree/enterprise 4.0.7 → 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.
Files changed (42) hide show
  1. package/LICENSE +54 -0
  2. package/package.json +1 -1
  3. package/src/lib/diff-engine.spec.ts +384 -0
  4. package/src/lib/diff-engine.ts +351 -0
  5. package/src/lib/enterprise-enhancer.ts +136 -0
  6. package/src/lib/enterprise.spec.ts +7 -0
  7. package/src/lib/enterprise.ts +3 -0
  8. package/src/lib/path-index.spec.ts +290 -0
  9. package/src/lib/path-index.ts +320 -0
  10. package/src/lib/scheduler.ts +16 -0
  11. package/src/lib/thread-pools.ts +11 -0
  12. package/src/lib/update-engine.spec.ts +93 -0
  13. package/src/lib/update-engine.ts +399 -0
  14. package/src/test-setup.ts +6 -0
  15. package/src/types/signaltree-core.d.ts +4 -0
  16. package/src/index.js +0 -10
  17. package/src/index.js.map +0 -1
  18. package/src/lib/diff-engine.d.ts +0 -108
  19. package/src/lib/diff-engine.js +0 -236
  20. package/src/lib/diff-engine.js.map +0 -1
  21. package/src/lib/enterprise-enhancer.d.ts +0 -81
  22. package/src/lib/enterprise-enhancer.js +0 -78
  23. package/src/lib/enterprise-enhancer.js.map +0 -1
  24. package/src/lib/enterprise.d.ts +0 -1
  25. package/src/lib/enterprise.js +0 -7
  26. package/src/lib/enterprise.js.map +0 -1
  27. package/src/lib/path-index.d.ts +0 -119
  28. package/src/lib/path-index.js +0 -265
  29. package/src/lib/path-index.js.map +0 -1
  30. package/src/lib/scheduler.d.ts +0 -2
  31. package/src/lib/scheduler.js +0 -25
  32. package/src/lib/scheduler.js.map +0 -1
  33. package/src/lib/thread-pools.d.ts +0 -4
  34. package/src/lib/thread-pools.js +0 -14
  35. package/src/lib/thread-pools.js.map +0 -1
  36. package/src/lib/update-engine.d.ts +0 -115
  37. package/src/lib/update-engine.js +0 -287
  38. package/src/lib/update-engine.js.map +0 -1
  39. package/src/test-setup.d.ts +0 -1
  40. package/src/test-setup.js +0 -8
  41. package/src/test-setup.js.map +0 -1
  42. /package/src/{index.d.ts → index.ts} +0 -0
@@ -0,0 +1,93 @@
1
+ import { OptimizedUpdateEngine } from './update-engine';
2
+
3
+ describe("OptimizedUpdateEngine", () => {
4
+ it("should detect simple changes", () => {
5
+ const tree = { name: "Alice", age: 30 };
6
+ const engine = new OptimizedUpdateEngine(tree);
7
+
8
+ const result = engine.update(tree, { age: 31 });
9
+
10
+ expect(result.changed).toBe(true);
11
+ expect(result.changedPaths.some((p) => p.includes("age"))).toBe(true);
12
+ });
13
+
14
+ it("should return false when no changes", () => {
15
+ const tree = { name: "Alice" };
16
+ const engine = new OptimizedUpdateEngine(tree);
17
+
18
+ const result = engine.update(tree, { name: "Alice" });
19
+
20
+ expect(result.changed).toBe(false);
21
+ });
22
+
23
+ it("should handle nested objects", () => {
24
+ const tree = {
25
+ user: {
26
+ profile: { name: "Alice", age: 30 },
27
+ },
28
+ };
29
+
30
+ const engine = new OptimizedUpdateEngine(tree);
31
+ const result = engine.update(tree, {
32
+ user: { profile: { age: 31 } },
33
+ });
34
+
35
+ expect(result.changed).toBe(true);
36
+ });
37
+
38
+ it("should respect maxDepth option", () => {
39
+ const tree = {
40
+ level1: {
41
+ level2: {
42
+ level3: { deep: "value" },
43
+ },
44
+ },
45
+ };
46
+
47
+ const engine = new OptimizedUpdateEngine(tree);
48
+ const result = engine.update(
49
+ tree,
50
+ {
51
+ level1: {
52
+ level2: {
53
+ level3: { deep: "changed" },
54
+ },
55
+ },
56
+ },
57
+ { maxDepth: 2 }
58
+ );
59
+
60
+ expect(result.changed).toBe(false);
61
+ });
62
+
63
+ it("should return index statistics", () => {
64
+ const tree = { a: 1, b: 2 };
65
+ const engine = new OptimizedUpdateEngine(tree);
66
+
67
+ const stats = engine.getIndexStats();
68
+
69
+ expect(stats).toHaveProperty("hits");
70
+ expect(stats).toHaveProperty("misses");
71
+ expect(stats).toHaveProperty("hitRate");
72
+ });
73
+
74
+ it("should handle large objects efficiently", () => {
75
+ const largeObj: Record<string, unknown> = {};
76
+ for (let i = 0; i < 1000; i++) {
77
+ largeObj["field" + i] = i;
78
+ }
79
+
80
+ const updates: Record<string, unknown> = {};
81
+ for (let i = 0; i < 1000; i++) {
82
+ updates["field" + i] = i + 1000;
83
+ }
84
+
85
+ const engine = new OptimizedUpdateEngine(largeObj);
86
+ const start = performance.now();
87
+ const result = engine.update(largeObj, updates);
88
+ const duration = performance.now() - start;
89
+
90
+ expect(result.changed).toBe(true);
91
+ expect(duration).toBeLessThan(200);
92
+ });
93
+ });
@@ -0,0 +1,399 @@
1
+ import { isSignal } from '@angular/core';
2
+
3
+ import { ChangeType, DiffEngine } from './diff-engine';
4
+ import { PathIndex } from './path-index';
5
+
6
+ import type { WritableSignal } from '@angular/core';
7
+ import type { Change, DiffOptions } from './diff-engine';
8
+ import type { Path } from './path-index';
9
+ /**
10
+ * OptimizedUpdateEngine - High-performance tree updates
11
+ * @packageDocumentation
12
+ */
13
+ /**
14
+ * Update options
15
+ */
16
+ export interface UpdateOptions extends DiffOptions {
17
+ /** Whether to batch updates */
18
+ batch?: boolean;
19
+
20
+ /** Batch size for chunked updates */
21
+ batchSize?: number;
22
+ }
23
+
24
+ /**
25
+ * Update result
26
+ */
27
+ export interface UpdateResult {
28
+ /** Whether any changes were made */
29
+ changed: boolean;
30
+
31
+ /** Update duration in milliseconds */
32
+ duration: number;
33
+
34
+ /** List of changed paths */
35
+ changedPaths: string[];
36
+
37
+ /** Update statistics */
38
+ stats?: {
39
+ totalPaths: number;
40
+ optimizedPaths: number;
41
+ batchedUpdates: number;
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Patch to apply
47
+ */
48
+ interface Patch {
49
+ type: ChangeType;
50
+ path: Path;
51
+ value?: unknown;
52
+ oldValue?: unknown;
53
+ priority: number;
54
+ signal?: WritableSignal<unknown> | null;
55
+ }
56
+
57
+ /**
58
+ * Apply result (internal)
59
+ */
60
+ interface ApplyResult {
61
+ appliedPaths: string[];
62
+ updateCount: number;
63
+ batchCount: number;
64
+ }
65
+
66
+ /**
67
+ * OptimizedUpdateEngine
68
+ *
69
+ * High-performance update engine using path indexing and diffing to minimize
70
+ * unnecessary signal updates.
71
+ *
72
+ * Features:
73
+ * - Diff-based updates (only update what changed)
74
+ * - Path indexing for O(k) lookups
75
+ * - Automatic batching for large updates
76
+ * - Priority-based patch ordering
77
+ * - Skip unchanged values
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * const tree = signalTree(data, { useLazySignals: true });
82
+ * const engine = new OptimizedUpdateEngine(tree);
83
+ *
84
+ * // Optimized update - only changes what's different
85
+ * const result = engine.update({
86
+ * user: { name: 'Alice' } // Only updates if name changed
87
+ * });
88
+ *
89
+ * console.log(result.changedPaths); // ['user.name']
90
+ * console.log(result.duration); // ~2ms
91
+ * ```
92
+ */
93
+ export class OptimizedUpdateEngine {
94
+ private pathIndex: PathIndex;
95
+ private diffEngine: DiffEngine;
96
+
97
+ constructor(tree: unknown) {
98
+ this.pathIndex = new PathIndex();
99
+ this.diffEngine = new DiffEngine();
100
+
101
+ // Build initial index
102
+ this.pathIndex.buildFromTree(tree);
103
+ }
104
+
105
+ /**
106
+ * Update tree with optimizations
107
+ *
108
+ * @param tree - Current tree state
109
+ * @param updates - Updates to apply
110
+ * @param options - Update options
111
+ * @returns Update result
112
+ */
113
+ update(
114
+ tree: unknown,
115
+ updates: unknown,
116
+ options: UpdateOptions = {}
117
+ ): UpdateResult {
118
+ const startTime = performance.now();
119
+
120
+ // Step 1: Generate diff to find actual changes
121
+ const diffOptions: Partial<DiffOptions> = {};
122
+ if (options.maxDepth !== undefined) diffOptions.maxDepth = options.maxDepth;
123
+ if (options.ignoreArrayOrder !== undefined)
124
+ diffOptions.ignoreArrayOrder = options.ignoreArrayOrder;
125
+ if (options.equalityFn !== undefined)
126
+ diffOptions.equalityFn = options.equalityFn;
127
+
128
+ const diff = this.diffEngine.diff(tree, updates, diffOptions);
129
+
130
+ if (diff.changes.length === 0) {
131
+ // No actual changes, skip update entirely
132
+ return {
133
+ changed: false,
134
+ duration: performance.now() - startTime,
135
+ changedPaths: [],
136
+ };
137
+ }
138
+
139
+ // Step 2: Convert diff to optimized patches
140
+ const patches = this.createPatches(diff.changes);
141
+
142
+ // Step 3: Sort patches for optimal application order
143
+ const sortedPatches = this.sortPatches(patches);
144
+
145
+ // Step 4: Apply patches with optional batching
146
+ const result = options.batch
147
+ ? this.batchApplyPatches(tree, sortedPatches, options.batchSize)
148
+ : this.applyPatches(tree, sortedPatches);
149
+
150
+ const duration = performance.now() - startTime;
151
+
152
+ return {
153
+ changed: true,
154
+ duration,
155
+ changedPaths: result.appliedPaths,
156
+ stats: {
157
+ totalPaths: diff.changes.length,
158
+ optimizedPaths: patches.length,
159
+ batchedUpdates: result.batchCount,
160
+ },
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Rebuild path index from current tree state
166
+ *
167
+ * @param tree - Current tree
168
+ */
169
+ rebuildIndex(tree: unknown): void {
170
+ this.pathIndex.clear();
171
+ this.pathIndex.buildFromTree(tree);
172
+ }
173
+
174
+ /**
175
+ * Get path index statistics
176
+ */
177
+ getIndexStats(): ReturnType<PathIndex['getStats']> {
178
+ return this.pathIndex.getStats();
179
+ }
180
+
181
+ /**
182
+ * Creates optimized patches from diff changes
183
+ */
184
+ private createPatches(changes: Change[]): Patch[] {
185
+ const patches: Patch[] = [];
186
+ const processedPaths = new Set<string>();
187
+
188
+ for (const change of changes) {
189
+ const pathStr = change.path.join('.');
190
+
191
+ // Skip if parent path already processed (optimization)
192
+ let skipPath = false;
193
+ for (const processed of processedPaths) {
194
+ if (pathStr.startsWith(processed + '.')) {
195
+ skipPath = true;
196
+ break;
197
+ }
198
+ }
199
+
200
+ if (skipPath) {
201
+ continue;
202
+ }
203
+
204
+ // Create patch based on change type
205
+ const patch = this.createPatch(change);
206
+ patches.push(patch);
207
+
208
+ // Mark path as processed
209
+ processedPaths.add(pathStr);
210
+
211
+ // If this is an object replacement, skip child paths
212
+ if (
213
+ change.type === ChangeType.REPLACE &&
214
+ typeof change.value === 'object'
215
+ ) {
216
+ processedPaths.add(pathStr);
217
+ }
218
+ }
219
+
220
+ return patches;
221
+ }
222
+
223
+ /**
224
+ * Creates a single patch from a change
225
+ */
226
+ private createPatch(change: Change): Patch {
227
+ return {
228
+ type: change.type,
229
+ path: change.path,
230
+ value: change.value,
231
+ oldValue: change.oldValue,
232
+ priority: this.calculatePriority(change),
233
+ signal: this.pathIndex.get(change.path),
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Calculates update priority for optimal ordering
239
+ */
240
+ private calculatePriority(change: Change): number {
241
+ let priority = 0;
242
+
243
+ // Shallow updates have higher priority
244
+ priority += (10 - change.path.length) * 10;
245
+
246
+ // Array updates have lower priority (more expensive)
247
+ if (change.path.some((p) => typeof p === 'number')) {
248
+ priority -= 20;
249
+ }
250
+
251
+ // Replace operations have higher priority than nested updates
252
+ if (change.type === ChangeType.REPLACE) {
253
+ priority += 30;
254
+ }
255
+
256
+ return priority;
257
+ }
258
+
259
+ /**
260
+ * Sorts patches for optimal application
261
+ */
262
+ private sortPatches(patches: Patch[]): Patch[] {
263
+ return patches.sort((a, b) => {
264
+ // Sort by priority (higher first)
265
+ if (a.priority !== b.priority) {
266
+ return b.priority - a.priority;
267
+ }
268
+
269
+ // Then by path depth (shallow first)
270
+ return a.path.length - b.path.length;
271
+ });
272
+ }
273
+
274
+ /**
275
+ * Applies patches directly (no batching)
276
+ */
277
+ private applyPatches(tree: unknown, patches: Patch[]): ApplyResult {
278
+ const appliedPaths: string[] = [];
279
+ let updateCount = 0;
280
+
281
+ for (const patch of patches) {
282
+ if (this.applyPatch(patch, tree)) {
283
+ appliedPaths.push(patch.path.join('.'));
284
+ updateCount++;
285
+ }
286
+ }
287
+
288
+ return {
289
+ appliedPaths,
290
+ updateCount,
291
+ batchCount: 1,
292
+ };
293
+ }
294
+
295
+ /**
296
+ * Applies patches with batching for better performance
297
+ */
298
+ private batchApplyPatches(
299
+ tree: unknown,
300
+ patches: Patch[],
301
+ batchSize = 50
302
+ ): ApplyResult {
303
+ const batches: Patch[][] = [];
304
+
305
+ for (let i = 0; i < patches.length; i += batchSize) {
306
+ batches.push(patches.slice(i, i + batchSize));
307
+ }
308
+
309
+ const appliedPaths: string[] = [];
310
+ let updateCount = 0;
311
+
312
+ // Process patches in batches
313
+ for (const currentBatch of batches) {
314
+ for (const patch of currentBatch) {
315
+ if (this.applyPatch(patch, tree)) {
316
+ appliedPaths.push(patch.path.join('.'));
317
+ updateCount++;
318
+ }
319
+ }
320
+ }
321
+
322
+ return {
323
+ appliedPaths,
324
+ updateCount,
325
+ batchCount: batches.length,
326
+ };
327
+ }
328
+
329
+ /**
330
+ * Applies a single patch to the tree object
331
+ */
332
+ private applyPatch(patch: Patch, tree: unknown): boolean {
333
+ try {
334
+ // First, try to update via signal if available
335
+ if (patch.signal && isSignal(patch.signal) && 'set' in patch.signal) {
336
+ const currentValue = patch.signal();
337
+
338
+ // Only update if value actually changed
339
+ if (this.isEqual(currentValue, patch.value)) {
340
+ return false;
341
+ }
342
+
343
+ // Update the signal - this will handle reactivity
344
+ (patch.signal as WritableSignal<unknown>).set(patch.value);
345
+
346
+ // After successful ADD, update the index
347
+ if (patch.type === ChangeType.ADD && patch.value !== undefined) {
348
+ this.pathIndex.set(patch.path, patch.signal);
349
+ }
350
+
351
+ return true;
352
+ } // Fallback: Navigate to parent object and update directly
353
+ let current: Record<string, unknown> = tree as Record<string, unknown>;
354
+ for (let i = 0; i < patch.path.length - 1; i++) {
355
+ const key = patch.path[i];
356
+ current = current[key] as Record<string, unknown>;
357
+ if (!current || typeof current !== 'object') {
358
+ return false;
359
+ }
360
+ }
361
+
362
+ const lastKey = patch.path[patch.path.length - 1];
363
+
364
+ // Only update if value actually changed
365
+ if (this.isEqual(current[lastKey], patch.value)) {
366
+ return false;
367
+ }
368
+
369
+ // Apply update directly to object
370
+ current[lastKey] = patch.value;
371
+ return true;
372
+ } catch (error) {
373
+ console.error(`Failed to apply patch at ${patch.path.join('.')}:`, error);
374
+ return false;
375
+ }
376
+ }
377
+ /**
378
+ * Check equality
379
+ */
380
+ private isEqual(a: unknown, b: unknown): boolean {
381
+ // Fast path for primitives
382
+ if (a === b) {
383
+ return true;
384
+ }
385
+ if (typeof a !== typeof b) {
386
+ return false;
387
+ }
388
+ if (typeof a !== 'object' || a === null || b === null) {
389
+ return false;
390
+ }
391
+
392
+ // Deep equality for objects (simple version)
393
+ try {
394
+ return JSON.stringify(a) === JSON.stringify(b);
395
+ } catch {
396
+ return false;
397
+ }
398
+ }
399
+ }
@@ -0,0 +1,6 @@
1
+ import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
2
+
3
+ setupZoneTestEnv({
4
+ errorOnUnknownElements: true,
5
+ errorOnUnknownProperties: true,
6
+ });
@@ -0,0 +1,4 @@
1
+ declare module '@signaltree/core' {
2
+ export type { Enhancer } from '../../../core/src/lib/types';
3
+ export * from '../../../core/src/index';
4
+ }
package/src/index.js DELETED
@@ -1,10 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const tslib_1 = require("tslib");
4
- tslib_1.__exportStar(require("./lib/diff-engine"), exports);
5
- tslib_1.__exportStar(require("./lib/path-index"), exports);
6
- tslib_1.__exportStar(require("./lib/update-engine"), exports);
7
- tslib_1.__exportStar(require("./lib/enterprise-enhancer"), exports);
8
- tslib_1.__exportStar(require("./lib/scheduler"), exports);
9
- tslib_1.__exportStar(require("./lib/thread-pools"), exports);
10
- //# sourceMappingURL=index.js.map
package/src/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../packages/enterprise/src/index.ts"],"names":[],"mappings":";;;AAAA,4DAAkC;AAClC,2DAAiC;AACjC,8DAAoC;AACpC,oEAA0C;AAC1C,0DAAgC;AAChC,6DAAmC"}
@@ -1,108 +0,0 @@
1
- /**
2
- * DiffEngine - Efficient change detection for tree updates
3
- * @packageDocumentation
4
- */
5
- import type { Path } from './path-index';
6
- /**
7
- * Type of change detected
8
- */
9
- export declare enum ChangeType {
10
- ADD = "add",
11
- UPDATE = "update",
12
- DELETE = "delete",
13
- REPLACE = "replace"
14
- }
15
- /**
16
- * A detected change
17
- */
18
- export interface Change {
19
- /** Type of change */
20
- type: ChangeType;
21
- /** Path to the changed value */
22
- path: Path;
23
- /** New value */
24
- value?: unknown;
25
- /** Old value (for updates/deletes) */
26
- oldValue?: unknown;
27
- }
28
- /**
29
- * Diff result
30
- */
31
- export interface Diff {
32
- /** List of changes */
33
- changes: Change[];
34
- /** Whether any changes were detected */
35
- hasChanges: boolean;
36
- }
37
- /**
38
- * Configuration for diff operation
39
- */
40
- export interface DiffOptions {
41
- /** Maximum depth to traverse */
42
- maxDepth?: number;
43
- /** Whether to detect deletions */
44
- detectDeletions?: boolean;
45
- /** Whether to ignore array order */
46
- ignoreArrayOrder?: boolean;
47
- /** Custom equality function */
48
- equalityFn?: (a: unknown, b: unknown) => boolean;
49
- /** Optional key validator for security (e.g., to prevent prototype pollution) */
50
- keyValidator?: (key: string) => boolean;
51
- }
52
- /**
53
- * DiffEngine
54
- *
55
- * Efficiently detects changes between two objects to minimize unnecessary updates.
56
- *
57
- * Features:
58
- * - Deep object comparison
59
- * - Circular reference detection
60
- * - Configurable equality checking
61
- * - Array diffing (ordered and unordered)
62
- * - Path tracking for precise updates
63
- *
64
- * @example
65
- * ```ts
66
- * const engine = new DiffEngine();
67
- *
68
- * const current = { user: { name: 'Alice', age: 30 } };
69
- * const updates = { user: { name: 'Alice', age: 31 } };
70
- *
71
- * const diff = engine.diff(current, updates);
72
- *
73
- * console.log(diff.changes);
74
- * // [{ type: 'update', path: ['user', 'age'], value: 31, oldValue: 30 }]
75
- * ```
76
- */
77
- export declare class DiffEngine {
78
- private defaultOptions;
79
- /**
80
- * Diff two objects and return changes
81
- *
82
- * @param current - Current state
83
- * @param updates - Updated state
84
- * @param options - Diff options
85
- * @returns Diff result with all changes
86
- */
87
- diff(current: unknown, updates: unknown, options?: DiffOptions): Diff;
88
- /**
89
- * Traverse and compare objects recursively
90
- */
91
- private traverse;
92
- /**
93
- * Diff arrays
94
- */
95
- private diffArrays;
96
- /**
97
- * Diff arrays in order (index-based)
98
- */
99
- private diffArraysOrdered;
100
- /**
101
- * Diff arrays ignoring order (value-based)
102
- */
103
- private diffArraysUnordered;
104
- /**
105
- * Stringify value for set comparison
106
- */
107
- private stringify;
108
- }