@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
@@ -69,11 +69,11 @@ describe('Cubit State Emissions', () => {
69
69
  const cubit = new CounterCubit();
70
70
  const observer = vi.fn();
71
71
 
72
- cubit._observer.subscribe({ id: 'test', fn: observer });
72
+ cubit.subscribe(observer);
73
73
 
74
74
  cubit.increment();
75
75
 
76
- expect(observer).toHaveBeenCalledWith(1, 0, undefined);
76
+ expect(observer).toHaveBeenCalledWith(1);
77
77
  expect(cubit.state).toBe(1);
78
78
  });
79
79
 
@@ -81,7 +81,7 @@ describe('Cubit State Emissions', () => {
81
81
  const cubit = new CounterCubit(5);
82
82
  const observer = vi.fn();
83
83
 
84
- cubit._observer.subscribe({ id: 'test', fn: observer });
84
+ cubit.subscribe(observer);
85
85
 
86
86
  // Try to emit same value
87
87
  cubit.set(5);
@@ -94,7 +94,7 @@ describe('Cubit State Emissions', () => {
94
94
  const cubit = new CounterCubit(NaN);
95
95
  const observer = vi.fn();
96
96
 
97
- cubit._observer.subscribe({ id: 'test', fn: observer });
97
+ cubit.subscribe(observer);
98
98
 
99
99
  // NaN === NaN is false, but Object.is(NaN, NaN) is true
100
100
  cubit.set(NaN);
@@ -106,28 +106,28 @@ describe('Cubit State Emissions', () => {
106
106
  const cubit = new CounterCubit(0);
107
107
  const observer = vi.fn();
108
108
 
109
- cubit._observer.subscribe({ id: 'test', fn: observer });
109
+ cubit.subscribe(observer);
110
110
 
111
111
  // Object.is can distinguish +0 and -0
112
112
  cubit.set(-0);
113
113
 
114
- expect(observer).toHaveBeenCalledWith(-0, 0, undefined);
114
+ expect(observer).toHaveBeenCalledWith(-0);
115
115
  });
116
116
 
117
117
  it('should emit multiple state changes sequentially', () => {
118
118
  const cubit = new CounterCubit();
119
119
  const observer = vi.fn();
120
120
 
121
- cubit._observer.subscribe({ id: 'test', fn: observer });
121
+ cubit.subscribe(observer);
122
122
 
123
123
  cubit.increment();
124
124
  cubit.increment();
125
125
  cubit.decrement();
126
126
 
127
127
  expect(observer).toHaveBeenCalledTimes(3);
128
- expect(observer).toHaveBeenNthCalledWith(1, 1, 0, undefined);
129
- expect(observer).toHaveBeenNthCalledWith(2, 2, 1, undefined);
130
- expect(observer).toHaveBeenNthCalledWith(3, 1, 2, undefined);
128
+ expect(observer).toHaveBeenNthCalledWith(1, 1);
129
+ expect(observer).toHaveBeenNthCalledWith(2, 2);
130
+ expect(observer).toHaveBeenNthCalledWith(3, 1);
131
131
  expect(cubit.state).toBe(1);
132
132
  });
133
133
  });
@@ -137,23 +137,15 @@ describe('Cubit State Emissions', () => {
137
137
  const cubit = new UserCubit();
138
138
  const observer = vi.fn();
139
139
 
140
- cubit._observer.subscribe({ id: 'test', fn: observer });
140
+ cubit.subscribe(observer);
141
141
 
142
142
  cubit.updateName('Jane Doe');
143
143
 
144
- expect(observer).toHaveBeenCalledWith(
145
- {
146
- name: 'Jane Doe',
147
- age: 30,
148
- email: 'john@example.com',
149
- },
150
- {
151
- name: 'John Doe',
152
- age: 30,
153
- email: 'john@example.com',
154
- },
155
- undefined,
156
- );
144
+ expect(observer).toHaveBeenCalledWith({
145
+ name: 'Jane Doe',
146
+ age: 30,
147
+ email: 'john@example.com',
148
+ });
157
149
  expect(cubit.state.name).toBe('Jane Doe');
158
150
  expect(cubit.state.age).toBe(30); // Unchanged
159
151
  });
@@ -162,7 +154,7 @@ describe('Cubit State Emissions', () => {
162
154
  const cubit = new UserCubit();
163
155
  const observer = vi.fn();
164
156
 
165
- cubit._observer.subscribe({ id: 'test', fn: observer });
157
+ cubit.subscribe(observer);
166
158
 
167
159
  cubit.updateMultiple({ name: 'Jane Smith', age: 25 });
168
160
 
@@ -178,7 +170,7 @@ describe('Cubit State Emissions', () => {
178
170
  const cubit = new UserCubit();
179
171
  const observer = vi.fn();
180
172
 
181
- cubit._observer.subscribe({ id: 'test', fn: observer });
173
+ cubit.subscribe(observer);
182
174
 
183
175
  // Patch with same values
184
176
  cubit.patch({ name: 'John Doe', age: 30 });
@@ -190,7 +182,7 @@ describe('Cubit State Emissions', () => {
190
182
  const cubit = new UserCubit();
191
183
  const observer = vi.fn();
192
184
 
193
- cubit._observer.subscribe({ id: 'test', fn: observer });
185
+ cubit.subscribe(observer);
194
186
 
195
187
  // One value different
196
188
  cubit.patch({ name: 'John Doe', age: 31 });
@@ -203,7 +195,7 @@ describe('Cubit State Emissions', () => {
203
195
  const cubit = new UserCubit();
204
196
  const observer = vi.fn();
205
197
 
206
- cubit._observer.subscribe({ id: 'test', fn: observer });
198
+ cubit.subscribe(observer);
207
199
 
208
200
  cubit.setPreferences('dark', true);
209
201
 
@@ -218,7 +210,7 @@ describe('Cubit State Emissions', () => {
218
210
  const cubit = new UserCubit();
219
211
  const observer = vi.fn();
220
212
 
221
- cubit._observer.subscribe({ id: 'test', fn: observer });
213
+ cubit.subscribe(observer);
222
214
 
223
215
  // Force emit even with same values
224
216
  cubit.patch({ name: 'John Doe' }, true);
@@ -278,7 +270,7 @@ describe('Cubit State Emissions', () => {
278
270
  const cubit = new ArrayCubit();
279
271
  const observer = vi.fn();
280
272
 
281
- cubit._observer.subscribe({ id: 'test', fn: observer });
273
+ cubit.subscribe(observer);
282
274
 
283
275
  // New array reference should trigger update
284
276
  cubit.add(4);
@@ -306,13 +298,13 @@ describe('Cubit State Emissions', () => {
306
298
  const cubit = new NullableCubit();
307
299
  const observer = vi.fn();
308
300
 
309
- cubit._observer.subscribe({ id: 'test', fn: observer });
301
+ cubit.subscribe(observer);
310
302
 
311
303
  // null and undefined are different according to Object.is
312
304
  expect(Object.is(null, undefined)).toBe(false);
313
305
 
314
306
  cubit.setNull();
315
- expect(observer).toHaveBeenCalledWith(null, 'initial', undefined);
307
+ expect(observer).toHaveBeenCalledWith(null);
316
308
  expect(cubit.state).toBe(null);
317
309
 
318
310
  observer.mockClear();
@@ -328,7 +320,7 @@ describe('Cubit State Emissions', () => {
328
320
 
329
321
  observer.mockClear();
330
322
  cubit.setString('value');
331
- expect(observer).toHaveBeenCalledWith('value', null, undefined);
323
+ expect(observer).toHaveBeenCalledWith('value');
332
324
  expect(cubit.state).toBe('value');
333
325
  });
334
326
 
@@ -354,7 +346,7 @@ describe('Cubit State Emissions', () => {
354
346
  const cubit = new ComplexCubit();
355
347
  const observer = vi.fn();
356
348
 
357
- cubit._observer.subscribe({ id: 'test', fn: observer });
349
+ cubit.subscribe(observer);
358
350
 
359
351
  // Different object reference with same values
360
352
  cubit.updateData({ id: 1, values: [1, 2, 3] });
@@ -369,19 +361,18 @@ describe('Cubit State Emissions', () => {
369
361
  const cubit = new CounterCubit();
370
362
 
371
363
  // Should have BlocBase properties and methods
372
- expect(cubit._state).toBe(0);
373
- expect(cubit._observer).toBeDefined();
374
- expect(cubit._consumers).toBeDefined();
375
- expect(typeof cubit._addConsumer).toBe('function');
364
+ expect(cubit.state).toBe(0);
365
+ expect(cubit._subscriptionManager).toBeDefined();
366
+ expect(typeof cubit.subscribe).toBe('function');
376
367
  });
377
368
 
378
369
  it('should work with state batching', () => {
379
370
  const cubit = new CounterCubit();
380
371
  const observer = vi.fn();
381
372
 
382
- cubit._observer.subscribe({ id: 'test', fn: observer });
373
+ cubit.subscribe(observer);
383
374
 
384
- cubit.batch(() => {
375
+ cubit._batchUpdates(() => {
385
376
  cubit.increment(); // 0 -> 1
386
377
  cubit.increment(); // 1 -> 2
387
378
  cubit.increment(); // 2 -> 3
@@ -390,7 +381,7 @@ describe('Cubit State Emissions', () => {
390
381
  // Should only notify once with final state
391
382
  // The oldState in the batch notification is from the last update (2 -> 3)
392
383
  expect(observer).toHaveBeenCalledTimes(1);
393
- expect(observer).toHaveBeenCalledWith(3, 2, undefined);
384
+ expect(observer).toHaveBeenCalledWith(3);
394
385
  expect(cubit.state).toBe(3);
395
386
  });
396
387
 
@@ -398,12 +389,11 @@ describe('Cubit State Emissions', () => {
398
389
  const cubit = new CounterCubit();
399
390
 
400
391
  cubit.increment();
401
- expect(cubit._oldState).toBe(0);
402
- expect(cubit._state).toBe(1);
392
+ // Old state tracking was removed in new model
393
+ expect(cubit.state).toBe(1);
403
394
 
404
395
  cubit.increment();
405
- expect(cubit._oldState).toBe(1);
406
- expect(cubit._state).toBe(2);
396
+ expect(cubit.state).toBe(2);
407
397
  });
408
398
  });
409
399
 
@@ -412,7 +402,7 @@ describe('Cubit State Emissions', () => {
412
402
  const cubit = new CounterCubit();
413
403
  const observer = vi.fn();
414
404
 
415
- cubit._observer.subscribe({ id: 'test', fn: observer });
405
+ cubit.subscribe(observer);
416
406
 
417
407
  // Rapid emissions
418
408
  for (let i = 0; i < 1000; i++) {
@@ -422,16 +412,15 @@ describe('Cubit State Emissions', () => {
422
412
  expect(cubit.state).toBe(1000);
423
413
  expect(observer).toHaveBeenCalledTimes(1000);
424
414
 
425
- // Check no accumulation of state
426
- expect(cubit._state).toBe(1000);
427
- expect(cubit._oldState).toBe(999);
415
+ // Check final state
416
+ expect(cubit.state).toBe(1000);
428
417
  });
429
418
 
430
419
  it('should handle concurrent patch operations', () => {
431
420
  const cubit = new UserCubit();
432
421
  const observer = vi.fn();
433
422
 
434
- cubit._observer.subscribe({ id: 'test', fn: observer });
423
+ cubit.subscribe(observer);
435
424
 
436
425
  // Multiple patches in quick succession
437
426
  cubit.patch({ name: 'Name1' });
@@ -0,0 +1,383 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { Blac } from '../Blac';
3
+ import { Cubit } from '../Cubit';
4
+ import { Bloc } from '../Bloc';
5
+ import { BlacAdapter } from '../adapter/BlacAdapter';
6
+
7
+ // Helper to force garbage collection if available
8
+ const forceGC = () => {
9
+ if ((global as any).gc) {
10
+ (global as any).gc();
11
+ }
12
+ };
13
+
14
+ // Helper to wait for WeakRef cleanup
15
+ const waitForCleanup = async (checkFn: () => boolean, maxWait = 1000) => {
16
+ const start = Date.now();
17
+ while (Date.now() - start < maxWait) {
18
+ forceGC();
19
+ await new Promise((resolve) => setTimeout(resolve, 10));
20
+ if (checkFn()) return true;
21
+ }
22
+ return false;
23
+ };
24
+
25
+ // Test state class
26
+ class TestState {
27
+ constructor(public value: number) {}
28
+ }
29
+
30
+ // Test Cubit
31
+ class TestCubit extends Cubit<TestState> {
32
+ constructor() {
33
+ super(new TestState(0));
34
+ }
35
+
36
+ increment = () => {
37
+ this.emit(new TestState(this.state.value + 1));
38
+ };
39
+ }
40
+
41
+ // Test event
42
+ class IncrementEvent {
43
+ constructor(public amount: number) {}
44
+ }
45
+
46
+ // Test Bloc
47
+ class TestBloc extends Bloc<TestState, IncrementEvent> {
48
+ constructor() {
49
+ super(new TestState(0));
50
+ this.on(IncrementEvent, this.handleIncrement);
51
+ }
52
+
53
+ handleIncrement = (
54
+ event: IncrementEvent,
55
+ emit: (state: TestState) => void,
56
+ ) => {
57
+ emit(new TestState(this.state.value + event.amount));
58
+ };
59
+ }
60
+
61
+ describe('Memory Leak Tests', () => {
62
+ let blac: Blac;
63
+
64
+ beforeEach(() => {
65
+ blac = Blac.getInstance();
66
+ Blac.enableLog = false; // Disable logging for tests
67
+ vi.useFakeTimers();
68
+ });
69
+
70
+ afterEach(() => {
71
+ vi.useRealTimers();
72
+ Blac.resetInstance();
73
+ });
74
+
75
+ describe('WeakRef Cleanup', () => {
76
+ it('should support component WeakRefs for automatic cleanup', async () => {
77
+ const cubit = blac.getBloc(TestCubit);
78
+ let component: any = { id: 'test-component' };
79
+ const weakRef = new WeakRef(component);
80
+
81
+ // Subscribe with component reference
82
+ const unsubscribe = cubit.subscribeComponent(weakRef, () => {});
83
+ expect(cubit.subscriptionCount).toBe(1);
84
+
85
+ // Verify the subscription works
86
+ const callback = vi.fn();
87
+ cubit.subscribeComponent(weakRef, callback);
88
+ cubit.increment();
89
+ expect(callback).toHaveBeenCalled();
90
+
91
+ // Clean up
92
+ unsubscribe();
93
+ });
94
+
95
+ it('should handle multiple subscriptions with weak references', async () => {
96
+ const cubit = blac.getBloc(TestCubit);
97
+ const component1 = { id: 'component-1' };
98
+ const component2 = { id: 'component-2' };
99
+ const component3 = { id: 'component-3' };
100
+
101
+ // Add subscriptions with weak refs
102
+ const unsub1 = cubit.subscribeComponent(
103
+ new WeakRef(component1),
104
+ () => {},
105
+ );
106
+ const unsub2 = cubit.subscribeComponent(
107
+ new WeakRef(component2),
108
+ () => {},
109
+ );
110
+ const unsub3 = cubit.subscribeComponent(
111
+ new WeakRef(component3),
112
+ () => {},
113
+ );
114
+
115
+ expect(cubit.subscriptionCount).toBe(3);
116
+
117
+ // Remove some subscriptions
118
+ unsub1();
119
+ unsub2();
120
+
121
+ expect(cubit.subscriptionCount).toBe(1);
122
+
123
+ // Clean up remaining
124
+ unsub3();
125
+ expect(cubit.subscriptionCount).toBe(0);
126
+ });
127
+ });
128
+
129
+ describe('Bloc Instance Disposal', () => {
130
+ it('should dispose non-keepAlive blocs when no consumers remain', async () => {
131
+ const cubit = blac.getBloc(TestCubit);
132
+ const adapter = new BlacAdapter(
133
+ { blocConstructor: TestCubit, componentRef: { current: {} } },
134
+ {},
135
+ );
136
+
137
+ // Subscribe and unsubscribe
138
+ adapter.mount();
139
+ const unsubscribe = adapter.createSubscription({ onChange: () => {} });
140
+ expect(cubit.subscriptionCount).toBe(1);
141
+
142
+ unsubscribe();
143
+ adapter.unmount();
144
+
145
+ // Wait for disposal
146
+ await vi.runAllTimersAsync();
147
+
148
+ // Bloc should be disposed
149
+ expect(cubit.isDisposed).toBe(true);
150
+ });
151
+
152
+ it('should not dispose keepAlive blocs when no consumers remain', async () => {
153
+ class KeepAliveCubit extends TestCubit {
154
+ static keepAlive = true;
155
+ }
156
+
157
+ const cubit = blac.getBloc(KeepAliveCubit);
158
+ const adapter = new BlacAdapter(
159
+ { blocConstructor: KeepAliveCubit, componentRef: { current: {} } },
160
+ {},
161
+ );
162
+
163
+ // Subscribe and unsubscribe
164
+ adapter.mount();
165
+ const unsubscribe = adapter.createSubscription({ onChange: () => {} });
166
+ expect(cubit.subscriptionCount).toBe(1);
167
+
168
+ unsubscribe();
169
+
170
+ // Wait for potential disposal
171
+ await vi.runAllTimersAsync();
172
+
173
+ // Bloc should still be active
174
+ expect(cubit.isDisposed).toBe(false);
175
+ });
176
+ });
177
+
178
+ describe('Memory Pressure Scenarios', () => {
179
+ it('should handle rapid subscription addition/removal without leaks', async () => {
180
+ const cubit = blac.getBloc(TestCubit);
181
+ const unsubscribes: (() => void)[] = [];
182
+
183
+ // Add many subscriptions
184
+ for (let i = 0; i < 1000; i++) {
185
+ const unsub = cubit.subscribe(() => {});
186
+ unsubscribes.push(unsub);
187
+ }
188
+
189
+ expect(cubit.subscriptionCount).toBe(1000);
190
+
191
+ // Remove half the subscriptions
192
+ for (let i = 0; i < 500; i++) {
193
+ unsubscribes[i]();
194
+ }
195
+
196
+ expect(cubit.subscriptionCount).toBe(500);
197
+
198
+ // Remove remaining subscriptions
199
+ for (let i = 500; i < 1000; i++) {
200
+ unsubscribes[i]();
201
+ }
202
+
203
+ expect(cubit.subscriptionCount).toBe(0);
204
+ });
205
+
206
+ it('should handle concurrent bloc creation and disposal', async () => {
207
+ const adapters: BlacAdapter<typeof TestBloc>[] = [];
208
+ const unsubscribes: (() => void)[] = [];
209
+
210
+ // Create many blocs through adapters
211
+ for (let i = 0; i < 100; i++) {
212
+ const adapter = new BlacAdapter(
213
+ { blocConstructor: TestBloc, componentRef: { current: {} } },
214
+ { instanceId: `test-bloc-${i}` },
215
+ );
216
+ adapters.push(adapter);
217
+ adapter.mount();
218
+ const unsub = adapter.createSubscription({ onChange: () => {} });
219
+ unsubscribes.push(unsub);
220
+ }
221
+
222
+ // Unsubscribe all and unmount
223
+ unsubscribes.forEach((unsub) => unsub());
224
+ adapters.forEach((adapter) => adapter.unmount());
225
+
226
+ // Wait for disposal
227
+ await vi.runAllTimersAsync();
228
+
229
+ // All blocs should be disposed
230
+ for (const adapter of adapters) {
231
+ expect(adapter.blocInstance.isDisposed).toBe(true);
232
+ }
233
+ });
234
+ });
235
+
236
+ describe('Proxy and Cache Cleanup', () => {
237
+ it('should not leak memory through proxy caches', async () => {
238
+ const { ProxyFactory } = await import('../adapter/ProxyFactory');
239
+ const { createStateProxy, getStats, resetStats } = ProxyFactory;
240
+ const stats = getStats();
241
+ const initialProxyCount = stats.stateProxiesCreated || 0;
242
+
243
+ // Create many proxied states
244
+ for (let i = 0; i < 100; i++) {
245
+ const cubit = new TestCubit();
246
+ const proxy = createStateProxy({
247
+ target: cubit.state,
248
+ consumerRef: { current: {} },
249
+ consumerTracker: {
250
+ trackAccess: () => {},
251
+ } as any,
252
+ });
253
+ // Access properties to trigger proxy creation
254
+ const _ = proxy.value;
255
+ }
256
+
257
+ // Proxies should have been created
258
+ const newStats = getStats();
259
+ expect(newStats.stateProxiesCreated || 0).toBeGreaterThan(
260
+ initialProxyCount,
261
+ );
262
+
263
+ // Clear stats
264
+ resetStats();
265
+ const clearedStats = getStats();
266
+ expect(clearedStats.stateProxiesCreated || 0).toBe(0);
267
+ });
268
+ });
269
+
270
+ describe('Event Queue Memory Management', () => {
271
+ it('should not accumulate events in queue indefinitely', async () => {
272
+ const bloc = blac.getBloc(TestBloc);
273
+
274
+ // Add many events rapidly
275
+ for (let i = 0; i < 1000; i++) {
276
+ bloc.add(new IncrementEvent(1));
277
+ }
278
+
279
+ // Wait for processing
280
+ await vi.runAllTimersAsync();
281
+
282
+ // Queue should be empty after processing
283
+ expect((bloc as any)._eventQueue.length).toBe(0);
284
+ });
285
+
286
+ it('should handle disposal during event processing', async () => {
287
+ const bloc = blac.getBloc(TestBloc);
288
+ const adapter = new BlacAdapter(
289
+ { blocConstructor: TestBloc, componentRef: { current: {} } },
290
+ {},
291
+ );
292
+
293
+ // Subscribe
294
+ adapter.mount();
295
+ const unsubscribe = adapter.createSubscription({ onChange: () => {} });
296
+
297
+ // Add events and immediately dispose
298
+ for (let i = 0; i < 10; i++) {
299
+ bloc.add(new IncrementEvent(1));
300
+ }
301
+
302
+ unsubscribe();
303
+ adapter.unmount();
304
+
305
+ // Wait for disposal
306
+ await vi.runAllTimersAsync();
307
+
308
+ // Bloc should be disposed and queue cleared
309
+ expect(bloc.isDisposed).toBe(true);
310
+ expect((bloc as any)._eventQueue.length).toBe(0);
311
+ });
312
+ });
313
+
314
+ describe('Disposal State Transitions', () => {
315
+ it('should handle concurrent disposal requests safely', async () => {
316
+ const cubit = blac.getBloc(TestCubit);
317
+ const adapter = new BlacAdapter(
318
+ { blocConstructor: TestCubit, componentRef: { current: {} } },
319
+ {},
320
+ );
321
+
322
+ // Add multiple consumers
323
+ adapter.mount();
324
+ const unsub1 = adapter.createSubscription({ onChange: () => {} });
325
+ const adapter2 = new BlacAdapter(
326
+ { blocConstructor: TestCubit, componentRef: { current: {} } },
327
+ {},
328
+ );
329
+ adapter2.mount();
330
+ const unsub2 = adapter2.createSubscription({ onChange: () => {} });
331
+
332
+ // Remove consumers concurrently
333
+ await Promise.all([
334
+ (async () => {
335
+ unsub1();
336
+ adapter.unmount();
337
+ })(),
338
+ (async () => {
339
+ unsub2();
340
+ adapter2.unmount();
341
+ })(),
342
+ ]);
343
+
344
+ // Wait for disposal
345
+ await vi.runAllTimersAsync();
346
+
347
+ // Should be disposed exactly once
348
+ expect(cubit.isDisposed).toBe(true);
349
+ });
350
+
351
+ it('should prevent state updates after disposal', async () => {
352
+ const cubit = blac.getBloc(TestCubit);
353
+ const adapter = new BlacAdapter(
354
+ { blocConstructor: TestCubit, componentRef: { current: {} } },
355
+ {},
356
+ );
357
+
358
+ // Subscribe and start disposal
359
+ adapter.mount();
360
+ const unsub = adapter.createSubscription({ onChange: () => {} });
361
+ unsub();
362
+ adapter.unmount();
363
+
364
+ // Wait for disposal to complete
365
+ await vi.runAllTimersAsync();
366
+
367
+ expect(cubit.isDisposed).toBe(true);
368
+
369
+ // Try to update state after disposal
370
+ const callback = vi.fn();
371
+ const unsubscribe = cubit.subscribe(callback);
372
+
373
+ // State should not change
374
+ const initialState = cubit.state;
375
+ cubit.increment();
376
+
377
+ // State should remain unchanged
378
+ expect(cubit.state).toBe(initialState);
379
+ expect(callback).not.toHaveBeenCalled();
380
+ unsubscribe();
381
+ });
382
+ });
383
+ });