@signaltree/enterprise 4.1.2 → 4.1.4

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/dist/index.js CHANGED
@@ -2,5 +2,5 @@ export { ChangeType, DiffEngine } from './lib/diff-engine.js';
2
2
  export { PathIndex } from './lib/path-index.js';
3
3
  export { OptimizedUpdateEngine } from './lib/update-engine.js';
4
4
  export { withEnterprise } from './lib/enterprise-enhancer.js';
5
- export { postTask } from './lib/scheduler.js';
5
+ export { configureScheduler, getSchedulerMetrics, postTask } from './lib/scheduler.js';
6
6
  export { createMockPool } from './lib/thread-pools.js';
@@ -12,23 +12,38 @@ class DiffEngine {
12
12
  detectDeletions: false,
13
13
  ignoreArrayOrder: false,
14
14
  equalityFn: (a, b) => a === b,
15
- keyValidator: undefined
15
+ keyValidator: undefined,
16
+ instrumentation: false
16
17
  };
17
18
  }
18
19
  diff(current, updates, options = {}) {
19
20
  const opts = Object.assign(Object.assign({}, this.defaultOptions), options);
20
21
  const changes = [];
21
22
  const visited = new WeakSet();
22
- this.traverse(current, updates, [], changes, visited, opts, 0);
23
+ const metrics = {
24
+ elementComparisons: 0,
25
+ prefixFastPathHits: 0,
26
+ wholeArrayReplaceHits: 0,
27
+ traversals: 0,
28
+ suffixFastPathHits: 0,
29
+ segmentSkips: 0,
30
+ samplesTaken: 0
31
+ };
32
+ this.traverse(current, updates, [], changes, visited, opts, 0, metrics);
23
33
  return {
24
34
  changes,
25
- hasChanges: changes.length > 0
35
+ hasChanges: changes.length > 0,
36
+ instrumentation: opts.instrumentation ? metrics : undefined
26
37
  };
27
38
  }
28
- traverse(curr, upd, path, changes, visited, opts, depth) {
39
+ traverse(curr, upd, path, changes, visited, opts, depth, metrics) {
40
+ if (opts.instrumentation) metrics.traversals++;
29
41
  if (depth > opts.maxDepth) {
30
42
  return;
31
43
  }
44
+ if (curr === upd) {
45
+ return;
46
+ }
32
47
  if (typeof upd !== 'object' || upd === null) {
33
48
  if (!opts.equalityFn(curr, upd)) {
34
49
  changes.push({
@@ -45,7 +60,7 @@ class DiffEngine {
45
60
  }
46
61
  visited.add(upd);
47
62
  if (Array.isArray(upd)) {
48
- this.diffArrays(curr, upd, path, changes, visited, opts, depth);
63
+ this.diffArrays(curr, upd, path, changes, visited, opts, depth, metrics);
49
64
  return;
50
65
  }
51
66
  if (!curr || typeof curr !== 'object' || Array.isArray(curr)) {
@@ -64,7 +79,7 @@ class DiffEngine {
64
79
  if (opts.keyValidator && !opts.keyValidator(key)) {
65
80
  continue;
66
81
  }
67
- this.traverse(currObj[key], updObj[key], [...path, key], changes, visited, opts, depth + 1);
82
+ this.traverse(currObj[key], updObj[key], [...path, key], changes, visited, opts, depth + 1, metrics);
68
83
  }
69
84
  }
70
85
  if (opts.detectDeletions) {
@@ -79,7 +94,7 @@ class DiffEngine {
79
94
  }
80
95
  }
81
96
  }
82
- diffArrays(curr, upd, path, changes, visited, opts, depth) {
97
+ diffArrays(curr, upd, path, changes, visited, opts, depth, metrics) {
83
98
  if (!Array.isArray(curr)) {
84
99
  changes.push({
85
100
  type: ChangeType.REPLACE,
@@ -92,11 +107,80 @@ class DiffEngine {
92
107
  if (opts.ignoreArrayOrder) {
93
108
  this.diffArraysUnordered(curr, upd, path, changes, opts);
94
109
  } else {
95
- this.diffArraysOrdered(curr, upd, path, changes, visited, opts, depth);
110
+ this.diffArraysOrdered(curr, upd, path, changes, visited, opts, depth, metrics);
96
111
  }
97
112
  }
98
- diffArraysOrdered(curr, upd, path, changes, visited, opts, depth) {
99
- const maxLength = Math.max(curr.length, upd.length);
113
+ diffArraysOrdered(curr, upd, path, changes, visited, opts, depth, metrics) {
114
+ if (curr === upd) {
115
+ return;
116
+ }
117
+ if (curr.length === upd.length) {
118
+ let identical = true;
119
+ for (let i = 0; i < curr.length; i++) {
120
+ if (opts.instrumentation) metrics.elementComparisons++;
121
+ if (curr[i] !== upd[i]) {
122
+ identical = false;
123
+ break;
124
+ }
125
+ }
126
+ if (identical) return;
127
+ }
128
+ const currLen = curr.length;
129
+ const updLen = upd.length;
130
+ const minLen = Math.min(currLen, updLen);
131
+ if (minLen > 0) {
132
+ let prefixIdentical = true;
133
+ for (let i = 0; i < minLen; i++) {
134
+ if (opts.instrumentation) metrics.elementComparisons++;
135
+ if (curr[i] !== upd[i]) {
136
+ prefixIdentical = false;
137
+ break;
138
+ }
139
+ }
140
+ if (prefixIdentical && currLen !== updLen) {
141
+ if (opts.instrumentation) metrics.prefixFastPathHits++;
142
+ if (updLen > currLen) {
143
+ for (let i = currLen; i < updLen; i++) {
144
+ changes.push({
145
+ type: ChangeType.ADD,
146
+ path: [...path, i],
147
+ value: upd[i]
148
+ });
149
+ }
150
+ } else if (currLen > updLen && opts.detectDeletions) {
151
+ for (let i = updLen; i < currLen; i++) {
152
+ changes.push({
153
+ type: ChangeType.DELETE,
154
+ path: [...path, i],
155
+ oldValue: curr[i]
156
+ });
157
+ }
158
+ }
159
+ return;
160
+ }
161
+ }
162
+ const LARGE_ARRAY_THRESHOLD = 1024;
163
+ const REPLACE_MISMATCH_RATIO = 0.4;
164
+ if (currLen >= LARGE_ARRAY_THRESHOLD && updLen >= LARGE_ARRAY_THRESHOLD && currLen === updLen) {
165
+ let mismatches = 0;
166
+ for (let i = 0; i < currLen; i++) {
167
+ if (opts.instrumentation) metrics.elementComparisons++;
168
+ if (curr[i] !== upd[i]) {
169
+ mismatches++;
170
+ if (mismatches / currLen > REPLACE_MISMATCH_RATIO) {
171
+ changes.push({
172
+ type: ChangeType.REPLACE,
173
+ path: [...path],
174
+ value: upd,
175
+ oldValue: curr
176
+ });
177
+ if (opts.instrumentation) metrics.wholeArrayReplaceHits++;
178
+ return;
179
+ }
180
+ }
181
+ }
182
+ }
183
+ const maxLength = Math.max(currLen, updLen);
100
184
  for (let i = 0; i < maxLength; i++) {
101
185
  if (i >= upd.length) {
102
186
  if (opts.detectDeletions) {
@@ -113,7 +197,7 @@ class DiffEngine {
113
197
  value: upd[i]
114
198
  });
115
199
  } else {
116
- this.traverse(curr[i], upd[i], [...path, i], changes, visited, opts, depth + 1);
200
+ this.traverse(curr[i], upd[i], [...path, i], changes, visited, opts, depth + 1, metrics);
117
201
  }
118
202
  }
119
203
  }
@@ -14,9 +14,13 @@ function withEnterprise() {
14
14
  updateEngine = new OptimizedUpdateEngine(signalTree.state);
15
15
  }
16
16
  const result = updateEngine.update(signalTree.state, updates, options);
17
- if (result.changed && result.stats && pathIndex) {
18
- pathIndex.clear();
19
- pathIndex.buildFromTree(signalTree.state);
17
+ if (result.changed && pathIndex) {
18
+ if (result.changedPaths.length) {
19
+ pathIndex.incrementalUpdate(signalTree.state, result.changedPaths);
20
+ } else {
21
+ pathIndex.clear();
22
+ pathIndex.buildFromTree(signalTree.state);
23
+ }
20
24
  }
21
25
  return result;
22
26
  };
@@ -16,6 +16,14 @@ class PathIndex {
16
16
  sets: 0,
17
17
  cleanups: 0
18
18
  };
19
+ this.enableInstrumentation = false;
20
+ this.instrumentation = {
21
+ incrementalUpdates: 0,
22
+ fullRebuilds: 0,
23
+ nodesTouched: 0,
24
+ deletions: 0,
25
+ rebuildDurationNs: 0
26
+ };
19
27
  }
20
28
  set(path, signal) {
21
29
  const pathStr = this.pathToString(path);
@@ -116,6 +124,7 @@ class PathIndex {
116
124
  clear() {
117
125
  this.root = new TrieNode();
118
126
  this.pathCache.clear();
127
+ if (this.enableInstrumentation) this.instrumentation.fullRebuilds++;
119
128
  }
120
129
  getStats() {
121
130
  const total = this.stats.hits + this.stats.misses;
@@ -154,6 +163,102 @@ class PathIndex {
154
163
  this.collectDescendants(child, [...currentPath, key], results);
155
164
  }
156
165
  }
166
+ deleteSubtree(path) {
167
+ if (path.length === 0) {
168
+ this.clear();
169
+ return;
170
+ }
171
+ let node = this.root;
172
+ const nodes = [node];
173
+ for (const segment of path) {
174
+ node = node.children.get(String(segment));
175
+ if (!node) {
176
+ return;
177
+ }
178
+ nodes.push(node);
179
+ }
180
+ const toClean = [];
181
+ const collectPaths = (n, current) => {
182
+ if (n.value) {
183
+ toClean.push(this.pathToString(current));
184
+ }
185
+ for (const [key, child] of n.children) {
186
+ collectPaths(child, [...current, key]);
187
+ }
188
+ };
189
+ collectPaths(nodes[nodes.length - 1], path);
190
+ for (const p of toClean) {
191
+ this.pathCache.delete(p);
192
+ }
193
+ const parent = nodes[nodes.length - 2];
194
+ parent.children.delete(String(path[path.length - 1]));
195
+ if (this.enableInstrumentation) this.instrumentation.deletions += toClean.length || 1;
196
+ }
197
+ incrementalUpdate(rootTree, changedPaths) {
198
+ const start = this.enableInstrumentation ? performance.now() : 0;
199
+ if (!changedPaths.length) return;
200
+ const FULL_REBUILD_THRESHOLD = 2000;
201
+ if (changedPaths.length > FULL_REBUILD_THRESHOLD || changedPaths.includes('')) {
202
+ this.clear();
203
+ this.buildFromTree(rootTree);
204
+ if (this.enableInstrumentation) {
205
+ this.instrumentation.rebuildDurationNs += (performance.now() - start) * 1e6;
206
+ }
207
+ return;
208
+ }
209
+ const ordered = [...changedPaths].sort((a, b) => a.length - b.length);
210
+ const effective = [];
211
+ for (const p of ordered) {
212
+ if (!effective.some(ep => p !== ep && p.startsWith(ep + '.'))) {
213
+ effective.push(p);
214
+ }
215
+ }
216
+ for (const pathStr of effective) {
217
+ const segments = pathStr === '' ? [] : pathStr.split('.');
218
+ let subtree = rootTree;
219
+ for (const seg of segments) {
220
+ if (!subtree || typeof subtree !== 'object') {
221
+ subtree = undefined;
222
+ break;
223
+ }
224
+ subtree = subtree[seg];
225
+ }
226
+ if (subtree === undefined || subtree === null) {
227
+ this.deleteSubtree(segments);
228
+ continue;
229
+ }
230
+ if (isSignal(subtree)) {
231
+ this.set(segments, subtree);
232
+ if (this.enableInstrumentation) this.instrumentation.nodesTouched++;
233
+ continue;
234
+ }
235
+ if (typeof subtree === 'object') {
236
+ this.deleteSubtree(segments);
237
+ this.buildFromTree(subtree, segments);
238
+ if (this.enableInstrumentation) this.instrumentation.nodesTouched++;
239
+ } else {
240
+ this.deleteSubtree(segments);
241
+ }
242
+ }
243
+ if (this.enableInstrumentation) {
244
+ this.instrumentation.incrementalUpdates++;
245
+ this.instrumentation.rebuildDurationNs += (performance.now() - start) * 1e6;
246
+ }
247
+ }
248
+ setInstrumentation(enabled) {
249
+ this.enableInstrumentation = enabled;
250
+ }
251
+ getInstrumentation(reset = false) {
252
+ const snapshot = Object.assign({}, this.instrumentation);
253
+ if (reset) {
254
+ this.instrumentation.incrementalUpdates = 0;
255
+ this.instrumentation.fullRebuilds = 0;
256
+ this.instrumentation.nodesTouched = 0;
257
+ this.instrumentation.deletions = 0;
258
+ this.instrumentation.rebuildDurationNs = 0;
259
+ }
260
+ return snapshot;
261
+ }
157
262
  }
158
263
 
159
264
  export { PathIndex };
@@ -1,12 +1,53 @@
1
1
  import { __awaiter } from 'tslib';
2
2
 
3
+ const defaultConfig = {
4
+ yieldEveryTasks: 500,
5
+ yieldEveryMs: 8,
6
+ instrumentation: false
7
+ };
8
+ let config = Object.assign({}, defaultConfig);
9
+ let metrics = {
10
+ drainCycles: 0,
11
+ tasksExecuted: 0,
12
+ maxQueueLength: 0,
13
+ yields: 0,
14
+ lastDrainDurationMs: 0,
15
+ totalDrainDurationMs: 0
16
+ };
3
17
  const q = [];
18
+ let draining = false;
19
+ function configureScheduler(newConfig) {
20
+ config = Object.assign(Object.assign({}, config), newConfig);
21
+ }
22
+ function getSchedulerMetrics(reset = false) {
23
+ const snapshot = Object.assign({}, metrics);
24
+ if (reset) {
25
+ metrics = {
26
+ drainCycles: 0,
27
+ tasksExecuted: 0,
28
+ maxQueueLength: 0,
29
+ yields: 0,
30
+ lastDrainDurationMs: 0,
31
+ totalDrainDurationMs: 0
32
+ };
33
+ }
34
+ return snapshot;
35
+ }
4
36
  function postTask(t) {
5
37
  q.push(t);
6
- if (q.length === 1) flush();
38
+ if (config.instrumentation && q.length > metrics.maxQueueLength) {
39
+ metrics.maxQueueLength = q.length;
40
+ }
41
+ if (!draining) {
42
+ draining = true;
43
+ Promise.resolve().then(drain);
44
+ }
7
45
  }
8
- function flush() {
46
+ function drain() {
9
47
  return __awaiter(this, void 0, void 0, function* () {
48
+ const start = config.instrumentation ? performance.now() : 0;
49
+ if (config.instrumentation) metrics.drainCycles++;
50
+ let tasksSinceYield = 0;
10
51
  while (q.length) {
11
52
  const t = q.shift();
12
53
  try {
@@ -14,9 +55,21 @@ function flush() {
14
55
  } catch (e) {
15
56
  console.error('[EnterpriseScheduler]', e);
16
57
  }
17
- yield Promise.resolve();
58
+ tasksSinceYield++;
59
+ if (config.instrumentation) metrics.tasksExecuted++;
60
+ if (tasksSinceYield >= config.yieldEveryTasks || config.instrumentation && performance.now() - start >= config.yieldEveryMs) {
61
+ if (config.instrumentation) metrics.yields++;
62
+ tasksSinceYield = 0;
63
+ yield Promise.resolve();
64
+ }
65
+ }
66
+ if (config.instrumentation) {
67
+ const duration = performance.now() - start;
68
+ metrics.lastDrainDurationMs = duration;
69
+ metrics.totalDrainDurationMs += duration;
18
70
  }
71
+ draining = false;
19
72
  });
20
73
  }
21
74
 
22
- export { postTask };
75
+ export { configureScheduler, getSchedulerMetrics, postTask };
@@ -166,14 +166,26 @@ class OptimizedUpdateEngine {
166
166
  }
167
167
  }
168
168
  isEqual(a, b) {
169
- if (a === b) {
169
+ if (a === b) return true;
170
+ if (typeof a !== typeof b) return false;
171
+ if (a === null || b === null) return false;
172
+ if (typeof a !== 'object') return false;
173
+ const ao = a;
174
+ const bo = b;
175
+ if (Array.isArray(ao) && Array.isArray(bo)) {
176
+ if (ao.length !== bo.length) return false;
177
+ for (let i = 0; i < ao.length; i++) {
178
+ if (ao[i] !== bo[i]) return false;
179
+ }
170
180
  return true;
171
181
  }
172
- if (typeof a !== typeof b) {
173
- return false;
174
- }
175
- if (typeof a !== 'object' || a === null || b === null) {
176
- return false;
182
+ const aKeys = Object.keys(ao);
183
+ const bKeys = Object.keys(bo);
184
+ if (aKeys.length !== bKeys.length) return false;
185
+ for (let i = 0; i < aKeys.length; i++) {
186
+ const k = aKeys[i];
187
+ if (!(k in bo)) return false;
188
+ if (ao[k] !== bo[k]) return false;
177
189
  }
178
190
  try {
179
191
  return JSON.stringify(a) === JSON.stringify(b);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signaltree/enterprise",
3
- "version": "4.1.2",
3
+ "version": "4.1.4",
4
4
  "description": "Enterprise-grade optimizations for SignalTree. Provides diff-based updates, bulk operation optimization, and advanced change tracking for large-scale applications.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -10,38 +10,31 @@
10
10
  "exports": {
11
11
  ".": {
12
12
  "import": "./dist/index.js",
13
- "default": "./dist/index.js",
14
- "types": "./src/index.d.ts"
13
+ "default": "./dist/index.js"
15
14
  },
16
15
  "./diff-engine": {
17
16
  "import": "./dist/lib/diff-engine.js",
18
- "default": "./dist/lib/diff-engine.js",
19
- "types": "./src/lib/diff-engine.d.ts"
17
+ "default": "./dist/lib/diff-engine.js"
20
18
  },
21
19
  "./path-index": {
22
20
  "import": "./dist/lib/path-index.js",
23
- "default": "./dist/lib/path-index.js",
24
- "types": "./src/lib/path-index.d.ts"
21
+ "default": "./dist/lib/path-index.js"
25
22
  },
26
23
  "./update-engine": {
27
24
  "import": "./dist/lib/update-engine.js",
28
- "default": "./dist/lib/update-engine.js",
29
- "types": "./src/lib/update-engine.d.ts"
25
+ "default": "./dist/lib/update-engine.js"
30
26
  },
31
27
  "./enterprise-enhancer": {
32
28
  "import": "./dist/lib/enterprise-enhancer.js",
33
- "default": "./dist/lib/enterprise-enhancer.js",
34
- "types": "./src/lib/enterprise-enhancer.d.ts"
29
+ "default": "./dist/lib/enterprise-enhancer.js"
35
30
  },
36
31
  "./scheduler": {
37
32
  "import": "./dist/lib/scheduler.js",
38
- "default": "./dist/lib/scheduler.js",
39
- "types": "./src/lib/scheduler.d.ts"
33
+ "default": "./dist/lib/scheduler.js"
40
34
  },
41
35
  "./thread-pools": {
42
36
  "import": "./dist/lib/thread-pools.js",
43
- "default": "./dist/lib/thread-pools.js",
44
- "types": "./src/lib/thread-pools.d.ts"
37
+ "default": "./dist/lib/thread-pools.js"
45
38
  }
46
39
  },
47
40
  "keywords": [
@@ -68,7 +61,7 @@
68
61
  },
69
62
  "peerDependencies": {
70
63
  "@angular/core": "^20.3.0",
71
- "@signaltree/core": "4.1.2",
64
+ "@signaltree/core": "4.1.4",
72
65
  "tslib": "^2.0.0"
73
66
  },
74
67
  "devDependencies": {