@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,93 +0,0 @@
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
- });
@@ -1,399 +0,0 @@
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
- }
package/src/test-setup.ts DELETED
@@ -1,6 +0,0 @@
1
- import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
2
-
3
- setupZoneTestEnv({
4
- errorOnUnknownElements: true,
5
- errorOnUnknownProperties: true,
6
- });
@@ -1,4 +0,0 @@
1
- declare module '@signaltree/core' {
2
- export type { Enhancer } from '../../../core/src/lib/types';
3
- export * from '../../../core/src/index';
4
- }