@signaltree/core 6.0.14 → 6.2.0

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,126 +1,165 @@
1
- import { applyState, isNodeAccessor } from '../../lib/utils.js';
1
+ import { copyTreeProperties } from '../utils/copy-tree-properties.js';
2
2
 
3
- let updateQueue = [];
4
- let isUpdating = false;
5
- let flushTimeoutId;
6
- let currentBatchingConfig = {};
7
- function addToQueue(update, config = currentBatchingConfig) {
8
- const maxSize = config.maxBatchSize ?? 100;
9
- if (update.path) {
10
- updateQueue = updateQueue.filter(existing => existing.path !== update.path);
11
- }
12
- updateQueue.push(update);
13
- if (updateQueue.length > maxSize) {
14
- flushUpdates();
15
- return true;
16
- }
17
- scheduleFlush(config);
18
- return false;
19
- }
20
- function scheduleFlush(config) {
21
- if (flushTimeoutId !== undefined) {
22
- clearTimeout(flushTimeoutId);
23
- }
24
- const delay = config.autoFlushDelay ?? config.debounceMs ?? 16;
25
- flushTimeoutId = setTimeout(() => {
26
- flushUpdates();
27
- }, delay);
28
- }
29
- function flushUpdates() {
30
- if (isUpdating) return;
31
- let queue;
32
- do {
33
- if (updateQueue.length === 0) return;
34
- isUpdating = true;
35
- queue = updateQueue;
36
- updateQueue = [];
37
- if (flushTimeoutId !== undefined) {
38
- clearTimeout(flushTimeoutId);
39
- flushTimeoutId = undefined;
40
- }
41
- queue.sort((a, b) => (b.depth ?? 0) - (a.depth ?? 0));
42
- try {
43
- queue.forEach(({
44
- fn
45
- }) => fn());
46
- } finally {
47
- isUpdating = false;
48
- }
49
- } while (updateQueue.length > 0);
50
- }
51
- function batchUpdates(fn, path) {
52
- const startTime = performance.now();
53
- const depth = 0;
54
- const update = {
55
- fn,
56
- startTime,
57
- depth,
58
- path
59
- };
60
- const wasFlushed = addToQueue(update, currentBatchingConfig);
61
- if (!wasFlushed) {
62
- const isTimedOut = currentBatchingConfig.batchTimeoutMs && updateQueue.length > 0 && startTime - updateQueue[0].startTime >= currentBatchingConfig.batchTimeoutMs;
63
- if (isTimedOut) {
64
- flushUpdates();
65
- } else if (!isUpdating && updateQueue.length > 0) {
66
- queueMicrotask(() => {
67
- flushUpdates();
68
- });
69
- }
70
- }
71
- }
72
- function batchingWithConfig(config = {}) {
3
+ function batching(config = {}) {
73
4
  const enabled = config.enabled ?? true;
74
- if (enabled) {
75
- currentBatchingConfig = {
76
- ...currentBatchingConfig,
77
- ...config
78
- };
79
- }
80
- const enhancer = tree => {
5
+ const notificationDelayMs = config.notificationDelayMs ?? 0;
6
+ return tree => {
81
7
  if (!enabled) {
8
+ const passthrough = {
9
+ batch: fn => fn(),
10
+ coalesce: fn => fn(),
11
+ hasPendingNotifications: () => false,
12
+ flushNotifications: () => {}
13
+ };
82
14
  const enhanced = tree;
83
- enhanced.batch = updater => {
84
- try {
15
+ Object.assign(enhanced, passthrough);
16
+ enhanced.batchUpdate = updater => {
17
+ if (typeof tree.batchUpdate === 'function') {
85
18
  tree.batchUpdate(updater);
86
- } catch {
87
- try {
88
- updater(enhanced.state);
89
- } catch {}
19
+ } else {
20
+ updater(tree());
90
21
  }
91
22
  };
92
- enhanced.batchUpdate = updater => {
23
+ return enhanced;
24
+ }
25
+ let notificationPending = false;
26
+ let notificationTimeoutId;
27
+ let inBatch = false;
28
+ let inCoalesce = false;
29
+ const coalescedUpdates = new Map();
30
+ const scheduleNotification = () => {
31
+ if (notificationPending) return;
32
+ notificationPending = true;
33
+ if (notificationDelayMs > 0) {
34
+ notificationTimeoutId = setTimeout(flushNotificationsInternal, notificationDelayMs);
35
+ } else {
36
+ queueMicrotask(flushNotificationsInternal);
37
+ }
38
+ };
39
+ const flushNotificationsInternal = () => {
40
+ if (!notificationPending) return;
41
+ notificationPending = false;
42
+ if (notificationTimeoutId !== undefined) {
43
+ clearTimeout(notificationTimeoutId);
44
+ notificationTimeoutId = undefined;
45
+ }
46
+ if (tree.__notifyChangeDetection) {
47
+ tree.__notifyChangeDetection();
48
+ }
49
+ };
50
+ const flushCoalescedUpdates = () => {
51
+ const updates = Array.from(coalescedUpdates.values());
52
+ coalescedUpdates.clear();
53
+ updates.forEach(fn => {
93
54
  try {
94
- const current = tree();
95
- const updates = updater(current);
96
- applyState(enhanced.state, updates);
97
- } catch (err) {
98
- try {
99
- tree.batchUpdate(updater);
100
- } catch {}
55
+ fn();
56
+ } catch (e) {
57
+ console.error('[SignalTree] Error in coalesced update:', e);
101
58
  }
102
- };
103
- return enhanced;
59
+ });
60
+ };
61
+ const wrapSignalSetters = (node, path = '') => {
62
+ if (!node || typeof node !== 'object') return;
63
+ if (typeof node.set === 'function' && !node.__batchingWrapped) {
64
+ const originalSet = node.set.bind(node);
65
+ node.set = value => {
66
+ if (inCoalesce) {
67
+ coalescedUpdates.set(path, () => originalSet(value));
68
+ } else {
69
+ originalSet(value);
70
+ }
71
+ if (!inBatch) {
72
+ scheduleNotification();
73
+ }
74
+ };
75
+ node.__batchingWrapped = true;
76
+ }
77
+ if (typeof node.update === 'function' && !node.__batchingUpdateWrapped) {
78
+ const originalUpdate = node.update.bind(node);
79
+ node.update = updater => {
80
+ if (inCoalesce) {
81
+ coalescedUpdates.set(`${path}:update:${Date.now()}`, () => originalUpdate(updater));
82
+ } else {
83
+ originalUpdate(updater);
84
+ }
85
+ if (!inBatch) {
86
+ scheduleNotification();
87
+ }
88
+ };
89
+ node.__batchingUpdateWrapped = true;
90
+ }
91
+ for (const key of Object.keys(node)) {
92
+ if (key.startsWith('_') || key === 'set' || key === 'update') continue;
93
+ const child = node[key];
94
+ if (child && typeof child === 'object') {
95
+ wrapSignalSetters(child, path ? `${path}.${key}` : key);
96
+ }
97
+ }
98
+ };
99
+ if (tree.$) {
100
+ wrapSignalSetters(tree.$);
104
101
  }
102
+ const batchingMethods = {
103
+ batch(fn) {
104
+ const wasBatching = inBatch;
105
+ inBatch = true;
106
+ try {
107
+ fn();
108
+ } finally {
109
+ inBatch = wasBatching;
110
+ if (!inBatch) {
111
+ scheduleNotification();
112
+ }
113
+ }
114
+ },
115
+ coalesce(fn) {
116
+ const wasCoalescing = inCoalesce;
117
+ const wasBatching = inBatch;
118
+ inCoalesce = true;
119
+ inBatch = true;
120
+ try {
121
+ fn();
122
+ } finally {
123
+ inCoalesce = wasCoalescing;
124
+ inBatch = wasBatching;
125
+ if (!wasCoalescing) {
126
+ flushCoalescedUpdates();
127
+ }
128
+ if (!inBatch) {
129
+ scheduleNotification();
130
+ }
131
+ }
132
+ },
133
+ hasPendingNotifications() {
134
+ return notificationPending;
135
+ },
136
+ flushNotifications() {
137
+ flushNotificationsInternal();
138
+ }
139
+ };
105
140
  const originalTreeCall = tree.bind(tree);
106
141
  const enhancedTree = function (...args) {
107
142
  if (args.length === 0) {
108
143
  return originalTreeCall();
109
144
  } else {
110
- batchUpdates(() => {
111
- if (args.length === 1) {
112
- const arg = args[0];
113
- if (typeof arg === 'function') {
114
- originalTreeCall(arg);
115
- } else {
116
- originalTreeCall(arg);
117
- }
145
+ if (args.length === 1) {
146
+ const arg = args[0];
147
+ if (typeof arg === 'function') {
148
+ originalTreeCall(arg);
149
+ } else {
150
+ originalTreeCall(arg);
118
151
  }
119
- });
152
+ }
153
+ if (!inBatch) {
154
+ scheduleNotification();
155
+ }
120
156
  }
121
157
  };
122
158
  Object.setPrototypeOf(enhancedTree, Object.getPrototypeOf(tree));
123
159
  Object.assign(enhancedTree, tree);
160
+ try {
161
+ copyTreeProperties(tree, enhancedTree);
162
+ } catch {}
124
163
  if ('state' in tree) {
125
164
  Object.defineProperty(enhancedTree, 'state', {
126
165
  value: tree.state,
@@ -135,15 +174,16 @@ function batchingWithConfig(config = {}) {
135
174
  configurable: true
136
175
  });
137
176
  }
177
+ Object.assign(enhancedTree, batchingMethods);
138
178
  enhancedTree.batchUpdate = updater => {
139
- batchUpdates(() => {
179
+ enhancedTree.batch(() => {
140
180
  const current = originalTreeCall();
141
181
  const updates = updater(current);
142
182
  Object.entries(updates).forEach(([key, value]) => {
143
183
  const property = enhancedTree.state[key];
144
- if (property && 'set' in property) {
184
+ if (property && typeof property.set === 'function') {
145
185
  property.set(value);
146
- } else if (isNodeAccessor(property)) {
186
+ } else if (typeof property === 'function') {
147
187
  property(value);
148
188
  }
149
189
  });
@@ -151,38 +191,28 @@ function batchingWithConfig(config = {}) {
151
191
  };
152
192
  return enhancedTree;
153
193
  };
154
- return enhancer;
155
- }
156
- function batching(config = {}) {
157
- return batchingWithConfig(config);
158
194
  }
159
195
  function highPerformanceBatching() {
160
- return batchingWithConfig({
196
+ return batching({
161
197
  enabled: true,
162
- maxBatchSize: 200,
163
- debounceMs: 0
198
+ notificationDelayMs: 0
164
199
  });
165
200
  }
201
+ function batchingWithConfig(config = {}) {
202
+ return batching(config);
203
+ }
166
204
  function flushBatchedUpdates() {
167
- if (updateQueue.length > 0) {
168
- const queue = updateQueue.slice();
169
- updateQueue = [];
170
- isUpdating = false;
171
- queue.sort((a, b) => (b.depth ?? 0) - (a.depth ?? 0));
172
- queue.forEach(({
173
- fn
174
- }) => {
175
- fn();
176
- });
177
- }
205
+ console.warn('[SignalTree] flushBatchedUpdates() is deprecated. Use tree.flushNotifications() instead.');
178
206
  }
179
207
  function hasPendingUpdates() {
180
- return updateQueue.length > 0;
208
+ console.warn('[SignalTree] hasPendingUpdates() is deprecated. Use tree.hasPendingNotifications() instead.');
209
+ return false;
181
210
  }
182
211
  function getBatchQueueSize() {
183
- return updateQueue.length;
212
+ console.warn('[SignalTree] getBatchQueueSize() is deprecated. Signal writes are now synchronous.');
213
+ return 0;
184
214
  }
185
- Object.assign((config = {}) => batchingWithConfig(config), {
215
+ Object.assign((config = {}) => batching(config), {
186
216
  highPerformance: highPerformanceBatching
187
217
  });
188
218
 
@@ -0,0 +1,20 @@
1
+ function copyTreeProperties(source, target) {
2
+ const skipKeys = new Set(['state', '$']);
3
+ for (const key of Object.getOwnPropertyNames(source)) {
4
+ if (skipKeys.has(key)) continue;
5
+ if (Object.prototype.hasOwnProperty.call(target, key)) continue;
6
+ const descriptor = Object.getOwnPropertyDescriptor(source, key);
7
+ if (!descriptor) continue;
8
+ if (descriptor.configurable === false) continue;
9
+ Object.defineProperty(target, key, descriptor);
10
+ }
11
+ for (const sym of Object.getOwnPropertySymbols(source)) {
12
+ if (Object.prototype.hasOwnProperty.call(target, sym)) continue;
13
+ const descriptor = Object.getOwnPropertyDescriptor(source, sym);
14
+ if (!descriptor) continue;
15
+ if (descriptor.configurable === false) continue;
16
+ Object.defineProperty(target, sym, descriptor);
17
+ }
18
+ }
19
+
20
+ export { copyTreeProperties };
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ export { createEditSession } from './lib/edit-session.js';
5
5
  export { getPathNotifier } from './lib/path-notifier.js';
6
6
  export { SecurityPresets, SecurityValidator } from './lib/security/security-validator.js';
7
7
  export { createEnhancer, resolveEnhancerOrder } from './enhancers/index.js';
8
- export { batching, flushBatchedUpdates, getBatchQueueSize, hasPendingUpdates, highPerformanceBatching } from './enhancers/batching/batching.js';
8
+ export { batching, batchingWithConfig, flushBatchedUpdates, getBatchQueueSize, hasPendingUpdates, highPerformanceBatching } from './enhancers/batching/batching.js';
9
9
  export { clearAllCaches, computedMemoization, deepStateMemoization, getGlobalCacheStats, highFrequencyMemoization, highPerformanceMemoization, lightweightMemoization, memoization, memoize, memoizeReference, memoizeShallow, selectorMemoization, shallowMemoization } from './enhancers/memoization/memoization.js';
10
10
  export { enableTimeTravel, timeTravel } from './enhancers/time-travel/time-travel.js';
11
11
  export { enableEntities, entities, highPerformanceEntities } from './enhancers/entities/entities.js';
package/dist/lib/utils.js CHANGED
@@ -260,40 +260,5 @@ function unwrap(node) {
260
260
  function snapshotState(state) {
261
261
  return unwrap(state);
262
262
  }
263
- function applyState(stateNode, snapshot) {
264
- if (snapshot === null || snapshot === undefined) return;
265
- if (typeof snapshot !== 'object') return;
266
- for (const key of Object.keys(snapshot)) {
267
- const val = snapshot[key];
268
- const target = stateNode[key];
269
- if (isNodeAccessor(target)) {
270
- if (val && typeof val === 'object') {
271
- try {
272
- applyState(target, val);
273
- } catch {
274
- try {
275
- target(val);
276
- } catch {}
277
- }
278
- } else {
279
- try {
280
- target(val);
281
- } catch {}
282
- }
283
- } else if (isSignal(target)) {
284
- try {
285
- target.set?.(val);
286
- } catch {
287
- try {
288
- target(val);
289
- } catch {}
290
- }
291
- } else {
292
- try {
293
- stateNode[key] = val;
294
- } catch {}
295
- }
296
- }
297
- }
298
263
 
299
- export { applyState, composeEnhancers, createLazySignalTree, isAnySignal, isBuiltInObject, isEntityMapMarker, isNodeAccessor, snapshotState, toWritableSignal, unwrap };
264
+ export { composeEnhancers, createLazySignalTree, isAnySignal, isBuiltInObject, isEntityMapMarker, isNodeAccessor, snapshotState, toWritableSignal, unwrap };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signaltree/core",
3
- "version": "6.0.14",
3
+ "version": "6.2.0",
4
4
  "description": "Lightweight, type-safe signal-based state management for Angular. Core package providing hierarchical signal trees, basic entity management, and async actions.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -1,8 +1,7 @@
1
- import type { ISignalTree, BatchingConfig } from '../../lib/types';
2
- export type BatchingMethods<T> = import('../../lib/types').BatchingMethods<T>;
3
- export declare function batchingWithConfig(config?: BatchingConfig): <T>(tree: ISignalTree<T>) => ISignalTree<T> & BatchingMethods<T>;
1
+ import type { ISignalTree, BatchingConfig, BatchingMethods } from '../../lib/types';
4
2
  export declare function batching(config?: BatchingConfig): <T>(tree: ISignalTree<T>) => ISignalTree<T> & BatchingMethods<T>;
5
3
  export declare function highPerformanceBatching(): <T>(tree: ISignalTree<T>) => ISignalTree<T> & BatchingMethods<T>;
4
+ export declare function batchingWithConfig(config?: BatchingConfig): <T>(tree: ISignalTree<T>) => ISignalTree<T> & BatchingMethods<T>;
6
5
  export declare function flushBatchedUpdates(): void;
7
6
  export declare function hasPendingUpdates(): boolean;
8
7
  export declare function getBatchQueueSize(): number;
@@ -0,0 +1 @@
1
+ export declare function copyTreeProperties<T extends object>(source: T, target: T): void;
package/src/index.d.ts CHANGED
@@ -7,7 +7,8 @@ export { getPathNotifier } from './lib/path-notifier';
7
7
  export { SecurityValidator, SecurityPresets, type SecurityEvent, type SecurityEventType, type SecurityValidatorConfig, } from './lib/security/security-validator';
8
8
  export { createEnhancer, resolveEnhancerOrder } from './enhancers/index';
9
9
  export { ENHANCER_META } from './lib/types';
10
- export { batching, highPerformanceBatching, flushBatchedUpdates, hasPendingUpdates, getBatchQueueSize, } from './enhancers/batching/batching';
10
+ export { batching, batchingWithConfig, highPerformanceBatching, flushBatchedUpdates, hasPendingUpdates, getBatchQueueSize, } from './enhancers/batching/batching';
11
+ export type { BatchingConfig, BatchingMethods } from './lib/types';
11
12
  export { memoization, selectorMemoization, computedMemoization, deepStateMemoization, highFrequencyMemoization, highPerformanceMemoization, lightweightMemoization, shallowMemoization, memoize, memoizeShallow, memoizeReference, clearAllCaches, getGlobalCacheStats, } from './enhancers/memoization/memoization';
12
13
  export { timeTravel, enableTimeTravel, } from './enhancers/time-travel/time-travel';
13
14
  export { entities, enableEntities, highPerformanceEntities, } from './enhancers/entities/entities';
@@ -47,13 +47,13 @@ export interface EffectsMethods<T> {
47
47
  }
48
48
  export interface BatchingConfig {
49
49
  enabled?: boolean;
50
- debounceMs?: number;
51
- batchTimeoutMs?: number;
52
- autoFlushDelay?: number;
53
- maxBatchSize?: number;
50
+ notificationDelayMs?: number;
54
51
  }
55
52
  export interface BatchingMethods<T = unknown> {
56
53
  batch(fn: () => void): void;
54
+ coalesce(fn: () => void): void;
55
+ hasPendingNotifications(): boolean;
56
+ flushNotifications(): void;
57
57
  }
58
58
  export interface MemoizationMethods<T> {
59
59
  memoize<R>(fn: (state: T) => R, cacheKey?: string): Signal<R>;