@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,351 +0,0 @@
1
- /**
2
- * DiffEngine - Efficient change detection for tree updates
3
- * @packageDocumentation
4
- */
5
-
6
- import type { Path } from './path-index';
7
-
8
- /**
9
- * Type of change detected
10
- */
11
- export enum ChangeType {
12
- ADD = 'add',
13
- UPDATE = 'update',
14
- DELETE = 'delete',
15
- REPLACE = 'replace',
16
- }
17
-
18
- /**
19
- * A detected change
20
- */
21
- export interface Change {
22
- /** Type of change */
23
- type: ChangeType;
24
-
25
- /** Path to the changed value */
26
- path: Path;
27
-
28
- /** New value */
29
- value?: unknown;
30
-
31
- /** Old value (for updates/deletes) */
32
- oldValue?: unknown;
33
- }
34
-
35
- /**
36
- * Diff result
37
- */
38
- export interface Diff {
39
- /** List of changes */
40
- changes: Change[];
41
-
42
- /** Whether any changes were detected */
43
- hasChanges: boolean;
44
- }
45
-
46
- /**
47
- * Configuration for diff operation
48
- */
49
- export interface DiffOptions {
50
- /** Maximum depth to traverse */
51
- maxDepth?: number;
52
-
53
- /** Whether to detect deletions */
54
- detectDeletions?: boolean;
55
-
56
- /** Whether to ignore array order */
57
- ignoreArrayOrder?: boolean;
58
-
59
- /** Custom equality function */
60
- equalityFn?: (a: unknown, b: unknown) => boolean;
61
-
62
- /** Optional key validator for security (e.g., to prevent prototype pollution) */
63
- keyValidator?: (key: string) => boolean;
64
- }
65
-
66
- /**
67
- * Internal resolved options with required fields
68
- */
69
- type ResolvedDiffOptions = Required<Omit<DiffOptions, 'keyValidator'>> & {
70
- keyValidator?: (key: string) => boolean;
71
- };
72
-
73
- /**
74
- * DiffEngine
75
- *
76
- * Efficiently detects changes between two objects to minimize unnecessary updates.
77
- *
78
- * Features:
79
- * - Deep object comparison
80
- * - Circular reference detection
81
- * - Configurable equality checking
82
- * - Array diffing (ordered and unordered)
83
- * - Path tracking for precise updates
84
- *
85
- * @example
86
- * ```ts
87
- * const engine = new DiffEngine();
88
- *
89
- * const current = { user: { name: 'Alice', age: 30 } };
90
- * const updates = { user: { name: 'Alice', age: 31 } };
91
- *
92
- * const diff = engine.diff(current, updates);
93
- *
94
- * console.log(diff.changes);
95
- * // [{ type: 'update', path: ['user', 'age'], value: 31, oldValue: 30 }]
96
- * ```
97
- */
98
- export class DiffEngine {
99
- private defaultOptions: ResolvedDiffOptions = {
100
- maxDepth: 100,
101
- detectDeletions: false,
102
- ignoreArrayOrder: false,
103
- equalityFn: (a, b) => a === b,
104
- keyValidator: undefined,
105
- };
106
-
107
- /**
108
- * Diff two objects and return changes
109
- *
110
- * @param current - Current state
111
- * @param updates - Updated state
112
- * @param options - Diff options
113
- * @returns Diff result with all changes
114
- */
115
- diff(current: unknown, updates: unknown, options: DiffOptions = {}): Diff {
116
- const opts = { ...this.defaultOptions, ...options };
117
- const changes: Change[] = [];
118
- const visited = new WeakSet();
119
-
120
- this.traverse(current, updates, [], changes, visited, opts, 0);
121
-
122
- return {
123
- changes,
124
- hasChanges: changes.length > 0,
125
- };
126
- }
127
-
128
- /**
129
- * Traverse and compare objects recursively
130
- */
131
- private traverse(
132
- curr: unknown,
133
- upd: unknown,
134
- path: Path,
135
- changes: Change[],
136
- visited: WeakSet<object>,
137
- opts: ResolvedDiffOptions,
138
- depth: number
139
- ): void {
140
- // Depth limit
141
- if (depth > opts.maxDepth) {
142
- return;
143
- }
144
-
145
- // Handle primitives
146
- if (typeof upd !== 'object' || upd === null) {
147
- if (!opts.equalityFn(curr, upd)) {
148
- changes.push({
149
- type: curr === undefined ? ChangeType.ADD : ChangeType.UPDATE,
150
- path: [...path],
151
- value: upd,
152
- oldValue: curr,
153
- });
154
- }
155
- return;
156
- }
157
-
158
- // Circular reference detection
159
- if (visited.has(upd)) {
160
- return;
161
- }
162
- visited.add(upd);
163
-
164
- // Handle arrays
165
- if (Array.isArray(upd)) {
166
- this.diffArrays(curr, upd, path, changes, visited, opts, depth);
167
- return;
168
- }
169
-
170
- // Handle objects
171
- if (!curr || typeof curr !== 'object' || Array.isArray(curr)) {
172
- // Type mismatch - replace entire subtree
173
- changes.push({
174
- type: ChangeType.REPLACE,
175
- path: [...path],
176
- value: upd,
177
- oldValue: curr,
178
- });
179
- return;
180
- }
181
-
182
- // Diff object properties
183
- const currObj = curr as Record<string, unknown>;
184
- const updObj = upd as Record<string, unknown>;
185
-
186
- for (const key in updObj) {
187
- if (Object.prototype.hasOwnProperty.call(updObj, key)) {
188
- // Validate key for security (e.g., prevent prototype pollution)
189
- if (opts.keyValidator && !opts.keyValidator(key)) {
190
- // Skip dangerous keys silently
191
- continue;
192
- }
193
-
194
- this.traverse(
195
- currObj[key],
196
- updObj[key],
197
- [...path, key],
198
- changes,
199
- visited,
200
- opts,
201
- depth + 1
202
- );
203
- }
204
- }
205
-
206
- // Check for deletions if enabled
207
- if (opts.detectDeletions) {
208
- for (const key in currObj) {
209
- if (
210
- Object.prototype.hasOwnProperty.call(currObj, key) &&
211
- !(key in updObj)
212
- ) {
213
- changes.push({
214
- type: ChangeType.DELETE,
215
- path: [...path, key],
216
- oldValue: currObj[key],
217
- });
218
- }
219
- }
220
- }
221
- }
222
-
223
- /**
224
- * Diff arrays
225
- */
226
- private diffArrays(
227
- curr: unknown,
228
- upd: unknown[],
229
- path: Path,
230
- changes: Change[],
231
- visited: WeakSet<object>,
232
- opts: ResolvedDiffOptions,
233
- depth: number
234
- ): void {
235
- if (!Array.isArray(curr)) {
236
- // Not an array - replace
237
- changes.push({
238
- type: ChangeType.REPLACE,
239
- path: [...path],
240
- value: upd,
241
- oldValue: curr,
242
- });
243
- return;
244
- }
245
-
246
- if (opts.ignoreArrayOrder) {
247
- this.diffArraysUnordered(curr, upd, path, changes, opts);
248
- } else {
249
- this.diffArraysOrdered(curr, upd, path, changes, visited, opts, depth);
250
- }
251
- }
252
-
253
- /**
254
- * Diff arrays in order (index-based)
255
- */
256
- private diffArraysOrdered(
257
- curr: unknown[],
258
- upd: unknown[],
259
- path: Path,
260
- changes: Change[],
261
- visited: WeakSet<object>,
262
- opts: ResolvedDiffOptions,
263
- depth: number
264
- ): void {
265
- // Check each index
266
- const maxLength = Math.max(curr.length, upd.length);
267
-
268
- for (let i = 0; i < maxLength; i++) {
269
- if (i >= upd.length) {
270
- // Deletion (if enabled)
271
- if (opts.detectDeletions) {
272
- changes.push({
273
- type: ChangeType.DELETE,
274
- path: [...path, i],
275
- oldValue: curr[i],
276
- });
277
- }
278
- } else if (i >= curr.length) {
279
- // Addition
280
- changes.push({
281
- type: ChangeType.ADD,
282
- path: [...path, i],
283
- value: upd[i],
284
- });
285
- } else {
286
- // Potential update
287
- this.traverse(
288
- curr[i],
289
- upd[i],
290
- [...path, i],
291
- changes,
292
- visited,
293
- opts,
294
- depth + 1
295
- );
296
- }
297
- }
298
- }
299
-
300
- /**
301
- * Diff arrays ignoring order (value-based)
302
- */
303
- private diffArraysUnordered(
304
- curr: unknown[],
305
- upd: unknown[],
306
- path: Path,
307
- changes: Change[],
308
- opts: ResolvedDiffOptions
309
- ): void {
310
- // Build value sets
311
- const currSet = new Set(curr.map((v) => this.stringify(v)));
312
- const updSet = new Set(upd.map((v) => this.stringify(v)));
313
-
314
- // Find additions
315
- upd.forEach((value, index) => {
316
- const str = this.stringify(value);
317
- if (!currSet.has(str)) {
318
- changes.push({
319
- type: ChangeType.ADD,
320
- path: [...path, index],
321
- value,
322
- });
323
- }
324
- });
325
-
326
- // Find deletions (if enabled)
327
- if (opts.detectDeletions) {
328
- curr.forEach((value, index) => {
329
- const str = this.stringify(value);
330
- if (!updSet.has(str)) {
331
- changes.push({
332
- type: ChangeType.DELETE,
333
- path: [...path, index],
334
- oldValue: value,
335
- });
336
- }
337
- });
338
- }
339
- }
340
-
341
- /**
342
- * Stringify value for set comparison
343
- */
344
- private stringify(value: unknown): string {
345
- try {
346
- return JSON.stringify(value);
347
- } catch {
348
- return String(value);
349
- }
350
- }
351
- }
@@ -1,136 +0,0 @@
1
- import { PathIndex } from './path-index';
2
- import { OptimizedUpdateEngine, UpdateResult } from './update-engine';
3
-
4
- import type { Signal } from '@angular/core';
5
- import type { Enhancer } from '@signaltree/core';
6
- /**
7
- * Enterprise-grade optimizations for large-scale applications.
8
- *
9
- * **Includes:**
10
- * - Diff-based updates (only update changed signals)
11
- * - Bulk operation optimization (2-5x faster)
12
- * - Advanced change tracking
13
- * - Update statistics and monitoring
14
- *
15
- * **Use when:**
16
- * - 500+ signals in state tree
17
- * - Bulk updates at high frequency (60Hz+)
18
- * - Real-time dashboards or data feeds
19
- * - Enterprise-scale applications
20
- *
21
- * **Skip when:**
22
- * - Small to medium apps (<100 signals)
23
- * - Infrequent state updates
24
- * - Startup/prototype projects
25
- *
26
- * **Bundle cost:** +2.4KB gzipped
27
- * **Performance gain:** 2-5x faster bulk updates, detailed monitoring
28
- *
29
- * @example
30
- * ```typescript
31
- * import { signalTree } from '@signaltree/core';
32
- * import { withEnterprise } from '@signaltree/enterprise';
33
- *
34
- * const tree = signalTree(largeState).with(withEnterprise());
35
- *
36
- * // Now available: optimized bulk updates
37
- * const result = tree.updateOptimized(newData, {
38
- * ignoreArrayOrder: true,
39
- * maxDepth: 10
40
- * });
41
- *
42
- * console.log(result.stats);
43
- * // { totalChanges: 45, adds: 10, updates: 30, deletes: 5 }
44
- * ```
45
- *
46
- * @public
47
- */
48
- export function withEnterprise<T extends Record<string, unknown>>(): Enhancer<
49
- T,
50
- T & EnterpriseEnhancedTree<T>
51
- > {
52
- return (tree: T): T & EnterpriseEnhancedTree<T> => {
53
- // Lazy initialization - only create when first needed
54
- let pathIndex: PathIndex<Signal<unknown>> | null = null;
55
- let updateEngine: OptimizedUpdateEngine | null = null;
56
-
57
- // Type assertion to access SignalTree properties
58
- const signalTree = tree as unknown as { state: unknown };
59
-
60
- // Cast tree to enhanced type for safe property assignment
61
- const enhancedTree = tree as T & EnterpriseEnhancedTree<T>;
62
-
63
- // Add updateOptimized method to tree
64
- enhancedTree.updateOptimized = (
65
- updates: Partial<T>,
66
- options?: {
67
- maxDepth?: number;
68
- ignoreArrayOrder?: boolean;
69
- equalityFn?: (a: unknown, b: unknown) => boolean;
70
- batch?: boolean;
71
- batchSize?: number;
72
- }
73
- ): UpdateResult => {
74
- // Lazy initialize on first use
75
- if (!updateEngine) {
76
- pathIndex = new PathIndex<Signal<unknown>>();
77
- pathIndex.buildFromTree(signalTree.state);
78
- updateEngine = new OptimizedUpdateEngine(signalTree.state);
79
- }
80
-
81
- const result = updateEngine.update(signalTree.state, updates, options);
82
-
83
- // Rebuild index if changes were made
84
- if (result.changed && result.stats && pathIndex) {
85
- pathIndex.clear();
86
- pathIndex.buildFromTree(signalTree.state);
87
- }
88
-
89
- return result;
90
- };
91
-
92
- // Add PathIndex access for debugging/monitoring
93
- enhancedTree.getPathIndex = () => pathIndex;
94
-
95
- return enhancedTree;
96
- };
97
- }
98
-
99
- /**
100
- * Type augmentation for trees enhanced with enterprise features.
101
- * This is applied when using withEnterprise().
102
- *
103
- * @public
104
- */
105
- export interface EnterpriseEnhancedTree<T> {
106
- /**
107
- * Optimized bulk update method using diff-based change detection.
108
- * Only available when using withEnterprise().
109
- *
110
- * @param newValue - The new state value
111
- * @param options - Update options
112
- * @returns Update result with statistics
113
- */
114
- updateOptimized(
115
- newValue: T,
116
- options?: {
117
- /** Maximum depth to traverse (default: 100) */
118
- maxDepth?: number;
119
- /** Ignore array element order (default: false) */
120
- ignoreArrayOrder?: boolean;
121
- /** Custom equality function */
122
- equalityFn?: (a: unknown, b: unknown) => boolean;
123
- /** Automatically batch updates (default: true) */
124
- autoBatch?: boolean;
125
- /** Number of patches per batch (default: 10) */
126
- batchSize?: number;
127
- }
128
- ): UpdateResult;
129
-
130
- /**
131
- * Get the PathIndex for debugging/monitoring.
132
- * Only available when using withEnterprise().
133
- * Returns null if updateOptimized hasn't been called yet (lazy initialization).
134
- */
135
- getPathIndex(): PathIndex<Signal<unknown>> | null;
136
- }
@@ -1,7 +0,0 @@
1
- import { enterprise } from './enterprise';
2
-
3
- describe('enterprise', () => {
4
- it('should work', () => {
5
- expect(enterprise()).toEqual('enterprise');
6
- });
7
- });
@@ -1,3 +0,0 @@
1
- export function enterprise(): string {
2
- return 'enterprise';
3
- }