@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.
- package/package.json +10 -15
- 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/dist/index.cjs.js +0 -11
- package/dist/index.cjs.js.map +0 -1
- package/dist/index.d.ts +0 -1078
- package/dist/index.es.js +0 -1992
- package/dist/index.es.js.map +0 -1
- 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
|
@@ -1,441 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { BlocBase, BlocLifecycleState } from '../BlocBase';
|
|
3
|
-
import { Blac } from '../Blac';
|
|
4
|
-
|
|
5
|
-
// Test implementation of BlocBase
|
|
6
|
-
class TestBloc extends BlocBase<number> {
|
|
7
|
-
constructor(initialState: number = 0) {
|
|
8
|
-
super(initialState);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
increment() {
|
|
12
|
-
this._pushState(this.state + 1, this.state);
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// Test bloc with static properties
|
|
17
|
-
class KeepAliveBloc extends BlocBase<string> {
|
|
18
|
-
static keepAlive = true;
|
|
19
|
-
|
|
20
|
-
constructor() {
|
|
21
|
-
super('initial');
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
class IsolatedBloc extends BlocBase<string> {
|
|
26
|
-
static isolated = true;
|
|
27
|
-
|
|
28
|
-
constructor() {
|
|
29
|
-
super('isolated');
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
describe('BlocBase Lifecycle Management', () => {
|
|
34
|
-
let blac: Blac;
|
|
35
|
-
|
|
36
|
-
beforeEach(() => {
|
|
37
|
-
blac = new Blac({ __unsafe_ignore_singleton: true });
|
|
38
|
-
vi.useFakeTimers();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
afterEach(() => {
|
|
42
|
-
vi.useRealTimers();
|
|
43
|
-
vi.clearAllMocks();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
describe('Atomic State Transitions', () => {
|
|
47
|
-
it('should transition through lifecycle states atomically', () => {
|
|
48
|
-
const bloc = new TestBloc();
|
|
49
|
-
|
|
50
|
-
// Initial state should be ACTIVE
|
|
51
|
-
expect((bloc as any)._disposalState).toBe(BlocLifecycleState.ACTIVE);
|
|
52
|
-
|
|
53
|
-
// Transition to DISPOSAL_REQUESTED
|
|
54
|
-
const result1 = (bloc as any)._atomicStateTransition(
|
|
55
|
-
BlocLifecycleState.ACTIVE,
|
|
56
|
-
BlocLifecycleState.DISPOSAL_REQUESTED,
|
|
57
|
-
);
|
|
58
|
-
expect(result1.success).toBe(true);
|
|
59
|
-
expect((bloc as any)._disposalState).toBe(
|
|
60
|
-
BlocLifecycleState.DISPOSAL_REQUESTED,
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
// Transition to DISPOSING
|
|
64
|
-
const result2 = (bloc as any)._atomicStateTransition(
|
|
65
|
-
BlocLifecycleState.DISPOSAL_REQUESTED,
|
|
66
|
-
BlocLifecycleState.DISPOSING,
|
|
67
|
-
);
|
|
68
|
-
expect(result2.success).toBe(true);
|
|
69
|
-
expect((bloc as any)._disposalState).toBe(BlocLifecycleState.DISPOSING);
|
|
70
|
-
|
|
71
|
-
// Transition to DISPOSED
|
|
72
|
-
const result3 = (bloc as any)._atomicStateTransition(
|
|
73
|
-
BlocLifecycleState.DISPOSING,
|
|
74
|
-
BlocLifecycleState.DISPOSED,
|
|
75
|
-
);
|
|
76
|
-
expect(result3.success).toBe(true);
|
|
77
|
-
expect((bloc as any)._disposalState).toBe(BlocLifecycleState.DISPOSED);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('should reject invalid state transitions', () => {
|
|
81
|
-
const bloc = new TestBloc();
|
|
82
|
-
|
|
83
|
-
// Try to transition from ACTIVE to DISPOSED directly (should work in _dispose but not here)
|
|
84
|
-
const result = (bloc as any)._atomicStateTransition(
|
|
85
|
-
BlocLifecycleState.DISPOSAL_REQUESTED,
|
|
86
|
-
BlocLifecycleState.DISPOSED,
|
|
87
|
-
);
|
|
88
|
-
expect(result.success).toBe(false);
|
|
89
|
-
expect(result.currentState).toBe(BlocLifecycleState.ACTIVE); // Still in ACTIVE because expectedState didn't match
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('should handle concurrent transition attempts', () => {
|
|
93
|
-
const bloc = new TestBloc();
|
|
94
|
-
|
|
95
|
-
// First transition succeeds
|
|
96
|
-
const result1 = (bloc as any)._atomicStateTransition(
|
|
97
|
-
BlocLifecycleState.ACTIVE,
|
|
98
|
-
BlocLifecycleState.DISPOSAL_REQUESTED,
|
|
99
|
-
);
|
|
100
|
-
expect(result1.success).toBe(true);
|
|
101
|
-
|
|
102
|
-
// Second attempt with same expected state fails
|
|
103
|
-
const result2 = (bloc as any)._atomicStateTransition(
|
|
104
|
-
BlocLifecycleState.ACTIVE,
|
|
105
|
-
BlocLifecycleState.DISPOSAL_REQUESTED,
|
|
106
|
-
);
|
|
107
|
-
expect(result2.success).toBe(false);
|
|
108
|
-
expect(result2.currentState).toBe(BlocLifecycleState.DISPOSAL_REQUESTED);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe('Consumer Management', () => {
|
|
113
|
-
it('should register and unregister consumers', () => {
|
|
114
|
-
const bloc = new TestBloc();
|
|
115
|
-
const consumerId = 'test-consumer-1';
|
|
116
|
-
const consumerRef = {};
|
|
117
|
-
|
|
118
|
-
// Add consumer
|
|
119
|
-
const added = bloc._addConsumer(consumerId, consumerRef);
|
|
120
|
-
expect(added).toBe(true);
|
|
121
|
-
expect(bloc._consumers.has(consumerId)).toBe(true);
|
|
122
|
-
expect(bloc._consumers.size).toBe(1);
|
|
123
|
-
|
|
124
|
-
// Remove consumer
|
|
125
|
-
bloc._removeConsumer(consumerId);
|
|
126
|
-
expect(bloc._consumers.has(consumerId)).toBe(false);
|
|
127
|
-
expect(bloc._consumers.size).toBe(0);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('should prevent duplicate consumer registration', () => {
|
|
131
|
-
const bloc = new TestBloc();
|
|
132
|
-
const consumerId = 'test-consumer-1';
|
|
133
|
-
|
|
134
|
-
bloc._addConsumer(consumerId);
|
|
135
|
-
expect(bloc._consumers.size).toBe(1);
|
|
136
|
-
|
|
137
|
-
// Try to add same consumer again
|
|
138
|
-
bloc._addConsumer(consumerId);
|
|
139
|
-
expect(bloc._consumers.size).toBe(1); // Should still be 1
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('should schedule disposal when last consumer is removed', async () => {
|
|
143
|
-
const bloc = new TestBloc();
|
|
144
|
-
const consumerId = 'test-consumer-1';
|
|
145
|
-
|
|
146
|
-
// Register bloc with Blac instance
|
|
147
|
-
blac.registerBlocInstance(bloc as BlocBase<unknown>);
|
|
148
|
-
|
|
149
|
-
// Add and remove consumer
|
|
150
|
-
bloc._addConsumer(consumerId);
|
|
151
|
-
bloc._removeConsumer(consumerId);
|
|
152
|
-
|
|
153
|
-
// Should transition to DISPOSAL_REQUESTED immediately
|
|
154
|
-
expect((bloc as any)._disposalState).toBe(
|
|
155
|
-
BlocLifecycleState.DISPOSAL_REQUESTED,
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
// After microtask, should be disposed
|
|
159
|
-
await vi.runAllTimersAsync();
|
|
160
|
-
expect((bloc as any)._disposalState).toBe(BlocLifecycleState.DISPOSED);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('should cancel disposal if consumer is re-added during grace period', () => {
|
|
164
|
-
const bloc = new TestBloc();
|
|
165
|
-
const consumerId1 = 'test-consumer-1';
|
|
166
|
-
const consumerId2 = 'test-consumer-2';
|
|
167
|
-
|
|
168
|
-
// Register bloc
|
|
169
|
-
blac.registerBlocInstance(bloc as BlocBase<unknown>);
|
|
170
|
-
|
|
171
|
-
// Add and remove consumer to trigger disposal
|
|
172
|
-
bloc._addConsumer(consumerId1);
|
|
173
|
-
bloc._removeConsumer(consumerId1);
|
|
174
|
-
|
|
175
|
-
// State should transition to DISPOSAL_REQUESTED immediately
|
|
176
|
-
expect((bloc as any)._disposalState).toBe(
|
|
177
|
-
BlocLifecycleState.DISPOSAL_REQUESTED,
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
// Add new consumer before microtask runs - should fail
|
|
181
|
-
const added = bloc._addConsumer(consumerId2);
|
|
182
|
-
expect(added).toBe(false); // Should fail because bloc is in disposal process
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('should clean up dead WeakRef consumers', () => {
|
|
186
|
-
const bloc = new TestBloc();
|
|
187
|
-
const consumerId1 = 'consumer-1';
|
|
188
|
-
const consumerId2 = 'consumer-2';
|
|
189
|
-
let consumerRef1: any = { name: 'consumer1' };
|
|
190
|
-
const consumerRef2 = { name: 'consumer2' };
|
|
191
|
-
|
|
192
|
-
// Add consumers with refs first
|
|
193
|
-
bloc._addConsumer(consumerId1, consumerRef1);
|
|
194
|
-
bloc._addConsumer(consumerId2, consumerRef2);
|
|
195
|
-
expect(bloc._consumers.size).toBe(2);
|
|
196
|
-
|
|
197
|
-
// Now mock the WeakRef's deref method to simulate garbage collection
|
|
198
|
-
const consumerRefsMap = (bloc as any)._consumerRefs as Map<
|
|
199
|
-
string,
|
|
200
|
-
WeakRef<object>
|
|
201
|
-
>;
|
|
202
|
-
const weakRef1 = consumerRefsMap.get(consumerId1);
|
|
203
|
-
const weakRef2 = consumerRefsMap.get(consumerId2);
|
|
204
|
-
|
|
205
|
-
if (weakRef1) {
|
|
206
|
-
// Mock the deref method to return undefined (simulating GC)
|
|
207
|
-
vi.spyOn(weakRef1, 'deref').mockReturnValue(undefined);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Validate consumers
|
|
211
|
-
bloc._validateConsumers();
|
|
212
|
-
|
|
213
|
-
// First consumer should be removed
|
|
214
|
-
expect(bloc._consumers.size).toBe(1);
|
|
215
|
-
expect(bloc._consumers.has(consumerId1)).toBe(false);
|
|
216
|
-
expect(bloc._consumers.has(consumerId2)).toBe(true);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('should reject consumer additions when bloc is disposed', () => {
|
|
220
|
-
const bloc = new TestBloc();
|
|
221
|
-
|
|
222
|
-
// Force dispose
|
|
223
|
-
bloc._dispose();
|
|
224
|
-
expect((bloc as any)._disposalState).toBe(BlocLifecycleState.DISPOSED);
|
|
225
|
-
|
|
226
|
-
// Try to add consumer
|
|
227
|
-
const added = bloc._addConsumer('new-consumer');
|
|
228
|
-
expect(added).toBe(false);
|
|
229
|
-
expect(bloc._consumers.size).toBe(0);
|
|
230
|
-
});
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
describe('Disposal Behavior', () => {
|
|
234
|
-
it('should properly dispose bloc and clean up resources', () => {
|
|
235
|
-
const bloc = new TestBloc();
|
|
236
|
-
const onDisposeSpy = vi.fn();
|
|
237
|
-
bloc.onDispose = onDisposeSpy;
|
|
238
|
-
|
|
239
|
-
// Add some consumers and observers
|
|
240
|
-
bloc._addConsumer('consumer-1');
|
|
241
|
-
const unsubscribe = bloc._observer.subscribe({
|
|
242
|
-
id: 'observer-1',
|
|
243
|
-
fn: vi.fn(),
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
// Dispose
|
|
247
|
-
const disposed = bloc._dispose();
|
|
248
|
-
expect(disposed).toBe(true);
|
|
249
|
-
expect(onDisposeSpy).toHaveBeenCalled();
|
|
250
|
-
expect(bloc._consumers.size).toBe(0);
|
|
251
|
-
expect(bloc._observer.size).toBe(0);
|
|
252
|
-
expect(bloc.isDisposed).toBe(true);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it('should handle disposal failures gracefully', () => {
|
|
256
|
-
const bloc = new TestBloc();
|
|
257
|
-
bloc.onDispose = () => {
|
|
258
|
-
throw new Error('Disposal error');
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
// Disposal should reset state on error
|
|
262
|
-
expect(() => bloc._dispose()).toThrow('Disposal error');
|
|
263
|
-
expect((bloc as any)._disposalState).toBe(BlocLifecycleState.ACTIVE);
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
it('should schedule disposal with microtask deferral', () => {
|
|
267
|
-
const bloc = new TestBloc();
|
|
268
|
-
blac.registerBlocInstance(bloc as BlocBase<unknown>);
|
|
269
|
-
|
|
270
|
-
// Remove last consumer
|
|
271
|
-
bloc._addConsumer('consumer-1');
|
|
272
|
-
bloc._removeConsumer('consumer-1');
|
|
273
|
-
|
|
274
|
-
// Disposal should be scheduled immediately
|
|
275
|
-
expect((bloc as any)._disposalState).toBe(
|
|
276
|
-
BlocLifecycleState.DISPOSAL_REQUESTED,
|
|
277
|
-
);
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
it('should be idempotent - multiple dispose calls should be safe', () => {
|
|
281
|
-
const bloc = new TestBloc();
|
|
282
|
-
|
|
283
|
-
const result1 = bloc._dispose();
|
|
284
|
-
expect(result1).toBe(true);
|
|
285
|
-
|
|
286
|
-
const result2 = bloc._dispose();
|
|
287
|
-
expect(result2).toBe(false); // Already disposed
|
|
288
|
-
});
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
describe('keepAlive and isolated flags', () => {
|
|
292
|
-
it('should respect keepAlive flag and not dispose when consumers are removed', async () => {
|
|
293
|
-
const bloc = new KeepAliveBloc();
|
|
294
|
-
blac.registerBlocInstance(bloc as BlocBase<unknown>);
|
|
295
|
-
|
|
296
|
-
expect(bloc.isKeepAlive).toBe(true);
|
|
297
|
-
|
|
298
|
-
// Add and remove consumer
|
|
299
|
-
bloc._addConsumer('consumer-1');
|
|
300
|
-
bloc._removeConsumer('consumer-1');
|
|
301
|
-
|
|
302
|
-
// Should not schedule disposal
|
|
303
|
-
await vi.runAllTimersAsync();
|
|
304
|
-
expect((bloc as any)._disposalState).toBe(BlocLifecycleState.ACTIVE);
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
it('should properly inherit isolated flag from static property', () => {
|
|
308
|
-
const bloc = new IsolatedBloc();
|
|
309
|
-
expect(bloc.isIsolated).toBe(true);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it('should handle missing static properties gracefully', () => {
|
|
313
|
-
class BlocWithoutStatics extends BlocBase<number> {
|
|
314
|
-
constructor() {
|
|
315
|
-
super(0);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const bloc = new BlocWithoutStatics();
|
|
320
|
-
expect(bloc.isKeepAlive).toBe(false);
|
|
321
|
-
expect(bloc.isIsolated).toBe(false);
|
|
322
|
-
});
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
describe('State Access During Lifecycle', () => {
|
|
326
|
-
it('should allow state access during all non-disposed states', () => {
|
|
327
|
-
const bloc = new TestBloc(42);
|
|
328
|
-
|
|
329
|
-
// ACTIVE state
|
|
330
|
-
expect(bloc.state).toBe(42);
|
|
331
|
-
|
|
332
|
-
// DISPOSAL_REQUESTED state
|
|
333
|
-
(bloc as any)._atomicStateTransition(
|
|
334
|
-
BlocLifecycleState.ACTIVE,
|
|
335
|
-
BlocLifecycleState.DISPOSAL_REQUESTED,
|
|
336
|
-
);
|
|
337
|
-
expect(bloc.state).toBe(42);
|
|
338
|
-
|
|
339
|
-
// DISPOSING state
|
|
340
|
-
(bloc as any)._atomicStateTransition(
|
|
341
|
-
BlocLifecycleState.DISPOSAL_REQUESTED,
|
|
342
|
-
BlocLifecycleState.DISPOSING,
|
|
343
|
-
);
|
|
344
|
-
expect(bloc.state).toBe(42);
|
|
345
|
-
|
|
346
|
-
// DISPOSED state - should return last known state
|
|
347
|
-
(bloc as any)._atomicStateTransition(
|
|
348
|
-
BlocLifecycleState.DISPOSING,
|
|
349
|
-
BlocLifecycleState.DISPOSED,
|
|
350
|
-
);
|
|
351
|
-
expect(bloc.state).toBe(42);
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
it('should correctly report isDisposed status', () => {
|
|
355
|
-
const bloc = new TestBloc();
|
|
356
|
-
|
|
357
|
-
expect(bloc.isDisposed).toBe(false);
|
|
358
|
-
|
|
359
|
-
bloc._dispose();
|
|
360
|
-
expect(bloc.isDisposed).toBe(true);
|
|
361
|
-
});
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
describe('Edge Cases', () => {
|
|
365
|
-
it('should handle rapid consumer additions and removals', async () => {
|
|
366
|
-
const bloc = new TestBloc();
|
|
367
|
-
blac.registerBlocInstance(bloc as BlocBase<unknown>);
|
|
368
|
-
|
|
369
|
-
// Rapidly add and remove consumers
|
|
370
|
-
for (let i = 0; i < 10; i++) {
|
|
371
|
-
bloc._addConsumer(`consumer-${i}`);
|
|
372
|
-
bloc._removeConsumer(`consumer-${i}`);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Should be in DISPOSAL_REQUESTED state
|
|
376
|
-
expect((bloc as any)._disposalState).toBe(
|
|
377
|
-
BlocLifecycleState.DISPOSAL_REQUESTED,
|
|
378
|
-
);
|
|
379
|
-
|
|
380
|
-
// After microtask, should be disposed
|
|
381
|
-
await vi.runAllTimersAsync();
|
|
382
|
-
expect((bloc as any)._disposalState).toBe(BlocLifecycleState.DISPOSED);
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
it('should handle disposal during active state mutations', () => {
|
|
386
|
-
const bloc = new TestBloc();
|
|
387
|
-
const observer = vi.fn();
|
|
388
|
-
|
|
389
|
-
bloc._observer.subscribe({
|
|
390
|
-
id: 'observer-1',
|
|
391
|
-
fn: observer,
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
// Start a state mutation
|
|
395
|
-
bloc.increment();
|
|
396
|
-
expect(observer).toHaveBeenCalledWith(1, 0, undefined);
|
|
397
|
-
|
|
398
|
-
// Dispose during active usage
|
|
399
|
-
bloc._dispose();
|
|
400
|
-
|
|
401
|
-
// Further mutations should not notify (bloc is disposed)
|
|
402
|
-
observer.mockClear();
|
|
403
|
-
bloc.increment();
|
|
404
|
-
expect(observer).not.toHaveBeenCalled();
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
it('should prevent memory leaks via WeakRef cleanup', () => {
|
|
408
|
-
const bloc = new TestBloc();
|
|
409
|
-
const consumerCount = 100;
|
|
410
|
-
const refs: any[] = [];
|
|
411
|
-
|
|
412
|
-
// Add many consumers
|
|
413
|
-
for (let i = 0; i < consumerCount; i++) {
|
|
414
|
-
const ref = { id: i };
|
|
415
|
-
refs.push(ref);
|
|
416
|
-
bloc._addConsumer(`consumer-${i}`, ref);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
expect(bloc._consumers.size).toBe(consumerCount);
|
|
420
|
-
|
|
421
|
-
// Mock half of the WeakRefs to return undefined (simulating GC)
|
|
422
|
-
const consumerRefsMap = (bloc as any)._consumerRefs as Map<
|
|
423
|
-
string,
|
|
424
|
-
WeakRef<object>
|
|
425
|
-
>;
|
|
426
|
-
let mockCount = 0;
|
|
427
|
-
|
|
428
|
-
for (const [consumerId, weakRef] of consumerRefsMap) {
|
|
429
|
-
if (mockCount < consumerCount / 2) {
|
|
430
|
-
vi.spyOn(weakRef, 'deref').mockReturnValue(undefined);
|
|
431
|
-
mockCount++;
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// Validate should clean up dead refs
|
|
436
|
-
bloc._validateConsumers();
|
|
437
|
-
|
|
438
|
-
expect(bloc._consumers.size).toBe(consumerCount / 2);
|
|
439
|
-
});
|
|
440
|
-
});
|
|
441
|
-
});
|
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { Bloc } from '../Bloc';
|
|
3
|
-
import { Cubit } from '../Cubit';
|
|
4
|
-
import { PropsUpdated } from '../events';
|
|
5
|
-
import { BlacAdapter } from '../adapter/BlacAdapter';
|
|
6
|
-
import { Blac } from '../Blac';
|
|
7
|
-
|
|
8
|
-
// Test types
|
|
9
|
-
interface TestProps {
|
|
10
|
-
query: string;
|
|
11
|
-
filters?: string[];
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface TestState {
|
|
15
|
-
data: string;
|
|
16
|
-
loading: boolean;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Test Cubit with props
|
|
20
|
-
class TestCubit extends Cubit<TestState, TestProps> {
|
|
21
|
-
onPropsChangedMock = vi.fn();
|
|
22
|
-
|
|
23
|
-
constructor() {
|
|
24
|
-
super({ data: '', loading: false });
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
protected onPropsChanged(oldProps: TestProps | undefined, newProps: TestProps): void {
|
|
28
|
-
this.onPropsChangedMock(oldProps, newProps);
|
|
29
|
-
if (oldProps?.query !== newProps.query) {
|
|
30
|
-
this.emit({ ...this.state, data: `Query: ${newProps.query}` });
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
loadData = () => {
|
|
35
|
-
const query = this.props?.query ?? 'default';
|
|
36
|
-
this.emit({ data: `Loaded: ${query}`, loading: false });
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Test Bloc with props
|
|
41
|
-
class TestBloc extends Bloc<TestState, PropsUpdated<TestProps>> {
|
|
42
|
-
constructor() {
|
|
43
|
-
super({ data: '', loading: false });
|
|
44
|
-
|
|
45
|
-
this.on(PropsUpdated<TestProps>, (event, emit) => {
|
|
46
|
-
emit({ ...this.state, data: `Props: ${event.props.query}` });
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
describe('Props functionality', () => {
|
|
52
|
-
beforeEach(() => {
|
|
53
|
-
// Clear Blac instance between tests
|
|
54
|
-
Blac.resetInstance();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe('PropsUpdated event', () => {
|
|
58
|
-
it('should create PropsUpdated event with correct props', () => {
|
|
59
|
-
const props = { query: 'test', filters: ['a', 'b'] };
|
|
60
|
-
const event = new PropsUpdated(props);
|
|
61
|
-
|
|
62
|
-
expect(event.props).toEqual(props);
|
|
63
|
-
expect(event.props).toBe(props); // Should be the same reference
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
describe('Cubit props support', () => {
|
|
68
|
-
it('should support props getter', () => {
|
|
69
|
-
const cubit = new TestCubit();
|
|
70
|
-
expect(cubit.props).toBeNull();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('should update props via _updateProps', () => {
|
|
74
|
-
const cubit = new TestCubit();
|
|
75
|
-
const props = { query: 'test' };
|
|
76
|
-
|
|
77
|
-
(cubit as any)._updateProps(props);
|
|
78
|
-
|
|
79
|
-
expect(cubit.props).toEqual(props);
|
|
80
|
-
expect(cubit.onPropsChangedMock).toHaveBeenCalledWith(null, props);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('should emit state when props change', () => {
|
|
84
|
-
const cubit = new TestCubit();
|
|
85
|
-
const props1 = { query: 'test1' };
|
|
86
|
-
const props2 = { query: 'test2' };
|
|
87
|
-
|
|
88
|
-
(cubit as any)._updateProps(props1);
|
|
89
|
-
expect(cubit.state.data).toBe('Query: test1');
|
|
90
|
-
|
|
91
|
-
(cubit as any)._updateProps(props2);
|
|
92
|
-
expect(cubit.state.data).toBe('Query: test2');
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('should access props in methods', () => {
|
|
96
|
-
const cubit = new TestCubit();
|
|
97
|
-
|
|
98
|
-
cubit.loadData();
|
|
99
|
-
expect(cubit.state.data).toBe('Loaded: default');
|
|
100
|
-
|
|
101
|
-
(cubit as any)._updateProps({ query: 'custom' });
|
|
102
|
-
cubit.loadData();
|
|
103
|
-
expect(cubit.state.data).toBe('Loaded: custom');
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe('Bloc props support', () => {
|
|
108
|
-
it('should handle PropsUpdated events', async () => {
|
|
109
|
-
const bloc = new TestBloc();
|
|
110
|
-
const props = { query: 'search' };
|
|
111
|
-
|
|
112
|
-
await bloc.add(new PropsUpdated(props));
|
|
113
|
-
|
|
114
|
-
expect(bloc.state.data).toBe('Props: search');
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('should queue PropsUpdated events like any other event', async () => {
|
|
118
|
-
const bloc = new TestBloc();
|
|
119
|
-
|
|
120
|
-
await bloc.add(new PropsUpdated({ query: 'first' }));
|
|
121
|
-
await bloc.add(new PropsUpdated({ query: 'second' }));
|
|
122
|
-
|
|
123
|
-
expect(bloc.state.data).toBe('Props: second');
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
describe('BlacAdapter props ownership', () => {
|
|
128
|
-
it('should allow first adapter to own props', () => {
|
|
129
|
-
const Constructor = TestCubit as any;
|
|
130
|
-
const adapter1 = new BlacAdapter(
|
|
131
|
-
{ componentRef: { current: {} }, blocConstructor: Constructor },
|
|
132
|
-
{ props: { query: 'test1' } }
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
expect(() => {
|
|
136
|
-
adapter1.updateProps({ query: 'test2' });
|
|
137
|
-
}).not.toThrow();
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('should prevent non-owner adapters from updating props', () => {
|
|
141
|
-
const warnSpy = vi.spyOn(Blac, 'warn').mockImplementation(() => {});
|
|
142
|
-
const Constructor = TestCubit as any;
|
|
143
|
-
|
|
144
|
-
// First adapter becomes owner
|
|
145
|
-
const adapter1 = new BlacAdapter(
|
|
146
|
-
{ componentRef: { current: {} }, blocConstructor: Constructor },
|
|
147
|
-
{ props: { query: 'test1' } }
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
// Second adapter tries to update props
|
|
151
|
-
const adapter2 = new BlacAdapter(
|
|
152
|
-
{ componentRef: { current: {} }, blocConstructor: Constructor },
|
|
153
|
-
{}
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
adapter2.updateProps({ query: 'hijack' });
|
|
157
|
-
|
|
158
|
-
expect(warnSpy).toHaveBeenCalledWith(
|
|
159
|
-
expect.stringContaining('non-owner adapter')
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
warnSpy.mockRestore();
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('should clear ownership on adapter unmount', () => {
|
|
166
|
-
const Constructor = TestCubit as any;
|
|
167
|
-
const adapter1 = new BlacAdapter(
|
|
168
|
-
{ componentRef: { current: {} }, blocConstructor: Constructor },
|
|
169
|
-
{ props: { query: 'test1' } }
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
adapter1.mount();
|
|
173
|
-
adapter1.unmount();
|
|
174
|
-
|
|
175
|
-
// New adapter should be able to take ownership
|
|
176
|
-
const adapter2 = new BlacAdapter(
|
|
177
|
-
{ componentRef: { current: {} }, blocConstructor: Constructor },
|
|
178
|
-
{ props: { query: 'test2' } }
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
expect(() => {
|
|
182
|
-
adapter2.updateProps({ query: 'test3' });
|
|
183
|
-
}).not.toThrow();
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it('should not update props if they are shallowly equal', () => {
|
|
187
|
-
const cubit = new TestCubit();
|
|
188
|
-
const Constructor = TestCubit as any;
|
|
189
|
-
const adapter = new BlacAdapter(
|
|
190
|
-
{ componentRef: { current: {} }, blocConstructor: Constructor },
|
|
191
|
-
{}
|
|
192
|
-
);
|
|
193
|
-
|
|
194
|
-
const props = { query: 'test' };
|
|
195
|
-
adapter.updateProps(props);
|
|
196
|
-
|
|
197
|
-
// Reset mock
|
|
198
|
-
cubit.onPropsChangedMock.mockClear();
|
|
199
|
-
|
|
200
|
-
// Update with same props
|
|
201
|
-
adapter.updateProps({ query: 'test' });
|
|
202
|
-
|
|
203
|
-
// onPropsChanged should not have been called
|
|
204
|
-
expect(cubit.onPropsChangedMock).not.toHaveBeenCalled();
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it('should ignore props updates during disposal', () => {
|
|
208
|
-
const Constructor = TestCubit as any;
|
|
209
|
-
const adapter = new BlacAdapter(
|
|
210
|
-
{ componentRef: { current: {} }, blocConstructor: Constructor },
|
|
211
|
-
{ props: { query: 'test1' } }
|
|
212
|
-
);
|
|
213
|
-
|
|
214
|
-
// Force disposal state
|
|
215
|
-
(adapter.blocInstance as any)._disposalState = 'disposing';
|
|
216
|
-
|
|
217
|
-
expect(() => {
|
|
218
|
-
adapter.updateProps({ query: 'test2' });
|
|
219
|
-
}).not.toThrow();
|
|
220
|
-
|
|
221
|
-
// Props should not have been updated
|
|
222
|
-
expect((adapter.blocInstance as TestCubit).props?.query).toBe('test1');
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
describe('Props integration', () => {
|
|
227
|
-
it('should dispatch PropsUpdated for Bloc instances', async () => {
|
|
228
|
-
const Constructor = TestBloc as any;
|
|
229
|
-
const adapter = new BlacAdapter(
|
|
230
|
-
{ componentRef: { current: {} }, blocConstructor: Constructor },
|
|
231
|
-
{}
|
|
232
|
-
);
|
|
233
|
-
|
|
234
|
-
const bloc = adapter.blocInstance as TestBloc;
|
|
235
|
-
adapter.updateProps({ query: 'adapter-test' });
|
|
236
|
-
|
|
237
|
-
// Wait for event processing
|
|
238
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
239
|
-
|
|
240
|
-
expect(bloc.state.data).toBe('Props: adapter-test');
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it('should call _updateProps for Cubit instances', () => {
|
|
244
|
-
const Constructor = TestCubit as any;
|
|
245
|
-
const adapter = new BlacAdapter(
|
|
246
|
-
{ componentRef: { current: {} }, blocConstructor: Constructor },
|
|
247
|
-
{}
|
|
248
|
-
);
|
|
249
|
-
|
|
250
|
-
adapter.updateProps({ query: 'adapter-test' });
|
|
251
|
-
|
|
252
|
-
expect((adapter.blocInstance as TestCubit).props?.query).toBe('adapter-test');
|
|
253
|
-
expect((adapter.blocInstance as TestCubit).state.data).toBe('Query: adapter-test');
|
|
254
|
-
});
|
|
255
|
-
});
|
|
256
|
-
});
|