@blac/core 2.0.0-rc-13 → 2.0.0-rc-16

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 (54) hide show
  1. package/package.json +10 -15
  2. package/src/Blac.ts +38 -71
  3. package/src/Bloc.ts +8 -9
  4. package/src/BlocBase.ts +246 -540
  5. package/src/Cubit.ts +1 -16
  6. package/src/__tests__/Blac.config.test.ts +10 -17
  7. package/src/__tests__/Bloc.event.test.ts +27 -52
  8. package/src/__tests__/BlocBase.subscription.test.ts +349 -0
  9. package/src/__tests__/Cubit.test.ts +39 -50
  10. package/src/__tests__/memory-leaks.test.ts +383 -0
  11. package/src/__tests__/plugins.test.ts +107 -97
  12. package/src/adapter/BlacAdapter.ts +347 -207
  13. package/src/adapter/ProxyFactory.ts +196 -255
  14. package/src/adapter/__tests__/BlacAdapter.test.ts +71 -77
  15. package/src/adapter/__tests__/ProxyFactory.test.ts +1 -1
  16. package/src/adapter/index.ts +0 -1
  17. package/src/errors/BlacError.ts +69 -0
  18. package/src/errors/ErrorManager.ts +101 -0
  19. package/src/errors/handleError.ts +114 -0
  20. package/src/errors/index.ts +13 -0
  21. package/src/events.ts +7 -1
  22. package/src/index.ts +7 -1
  23. package/src/lifecycle/BlocLifecycle.ts +156 -0
  24. package/src/lifecycle/index.ts +1 -0
  25. package/src/plugins/BlocPluginRegistry.ts +71 -51
  26. package/src/plugins/SystemPluginRegistry.ts +63 -13
  27. package/src/plugins/__tests__/BlocPluginRegistry.test.ts +450 -0
  28. package/src/plugins/__tests__/SystemPluginRegistry.test.ts +404 -0
  29. package/src/plugins/types.ts +32 -3
  30. package/src/subscription/README.md +132 -0
  31. package/src/subscription/SubscriptionManager.ts +395 -0
  32. package/src/subscription/__tests__/SubscriptionManager.test.ts +355 -0
  33. package/src/subscription/index.ts +2 -0
  34. package/src/subscription/types.ts +90 -0
  35. package/src/testing.ts +31 -37
  36. package/src/types.ts +4 -21
  37. package/src/utils/BatchingManager.ts +59 -0
  38. package/src/utils/RerenderLogger.ts +265 -0
  39. package/src/utils/__tests__/generateInstanceId.test.ts +104 -0
  40. package/src/utils/__tests__/shallowEqual.test.ts +171 -0
  41. package/src/utils/__tests__/uuid.test.ts +144 -0
  42. package/src/utils/generateInstanceId.ts +54 -0
  43. package/src/utils/shallowEqual.ts +1 -1
  44. package/README.md +0 -329
  45. package/dist/index.cjs.js +0 -11
  46. package/dist/index.cjs.js.map +0 -1
  47. package/dist/index.d.ts +0 -1078
  48. package/dist/index.es.js +0 -1992
  49. package/dist/index.es.js.map +0 -1
  50. package/src/BlacObserver.ts +0 -189
  51. package/src/__tests__/BlocBase.lifecycle.test.ts +0 -441
  52. package/src/__tests__/props.test.ts +0 -256
  53. package/src/adapter/ConsumerTracker.ts +0 -241
  54. package/src/adapter/__tests__/ConsumerTracker.test.ts +0 -562
@@ -0,0 +1,355 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { SubscriptionManager } from '../SubscriptionManager';
3
+ import { BlocBase } from '../../BlocBase';
4
+ import { Cubit } from '../../Cubit';
5
+
6
+ class TestCubit extends Cubit<{ count: number; nested: { value: string } }> {
7
+ increment = () => {
8
+ this.emit({ ...this.state, count: this.state.count + 1 });
9
+ };
10
+
11
+ updateNested = (value: string) => {
12
+ this.emit({ ...this.state, nested: { value } });
13
+ };
14
+ }
15
+
16
+ describe('SubscriptionManager', () => {
17
+ let cubit: TestCubit;
18
+ let manager: SubscriptionManager<{
19
+ count: number;
20
+ nested: { value: string };
21
+ }>;
22
+
23
+ beforeEach(() => {
24
+ cubit = new TestCubit({ count: 0, nested: { value: 'initial' } });
25
+ manager = new SubscriptionManager(cubit);
26
+ });
27
+
28
+ describe('subscribe', () => {
29
+ it('should create a subscription and return unsubscribe function', () => {
30
+ const notify = vi.fn();
31
+ const unsubscribe = manager.subscribe({
32
+ type: 'observer',
33
+ notify,
34
+ });
35
+
36
+ expect(manager.size).toBe(1);
37
+
38
+ unsubscribe();
39
+ expect(manager.size).toBe(0);
40
+ });
41
+
42
+ it('should initialize selector value on subscription', () => {
43
+ const notify = vi.fn();
44
+ const selector = vi.fn((state) => state.count);
45
+
46
+ manager.subscribe({
47
+ type: 'consumer',
48
+ selector,
49
+ notify,
50
+ });
51
+
52
+ expect(selector).toHaveBeenCalledWith(cubit.state, cubit);
53
+ });
54
+
55
+ it('should handle selector errors gracefully', () => {
56
+ const notify = vi.fn();
57
+ const selector = vi.fn(() => {
58
+ throw new Error('Selector error');
59
+ });
60
+
61
+ // Should not throw
62
+ expect(() => {
63
+ manager.subscribe({
64
+ type: 'observer',
65
+ selector,
66
+ notify,
67
+ });
68
+ }).not.toThrow();
69
+ });
70
+ });
71
+
72
+ describe('notify', () => {
73
+ it('should notify subscriptions without selectors', () => {
74
+ const notify = vi.fn();
75
+ manager.subscribe({
76
+ type: 'observer',
77
+ notify,
78
+ });
79
+
80
+ const oldState = { count: 0, nested: { value: 'old' } };
81
+ const newState = { count: 1, nested: { value: 'new' } };
82
+
83
+ manager.notify(newState, oldState);
84
+
85
+ expect(notify).toHaveBeenCalledWith(newState, oldState, undefined);
86
+ });
87
+
88
+ it('should use selector for change detection', () => {
89
+ const notify = vi.fn();
90
+ const selector = (state: any) => state.count;
91
+
92
+ manager.subscribe({
93
+ type: 'consumer',
94
+ selector,
95
+ notify,
96
+ });
97
+
98
+ // Same count, should not notify
99
+ manager.notify(
100
+ { count: 0, nested: { value: 'new' } },
101
+ { count: 0, nested: { value: 'old' } },
102
+ );
103
+ expect(notify).not.toHaveBeenCalled();
104
+
105
+ // Different count, should notify
106
+ manager.notify(
107
+ { count: 1, nested: { value: 'new' } },
108
+ { count: 0, nested: { value: 'old' } },
109
+ );
110
+ expect(notify).toHaveBeenCalledWith(1, 0, undefined);
111
+ });
112
+
113
+ it('should use custom equality function', () => {
114
+ const notify = vi.fn();
115
+ const selector = (state: any) => state.nested;
116
+ const equalityFn = (a: any, b: any) => a.value === b.value;
117
+
118
+ manager.subscribe({
119
+ type: 'observer',
120
+ selector,
121
+ equalityFn,
122
+ notify,
123
+ });
124
+
125
+ // First notification to establish baseline
126
+ manager.notify(
127
+ { count: 0, nested: { value: 'initial' } },
128
+ { count: 0, nested: { value: 'initial' } },
129
+ );
130
+ notify.mockClear();
131
+
132
+ // Same nested value, should not notify
133
+ manager.notify(
134
+ { count: 1, nested: { value: 'initial' } },
135
+ { count: 0, nested: { value: 'initial' } },
136
+ );
137
+ expect(notify).not.toHaveBeenCalled();
138
+
139
+ // Different nested value, should notify
140
+ manager.notify(
141
+ { count: 1, nested: { value: 'new' } },
142
+ { count: 0, nested: { value: 'initial' } },
143
+ );
144
+ expect(notify).toHaveBeenCalled();
145
+ });
146
+
147
+ it('should respect priority order', () => {
148
+ const callOrder: string[] = [];
149
+
150
+ manager.subscribe({
151
+ type: 'observer',
152
+ priority: 1,
153
+ notify: () => callOrder.push('low'),
154
+ });
155
+
156
+ manager.subscribe({
157
+ type: 'observer',
158
+ priority: 10,
159
+ notify: () => callOrder.push('high'),
160
+ });
161
+
162
+ manager.subscribe({
163
+ type: 'observer',
164
+ priority: 5,
165
+ notify: () => callOrder.push('medium'),
166
+ });
167
+
168
+ manager.notify(
169
+ { count: 1, nested: { value: 'new' } },
170
+ { count: 0, nested: { value: 'old' } },
171
+ );
172
+
173
+ expect(callOrder).toEqual(['high', 'medium', 'low']);
174
+ });
175
+
176
+ it('should handle notify errors gracefully', () => {
177
+ const goodNotify = vi.fn();
178
+ const badNotify = vi.fn(() => {
179
+ throw new Error('Notify error');
180
+ });
181
+
182
+ manager.subscribe({ type: 'observer', notify: badNotify });
183
+ manager.subscribe({ type: 'observer', notify: goodNotify });
184
+
185
+ // Should not throw and should call good notify
186
+ expect(() => {
187
+ manager.notify(
188
+ { count: 1, nested: { value: 'new' } },
189
+ { count: 0, nested: { value: 'old' } },
190
+ );
191
+ }).not.toThrow();
192
+
193
+ expect(goodNotify).toHaveBeenCalled();
194
+ });
195
+ });
196
+
197
+ describe('trackAccess', () => {
198
+ it('should track path dependencies', () => {
199
+ const notify = vi.fn();
200
+ const unsubscribe = manager.subscribe({
201
+ type: 'consumer',
202
+ notify,
203
+ });
204
+
205
+ // Get subscription ID (hacky but works for testing)
206
+ const subscriptionId = Array.from(
207
+ (manager as any).subscriptions.keys(),
208
+ )[0] as string;
209
+
210
+ manager.trackAccess(subscriptionId, 'count');
211
+ manager.trackAccess(subscriptionId, 'nested.value');
212
+
213
+ const subscription = manager.getSubscription(subscriptionId);
214
+ expect(subscription?.dependencies).toContain('count');
215
+ expect(subscription?.dependencies).toContain('nested.value');
216
+ });
217
+
218
+ it('should update metadata on access', () => {
219
+ const notify = vi.fn();
220
+ manager.subscribe({ type: 'consumer', notify });
221
+
222
+ const subscriptionId = Array.from(
223
+ (manager as any).subscriptions.keys(),
224
+ )[0] as string;
225
+
226
+ manager.trackAccess(subscriptionId, 'count');
227
+
228
+ const subscription = manager.getSubscription(subscriptionId);
229
+ expect(subscription?.metadata?.accessCount).toBe(1);
230
+ expect(subscription?.metadata?.lastAccessTime).toBeGreaterThan(0);
231
+ });
232
+ });
233
+
234
+ describe('shouldNotifyForPaths', () => {
235
+ it('should return true for direct path match', () => {
236
+ const notify = vi.fn();
237
+ manager.subscribe({ type: 'consumer', notify });
238
+
239
+ const subscriptionId = Array.from(
240
+ (manager as any).subscriptions.keys(),
241
+ )[0] as string;
242
+ manager.trackAccess(subscriptionId, 'count');
243
+
244
+ const changedPaths = new Set(['count']);
245
+ expect(manager.shouldNotifyForPaths(subscriptionId, changedPaths)).toBe(
246
+ true,
247
+ );
248
+ });
249
+
250
+ it('should return true for nested path changes', () => {
251
+ const notify = vi.fn();
252
+ manager.subscribe({ type: 'consumer', notify });
253
+
254
+ const subscriptionId = Array.from(
255
+ (manager as any).subscriptions.keys(),
256
+ )[0] as string;
257
+ manager.trackAccess(subscriptionId, 'nested');
258
+
259
+ const changedPaths = new Set(['nested.value']);
260
+ expect(manager.shouldNotifyForPaths(subscriptionId, changedPaths)).toBe(
261
+ true,
262
+ );
263
+ });
264
+
265
+ it('should return false for unrelated path changes', () => {
266
+ const notify = vi.fn();
267
+ manager.subscribe({ type: 'consumer', notify });
268
+
269
+ const subscriptionId = Array.from(
270
+ (manager as any).subscriptions.keys(),
271
+ )[0] as string;
272
+ manager.trackAccess(subscriptionId, 'count');
273
+
274
+ const changedPaths = new Set(['nested.value']);
275
+ expect(manager.shouldNotifyForPaths(subscriptionId, changedPaths)).toBe(
276
+ false,
277
+ );
278
+ });
279
+ });
280
+
281
+ describe('weak reference handling', () => {
282
+ it('should clean up subscriptions with dead weak references', async () => {
283
+ let obj: any = { id: 'test' };
284
+ const weakRef = new WeakRef(obj);
285
+ const notify = vi.fn();
286
+
287
+ manager.subscribe({
288
+ type: 'consumer',
289
+ weakRef,
290
+ notify,
291
+ });
292
+
293
+ expect(manager.size).toBe(1);
294
+
295
+ // Clear strong reference
296
+ obj = null;
297
+
298
+ // Force garbage collection (this is platform-specific and may not work in all environments)
299
+ if ((global as any).gc) {
300
+ (global as any).gc();
301
+ }
302
+
303
+ // Trigger notification which should clean up dead refs
304
+ await new Promise((resolve) => setTimeout(resolve, 0));
305
+ manager.notify(
306
+ { count: 1, nested: { value: 'new' } },
307
+ { count: 0, nested: { value: 'old' } },
308
+ );
309
+
310
+ // Wait for microtask
311
+ await new Promise((resolve) => queueMicrotask(resolve));
312
+
313
+ // Note: This test may not reliably pass in all environments due to GC timing
314
+ // In production, the cleanup will happen eventually
315
+ });
316
+ });
317
+
318
+ describe('getStats', () => {
319
+ it('should return correct statistics', () => {
320
+ manager.subscribe({ type: 'consumer', notify: vi.fn() });
321
+ manager.subscribe({ type: 'observer', notify: vi.fn() });
322
+ manager.subscribe({ type: 'observer', notify: vi.fn() });
323
+
324
+ const stats = manager.getStats();
325
+
326
+ expect(stats.activeSubscriptions).toBe(3);
327
+ expect(stats.consumerCount).toBe(1);
328
+ expect(stats.observerCount).toBe(2);
329
+ expect(stats.totalNotifications).toBe(0);
330
+
331
+ // Trigger a notification
332
+ manager.notify(
333
+ { count: 1, nested: { value: 'new' } },
334
+ { count: 0, nested: { value: 'old' } },
335
+ );
336
+
337
+ const updatedStats = manager.getStats();
338
+ expect(updatedStats.totalNotifications).toBe(3); // All 3 subscriptions notified
339
+ });
340
+ });
341
+
342
+ describe('clear', () => {
343
+ it('should remove all subscriptions', () => {
344
+ manager.subscribe({ type: 'consumer', notify: vi.fn() });
345
+ manager.subscribe({ type: 'observer', notify: vi.fn() });
346
+
347
+ expect(manager.size).toBe(2);
348
+
349
+ manager.clear();
350
+
351
+ expect(manager.size).toBe(0);
352
+ expect(manager.hasSubscriptions).toBe(false);
353
+ });
354
+ });
355
+ });
@@ -0,0 +1,2 @@
1
+ export * from './types';
2
+ export * from './SubscriptionManager';
@@ -0,0 +1,90 @@
1
+ import { BlocBase } from '../BlocBase';
2
+
3
+ /**
4
+ * Unified subscription interface that replaces the dual consumer/observer system
5
+ */
6
+ export interface Subscription<S = unknown> {
7
+ /** Unique identifier for the subscription */
8
+ id: string;
9
+
10
+ /** Type of subscription for backward compatibility */
11
+ type: 'consumer' | 'observer';
12
+
13
+ /** Optional selector function to derive values from state */
14
+ selector?: (state: S, bloc?: BlocBase<S>) => unknown;
15
+
16
+ /** Custom equality function for selector results (defaults to Object.is) */
17
+ equalityFn?: (prev: unknown, next: unknown) => boolean;
18
+
19
+ /** Notification callback when state changes */
20
+ notify: (value: unknown, oldValue?: unknown, action?: unknown) => void;
21
+
22
+ /** Priority for notification order (higher = earlier) */
23
+ priority?: number;
24
+
25
+ /** WeakRef to the subscriber for automatic cleanup (React components) */
26
+ weakRef?: WeakRef<object>;
27
+
28
+ /** Last computed selector value for comparison */
29
+ lastValue?: unknown;
30
+
31
+ /** Tracked state path dependencies */
32
+ dependencies?: Set<string>;
33
+
34
+ /** Metadata for tracking and debugging */
35
+ metadata?: SubscriptionMetadata;
36
+ }
37
+
38
+ export interface SubscriptionMetadata {
39
+ /** Timestamp of last notification */
40
+ lastNotified?: number;
41
+
42
+ /** Whether the subscriber has rendered (React-specific) */
43
+ hasRendered?: boolean;
44
+
45
+ /** Number of times this subscription accessed state */
46
+ accessCount?: number;
47
+
48
+ /** First access timestamp */
49
+ firstAccessTime?: number;
50
+
51
+ /** Last access timestamp */
52
+ lastAccessTime?: number;
53
+ }
54
+
55
+ export interface SubscriptionOptions<S> {
56
+ /** Type of subscription */
57
+ type: 'consumer' | 'observer';
58
+
59
+ /** Optional selector function */
60
+ selector?: (state: S, bloc?: BlocBase<S>) => unknown;
61
+
62
+ /** Custom equality function */
63
+ equalityFn?: (prev: unknown, next: unknown) => boolean;
64
+
65
+ /** Notification callback */
66
+ notify: (value: unknown, oldValue?: unknown, action?: unknown) => void;
67
+
68
+ /** Priority for notifications */
69
+ priority?: number;
70
+
71
+ /** WeakRef to subscriber */
72
+ weakRef?: WeakRef<object>;
73
+ }
74
+
75
+ export interface SubscriptionManagerStats {
76
+ /** Total number of active subscriptions */
77
+ activeSubscriptions: number;
78
+
79
+ /** Number of consumer subscriptions */
80
+ consumerCount: number;
81
+
82
+ /** Number of observer subscriptions */
83
+ observerCount: number;
84
+
85
+ /** Total notifications sent */
86
+ totalNotifications: number;
87
+
88
+ /** Number of tracked dependencies */
89
+ trackedDependencies: number;
90
+ }
package/src/testing.ts CHANGED
@@ -57,15 +57,12 @@ export class BlocTest {
57
57
  );
58
58
  }, timeout);
59
59
 
60
- const unsubscribe = bloc._observer.subscribe({
61
- id: `test-waiter-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
62
- fn: (newState: S) => {
63
- if (predicate(newState)) {
64
- clearTimeout(timeoutId);
65
- unsubscribe();
66
- resolve(newState);
67
- }
68
- },
60
+ const unsubscribe = bloc.subscribe((newState: S) => {
61
+ if (predicate(newState)) {
62
+ clearTimeout(timeoutId);
63
+ unsubscribe();
64
+ resolve(newState);
65
+ }
69
66
  });
70
67
 
71
68
  // Check current state immediately
@@ -97,36 +94,33 @@ export class BlocTest {
97
94
  );
98
95
  }, timeout);
99
96
 
100
- const unsubscribe = bloc._observer.subscribe({
101
- id: `test-expecter-${crypto.randomUUID()}`,
102
- fn: (newState: S) => {
103
- receivedStates.push(newState);
104
-
105
- // Check if we have all expected states
106
- if (receivedStates.length === expectedStates.length) {
107
- clearTimeout(timeoutId);
108
- unsubscribe();
109
-
110
- // Verify all states match using deep equality
111
- for (let i = 0; i < expectedStates.length; i++) {
112
- const expected = expectedStates[i];
113
- const received = receivedStates[i];
114
-
115
- // Use JSON comparison for deep equality
116
- if (JSON.stringify(expected) !== JSON.stringify(received)) {
117
- reject(
118
- new Error(
119
- `State mismatch at index ${i}. Expected: ${JSON.stringify(expected)}, ` +
120
- `Received: ${JSON.stringify(received)}`,
121
- ),
122
- );
123
- return;
124
- }
97
+ const unsubscribe = bloc.subscribe((newState: S) => {
98
+ receivedStates.push(newState);
99
+
100
+ // Check if we have all expected states
101
+ if (receivedStates.length === expectedStates.length) {
102
+ clearTimeout(timeoutId);
103
+ unsubscribe();
104
+
105
+ // Verify all states match using deep equality
106
+ for (let i = 0; i < expectedStates.length; i++) {
107
+ const expected = expectedStates[i];
108
+ const received = receivedStates[i];
109
+
110
+ // Use JSON comparison for deep equality
111
+ if (JSON.stringify(expected) !== JSON.stringify(received)) {
112
+ reject(
113
+ new Error(
114
+ `State mismatch at index ${i}. Expected: ${JSON.stringify(expected)}, ` +
115
+ `Received: ${JSON.stringify(received)}`,
116
+ ),
117
+ );
118
+ return;
125
119
  }
126
-
127
- resolve();
128
120
  }
129
- },
121
+
122
+ resolve();
123
+ }
130
124
  });
131
125
  });
132
126
  }
package/src/types.ts CHANGED
@@ -12,9 +12,7 @@ export type BlocClassNoParams<B> = new (args: never[]) => B;
12
12
  /**
13
13
  * Represents the abstract base types for Bloc and Cubit
14
14
  */
15
- export type BlocBaseAbstract =
16
- | typeof Bloc<any, any, any>
17
- | typeof Cubit<any, any>;
15
+ export type BlocBaseAbstract = typeof Bloc<any, any> | typeof Cubit<any>;
18
16
 
19
17
  /**
20
18
  * Represents a constructor type for a Bloc that can take any parameters
@@ -25,6 +23,9 @@ export type BlocConstructor<B> = (new (...args: any) => B) & {
25
23
  keepAlive?: boolean;
26
24
  };
27
25
 
26
+ export type BlocConstructorParams<B extends BlocConstructor<BlocBase<any>>> =
27
+ ConstructorParameters<B>[0];
28
+
28
29
  /**
29
30
  * Extracts the state type from a BlocBase instance
30
31
  * @template B - The BlocBase type to extract the state from
@@ -38,24 +39,6 @@ export type ValueType<B extends BlocBase<any>> =
38
39
  */
39
40
  export type BlocState<T> = T extends BlocBase<infer S> ? S : never;
40
41
 
41
- /**
42
- * Extracts the props type from either a Bloc or Cubit
43
- * @template T - The Bloc or Cubit type to extract the props from
44
- */
45
- export type InferPropsFromGeneric<T> =
46
- T extends Bloc<any, any, infer P>
47
- ? P
48
- : T extends Cubit<any, infer P>
49
- ? P
50
- : never;
51
-
52
- /**
53
- * Extracts the constructor parameters type from a BlocBase
54
- * @template B - The BlocBase type to extract the constructor parameters from
55
- */
56
- export type BlocConstructorParameters<B extends BlocBase<any>> =
57
- BlocConstructor<B> extends new (...args: infer P) => B ? P : never;
58
-
59
42
  /**
60
43
  * Enhanced constraint for Bloc events - must be objects with proper constructor
61
44
  */
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Manages batching of state updates to optimize performance
3
+ */
4
+ export class BatchingManager<S> {
5
+ private pendingUpdates: Array<{
6
+ newState: S;
7
+ oldState: S;
8
+ action?: unknown;
9
+ }> = [];
10
+ private isBatching = false;
11
+
12
+ /**
13
+ * Execute a callback with batched updates
14
+ */
15
+ batchUpdates(
16
+ callback: () => void,
17
+ onFlush: (finalUpdate: {
18
+ newState: S;
19
+ oldState: S;
20
+ action?: unknown;
21
+ }) => void,
22
+ ): void {
23
+ if (this.isBatching) {
24
+ callback();
25
+ return;
26
+ }
27
+
28
+ this.isBatching = true;
29
+ this.pendingUpdates = [];
30
+
31
+ try {
32
+ callback();
33
+
34
+ if (this.pendingUpdates.length > 0) {
35
+ const finalUpdate = this.pendingUpdates[this.pendingUpdates.length - 1];
36
+ onFlush(finalUpdate);
37
+ }
38
+ } finally {
39
+ this.isBatching = false;
40
+ this.pendingUpdates = [];
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Add an update to the batch
46
+ */
47
+ addUpdate(update: { newState: S; oldState: S; action?: unknown }): void {
48
+ if (this.isBatching) {
49
+ this.pendingUpdates.push(update);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Check if currently batching
55
+ */
56
+ get isCurrentlyBatching(): boolean {
57
+ return this.isBatching;
58
+ }
59
+ }