@blac/core 2.0.0-rc-13 → 2.0.0-rc-14
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.cjs.js +2 -2
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +522 -297
- package/dist/index.es.js +1833 -1550
- package/dist/index.es.js.map +1 -1
- package/package.json +6 -7
- package/src/Blac.ts +38 -71
- package/src/Bloc.ts +8 -9
- package/src/BlocBase.ts +246 -540
- package/src/Cubit.ts +1 -16
- package/src/__tests__/Blac.config.test.ts +10 -17
- package/src/__tests__/Bloc.event.test.ts +27 -52
- package/src/__tests__/BlocBase.subscription.test.ts +349 -0
- package/src/__tests__/Cubit.test.ts +39 -50
- package/src/__tests__/memory-leaks.test.ts +383 -0
- package/src/__tests__/plugins.test.ts +107 -97
- package/src/adapter/BlacAdapter.ts +347 -207
- package/src/adapter/ProxyFactory.ts +196 -255
- package/src/adapter/__tests__/BlacAdapter.test.ts +71 -77
- package/src/adapter/__tests__/ProxyFactory.test.ts +1 -1
- package/src/adapter/index.ts +0 -1
- package/src/errors/BlacError.ts +69 -0
- package/src/errors/ErrorManager.ts +101 -0
- package/src/errors/handleError.ts +114 -0
- package/src/errors/index.ts +13 -0
- package/src/events.ts +7 -1
- package/src/index.ts +7 -1
- package/src/lifecycle/BlocLifecycle.ts +156 -0
- package/src/lifecycle/index.ts +1 -0
- package/src/plugins/BlocPluginRegistry.ts +71 -51
- package/src/plugins/SystemPluginRegistry.ts +63 -13
- package/src/plugins/__tests__/BlocPluginRegistry.test.ts +450 -0
- package/src/plugins/__tests__/SystemPluginRegistry.test.ts +404 -0
- package/src/plugins/types.ts +32 -3
- package/src/subscription/README.md +132 -0
- package/src/subscription/SubscriptionManager.ts +395 -0
- package/src/subscription/__tests__/SubscriptionManager.test.ts +355 -0
- package/src/subscription/index.ts +2 -0
- package/src/subscription/types.ts +90 -0
- package/src/testing.ts +31 -37
- package/src/types.ts +4 -21
- package/src/utils/BatchingManager.ts +59 -0
- package/src/utils/RerenderLogger.ts +265 -0
- package/src/utils/__tests__/generateInstanceId.test.ts +104 -0
- package/src/utils/__tests__/shallowEqual.test.ts +171 -0
- package/src/utils/__tests__/uuid.test.ts +144 -0
- package/src/utils/generateInstanceId.ts +54 -0
- package/src/utils/shallowEqual.ts +1 -1
- package/README.md +0 -329
- package/src/BlacObserver.ts +0 -189
- package/src/__tests__/BlocBase.lifecycle.test.ts +0 -441
- package/src/__tests__/props.test.ts +0 -256
- package/src/adapter/ConsumerTracker.ts +0 -241
- 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.
|
|
72
|
+
cubit.subscribe(observer);
|
|
73
73
|
|
|
74
74
|
cubit.increment();
|
|
75
75
|
|
|
76
|
-
expect(observer).toHaveBeenCalledWith(1
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
129
|
-
expect(observer).toHaveBeenNthCalledWith(2, 2
|
|
130
|
-
expect(observer).toHaveBeenNthCalledWith(3, 1
|
|
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.
|
|
140
|
+
cubit.subscribe(observer);
|
|
141
141
|
|
|
142
142
|
cubit.updateName('Jane Doe');
|
|
143
143
|
|
|
144
|
-
expect(observer).toHaveBeenCalledWith(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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'
|
|
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.
|
|
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.
|
|
373
|
-
expect(cubit.
|
|
374
|
-
expect(cubit.
|
|
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.
|
|
373
|
+
cubit.subscribe(observer);
|
|
383
374
|
|
|
384
|
-
cubit.
|
|
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
|
|
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
|
-
|
|
402
|
-
expect(cubit.
|
|
392
|
+
// Old state tracking was removed in new model
|
|
393
|
+
expect(cubit.state).toBe(1);
|
|
403
394
|
|
|
404
395
|
cubit.increment();
|
|
405
|
-
expect(cubit.
|
|
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.
|
|
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
|
|
426
|
-
expect(cubit.
|
|
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.
|
|
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
|
+
});
|