@blac/core 2.0.0-rc-8 → 2.0.0-rc-10
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 +185 -4
- package/dist/index.es.js +1099 -601
- package/dist/index.es.js.map +1 -1
- package/package.json +5 -4
- package/src/Blac.ts +63 -40
- package/src/BlacObserver.ts +58 -24
- package/src/Bloc.ts +166 -157
- package/src/BlocBase.ts +266 -90
- package/src/Cubit.ts +1 -1
- package/src/__tests__/Bloc.event.test.ts +419 -0
- package/src/__tests__/BlocBase.lifecycle.test.ts +441 -0
- package/src/__tests__/Cubit.test.ts +473 -0
- package/src/adapter/BlacAdapter.ts +262 -0
- package/src/adapter/ConsumerTracker.ts +241 -0
- package/src/adapter/ProxyFactory.ts +308 -0
- package/src/adapter/__tests__/BlacAdapter.test.ts +561 -0
- package/src/adapter/__tests__/ConsumerTracker.test.ts +562 -0
- package/src/adapter/__tests__/ProxyFactory.test.ts +655 -0
- package/src/adapter/index.ts +3 -0
- package/src/index.ts +2 -1
- package/src/testing.ts +60 -47
- package/src/types.ts +11 -3
- package/src/utils/uuid.ts +4 -4
- package/src/BlacEvent.ts +0 -5
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { Bloc } from '../Bloc';
|
|
3
|
+
import { Blac } from '../Blac';
|
|
4
|
+
|
|
5
|
+
// Define test events
|
|
6
|
+
class IncrementEvent {
|
|
7
|
+
constructor(public amount: number = 1) {}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class AsyncIncrementEvent {
|
|
11
|
+
constructor(
|
|
12
|
+
public amount: number,
|
|
13
|
+
public delay: number,
|
|
14
|
+
) {}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class DecrementEvent {
|
|
18
|
+
constructor(public amount: number = 1) {}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class ErrorEvent {
|
|
22
|
+
constructor(public message: string) {}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class CriticalErrorEvent extends Error {
|
|
26
|
+
name = 'CriticalError';
|
|
27
|
+
constructor(message: string) {
|
|
28
|
+
super(message);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Test Bloc implementation
|
|
33
|
+
class CounterBloc extends Bloc<
|
|
34
|
+
number,
|
|
35
|
+
IncrementEvent | AsyncIncrementEvent | DecrementEvent | ErrorEvent
|
|
36
|
+
> {
|
|
37
|
+
constructor(initialValue = 0) {
|
|
38
|
+
super(initialValue);
|
|
39
|
+
|
|
40
|
+
// Register event handlers
|
|
41
|
+
this.on(IncrementEvent, (event, emit) => {
|
|
42
|
+
emit(this.state + event.amount);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
this.on(AsyncIncrementEvent, async (event, emit) => {
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, event.delay));
|
|
47
|
+
emit(this.state + event.amount);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
this.on(DecrementEvent, (event, emit) => {
|
|
51
|
+
emit(this.state - event.amount);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
this.on(ErrorEvent, (event, emit) => {
|
|
55
|
+
throw new Error(event.message);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Bloc with critical error handling
|
|
61
|
+
class CriticalErrorBloc extends Bloc<string> {
|
|
62
|
+
constructor() {
|
|
63
|
+
super('initial');
|
|
64
|
+
|
|
65
|
+
this.on(CriticalErrorEvent, (event) => {
|
|
66
|
+
throw event;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe('Bloc Event Handling', () => {
|
|
72
|
+
let bloc: CounterBloc;
|
|
73
|
+
let blacInstance: Blac;
|
|
74
|
+
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
blacInstance = new Blac({ __unsafe_ignore_singleton: true });
|
|
77
|
+
Blac.enableLog = false; // Disable logging for tests
|
|
78
|
+
bloc = blacInstance.getBloc(CounterBloc);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
afterEach(() => {
|
|
82
|
+
vi.clearAllMocks();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('Event Handler Registration', () => {
|
|
86
|
+
it('should register event handlers with on() method', () => {
|
|
87
|
+
expect(bloc.eventHandlers.size).toBe(4); // 4 handlers registered
|
|
88
|
+
expect(bloc.eventHandlers.has(IncrementEvent)).toBe(true);
|
|
89
|
+
expect(bloc.eventHandlers.has(AsyncIncrementEvent)).toBe(true);
|
|
90
|
+
expect(bloc.eventHandlers.has(DecrementEvent)).toBe(true);
|
|
91
|
+
expect(bloc.eventHandlers.has(ErrorEvent)).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should overwrite existing handler when registering same event type', () => {
|
|
95
|
+
const originalHandler = bloc.eventHandlers.get(IncrementEvent);
|
|
96
|
+
|
|
97
|
+
// Register new handler for same event
|
|
98
|
+
const newHandler = vi.fn();
|
|
99
|
+
(bloc as any).on(IncrementEvent, newHandler);
|
|
100
|
+
|
|
101
|
+
expect(bloc.eventHandlers.get(IncrementEvent)).toBe(newHandler);
|
|
102
|
+
expect(bloc.eventHandlers.get(IncrementEvent)).not.toBe(originalHandler);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should handle events with proper type inference', async () => {
|
|
106
|
+
const observer = vi.fn();
|
|
107
|
+
bloc._observer.subscribe({ id: 'test', fn: observer });
|
|
108
|
+
|
|
109
|
+
await bloc.add(new IncrementEvent(5));
|
|
110
|
+
|
|
111
|
+
expect(observer).toHaveBeenCalledWith(5, 0, expect.any(IncrementEvent));
|
|
112
|
+
expect(bloc.state).toBe(5);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('Event Queue and Sequential Processing', () => {
|
|
117
|
+
it('should queue events and process them sequentially', async () => {
|
|
118
|
+
const observer = vi.fn();
|
|
119
|
+
bloc._observer.subscribe({ id: 'test', fn: observer });
|
|
120
|
+
|
|
121
|
+
// Add multiple events rapidly
|
|
122
|
+
const promises = [
|
|
123
|
+
bloc.add(new IncrementEvent(1)),
|
|
124
|
+
bloc.add(new IncrementEvent(2)),
|
|
125
|
+
bloc.add(new IncrementEvent(3)),
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
await Promise.all(promises);
|
|
129
|
+
|
|
130
|
+
// Should be called 3 times with sequential state updates
|
|
131
|
+
expect(observer).toHaveBeenCalledTimes(3);
|
|
132
|
+
expect(observer).toHaveBeenNthCalledWith(
|
|
133
|
+
1,
|
|
134
|
+
1,
|
|
135
|
+
0,
|
|
136
|
+
expect.any(IncrementEvent),
|
|
137
|
+
);
|
|
138
|
+
expect(observer).toHaveBeenNthCalledWith(
|
|
139
|
+
2,
|
|
140
|
+
3,
|
|
141
|
+
1,
|
|
142
|
+
expect.any(IncrementEvent),
|
|
143
|
+
);
|
|
144
|
+
expect(observer).toHaveBeenNthCalledWith(
|
|
145
|
+
3,
|
|
146
|
+
6,
|
|
147
|
+
3,
|
|
148
|
+
expect.any(IncrementEvent),
|
|
149
|
+
);
|
|
150
|
+
expect(bloc.state).toBe(6);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should process async events in order', async () => {
|
|
154
|
+
const observer = vi.fn();
|
|
155
|
+
bloc._observer.subscribe({ id: 'test', fn: observer });
|
|
156
|
+
|
|
157
|
+
// Add async events with different delays
|
|
158
|
+
const promises = [
|
|
159
|
+
bloc.add(new AsyncIncrementEvent(1, 50)),
|
|
160
|
+
bloc.add(new AsyncIncrementEvent(2, 10)),
|
|
161
|
+
bloc.add(new AsyncIncrementEvent(3, 30)),
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
await Promise.all(promises);
|
|
165
|
+
|
|
166
|
+
// Despite different delays, events should process in order
|
|
167
|
+
expect(observer).toHaveBeenCalledTimes(3);
|
|
168
|
+
expect(observer).toHaveBeenNthCalledWith(
|
|
169
|
+
1,
|
|
170
|
+
1,
|
|
171
|
+
0,
|
|
172
|
+
expect.any(AsyncIncrementEvent),
|
|
173
|
+
);
|
|
174
|
+
expect(observer).toHaveBeenNthCalledWith(
|
|
175
|
+
2,
|
|
176
|
+
3,
|
|
177
|
+
1,
|
|
178
|
+
expect.any(AsyncIncrementEvent),
|
|
179
|
+
);
|
|
180
|
+
expect(observer).toHaveBeenNthCalledWith(
|
|
181
|
+
3,
|
|
182
|
+
6,
|
|
183
|
+
3,
|
|
184
|
+
expect.any(AsyncIncrementEvent),
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should handle mixed sync and async events', async () => {
|
|
189
|
+
const observer = vi.fn();
|
|
190
|
+
bloc._observer.subscribe({ id: 'test', fn: observer });
|
|
191
|
+
|
|
192
|
+
const promises = [
|
|
193
|
+
bloc.add(new IncrementEvent(1)),
|
|
194
|
+
bloc.add(new AsyncIncrementEvent(2, 20)),
|
|
195
|
+
bloc.add(new DecrementEvent(1)),
|
|
196
|
+
bloc.add(new AsyncIncrementEvent(3, 10)),
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
await Promise.all(promises);
|
|
200
|
+
|
|
201
|
+
expect(observer).toHaveBeenCalledTimes(4);
|
|
202
|
+
expect(bloc.state).toBe(5); // 0 + 1 + 2 - 1 + 3 = 5
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('Error Handling', () => {
|
|
207
|
+
it('should handle errors in event handlers gracefully', async () => {
|
|
208
|
+
const errorSpy = vi.spyOn(Blac, 'error').mockImplementation(() => {});
|
|
209
|
+
const observer = vi.fn();
|
|
210
|
+
bloc._observer.subscribe({ id: 'test', fn: observer });
|
|
211
|
+
|
|
212
|
+
await bloc.add(new ErrorEvent('Test error'));
|
|
213
|
+
|
|
214
|
+
// Error should be logged but not crash the bloc
|
|
215
|
+
expect(errorSpy).toHaveBeenCalled();
|
|
216
|
+
expect(observer).not.toHaveBeenCalled(); // No state change
|
|
217
|
+
expect(bloc.state).toBe(0); // State unchanged
|
|
218
|
+
|
|
219
|
+
// Bloc should still be functional
|
|
220
|
+
await bloc.add(new IncrementEvent(1));
|
|
221
|
+
expect(bloc.state).toBe(1);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should re-throw critical errors', async () => {
|
|
225
|
+
const criticalBloc = new CriticalErrorBloc();
|
|
226
|
+
|
|
227
|
+
await expect(
|
|
228
|
+
criticalBloc.add(new CriticalErrorEvent('Critical failure')),
|
|
229
|
+
).rejects.toThrow('Critical failure');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should log warning for unhandled events', async () => {
|
|
233
|
+
const warnSpy = vi.spyOn(Blac, 'warn').mockImplementation(() => {});
|
|
234
|
+
|
|
235
|
+
// Create event without handler
|
|
236
|
+
class UnhandledEvent {}
|
|
237
|
+
|
|
238
|
+
await bloc.add(new UnhandledEvent() as any);
|
|
239
|
+
|
|
240
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
241
|
+
expect.stringContaining('No handler registered'),
|
|
242
|
+
expect.any(String),
|
|
243
|
+
expect.any(Array),
|
|
244
|
+
expect.any(String),
|
|
245
|
+
expect.any(UnhandledEvent),
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe('Event Context and Metadata', () => {
|
|
251
|
+
it('should pass event instance to state change notifications', async () => {
|
|
252
|
+
const observer = vi.fn();
|
|
253
|
+
bloc._observer.subscribe({ id: 'test', fn: observer });
|
|
254
|
+
|
|
255
|
+
const event = new IncrementEvent(10);
|
|
256
|
+
await bloc.add(event);
|
|
257
|
+
|
|
258
|
+
expect(observer).toHaveBeenCalledWith(10, 0, event);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should maintain correct state context during handler execution', async () => {
|
|
262
|
+
let capturedStates: number[] = [];
|
|
263
|
+
|
|
264
|
+
// Custom bloc that captures state during handler
|
|
265
|
+
class StateCapturingBloc extends Bloc<number> {
|
|
266
|
+
constructor() {
|
|
267
|
+
super(0);
|
|
268
|
+
|
|
269
|
+
this.on(IncrementEvent, (event, emit) => {
|
|
270
|
+
capturedStates.push(this.state); // Capture current state
|
|
271
|
+
emit(this.state + event.amount);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const capturingBloc = new StateCapturingBloc();
|
|
277
|
+
|
|
278
|
+
await capturingBloc.add(new IncrementEvent(1));
|
|
279
|
+
await capturingBloc.add(new IncrementEvent(2));
|
|
280
|
+
await capturingBloc.add(new IncrementEvent(3));
|
|
281
|
+
|
|
282
|
+
expect(capturedStates).toEqual([0, 1, 3]); // State at time of each handler
|
|
283
|
+
expect(capturingBloc.state).toBe(6);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe('Edge Cases', () => {
|
|
288
|
+
it('should handle empty event queue gracefully', async () => {
|
|
289
|
+
// Process empty queue should not throw
|
|
290
|
+
await (bloc as any)._processEventQueue();
|
|
291
|
+
expect(bloc.state).toBe(0);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should handle rapid event additions during processing', async () => {
|
|
295
|
+
const observer = vi.fn();
|
|
296
|
+
bloc._observer.subscribe({ id: 'test', fn: observer });
|
|
297
|
+
|
|
298
|
+
// Custom bloc that adds more events during processing
|
|
299
|
+
class ChainReactionBloc extends Bloc<number> {
|
|
300
|
+
constructor() {
|
|
301
|
+
super(0);
|
|
302
|
+
|
|
303
|
+
this.on(IncrementEvent, async (event, emit) => {
|
|
304
|
+
emit(this.state + event.amount);
|
|
305
|
+
|
|
306
|
+
// Add another event during processing
|
|
307
|
+
if (this.state < 5) {
|
|
308
|
+
await this.add(new IncrementEvent(1));
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const chainBloc = new ChainReactionBloc();
|
|
315
|
+
const chainObserver = vi.fn();
|
|
316
|
+
chainBloc._observer.subscribe({ id: 'test', fn: chainObserver });
|
|
317
|
+
|
|
318
|
+
await chainBloc.add(new IncrementEvent(1));
|
|
319
|
+
|
|
320
|
+
// Should process all chained events
|
|
321
|
+
expect(chainBloc.state).toBe(5);
|
|
322
|
+
expect(chainObserver).toHaveBeenCalledTimes(5);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should handle event handler that emits multiple times', async () => {
|
|
326
|
+
class MultiEmitBloc extends Bloc<number[]> {
|
|
327
|
+
constructor() {
|
|
328
|
+
super([]);
|
|
329
|
+
|
|
330
|
+
this.on(IncrementEvent, (event, emit) => {
|
|
331
|
+
// Emit multiple state updates
|
|
332
|
+
for (let i = 1; i <= event.amount; i++) {
|
|
333
|
+
emit([...this.state, i]);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const multiBloc = new MultiEmitBloc();
|
|
340
|
+
const observer = vi.fn();
|
|
341
|
+
multiBloc._observer.subscribe({ id: 'test', fn: observer });
|
|
342
|
+
|
|
343
|
+
await multiBloc.add(new IncrementEvent(3));
|
|
344
|
+
|
|
345
|
+
expect(observer).toHaveBeenCalledTimes(3);
|
|
346
|
+
expect(multiBloc.state).toEqual([1, 2, 3]);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('should handle events with constructor-less classes', async () => {
|
|
350
|
+
// Event without explicit constructor
|
|
351
|
+
class SimpleEvent {}
|
|
352
|
+
|
|
353
|
+
class SimpleBloc extends Bloc<string> {
|
|
354
|
+
constructor() {
|
|
355
|
+
super('initial');
|
|
356
|
+
this.on(SimpleEvent, (event, emit) => {
|
|
357
|
+
emit('handled');
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const simpleBloc = new SimpleBloc();
|
|
363
|
+
await simpleBloc.add(new SimpleEvent() as any);
|
|
364
|
+
|
|
365
|
+
expect(simpleBloc.state).toBe('handled');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('should maintain event processing integrity during disposal', async () => {
|
|
369
|
+
const observer = vi.fn();
|
|
370
|
+
bloc._observer.subscribe({ id: 'test', fn: observer });
|
|
371
|
+
|
|
372
|
+
// Start async event processing
|
|
373
|
+
const promise = bloc.add(new AsyncIncrementEvent(5, 50));
|
|
374
|
+
|
|
375
|
+
// Dispose bloc while event is processing
|
|
376
|
+
setTimeout(() => bloc._dispose(), 25);
|
|
377
|
+
|
|
378
|
+
await promise;
|
|
379
|
+
|
|
380
|
+
// Event should complete processing despite disposal attempt
|
|
381
|
+
expect(bloc.state).toBe(5);
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
describe('Performance Considerations', () => {
|
|
386
|
+
it('should handle large event queues efficiently', async () => {
|
|
387
|
+
const eventCount = 1000;
|
|
388
|
+
const promises: Promise<void>[] = [];
|
|
389
|
+
|
|
390
|
+
for (let i = 0; i < eventCount; i++) {
|
|
391
|
+
promises.push(bloc.add(new IncrementEvent(1)));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const startTime = performance.now();
|
|
395
|
+
await Promise.all(promises);
|
|
396
|
+
const endTime = performance.now();
|
|
397
|
+
|
|
398
|
+
expect(bloc.state).toBe(eventCount);
|
|
399
|
+
expect(endTime - startTime).toBeLessThan(1000); // Should process quickly
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('should not leak memory with event references', async () => {
|
|
403
|
+
const events: any[] = [];
|
|
404
|
+
|
|
405
|
+
// Create many events
|
|
406
|
+
for (let i = 0; i < 100; i++) {
|
|
407
|
+
const event = new IncrementEvent(1);
|
|
408
|
+
events.push(event);
|
|
409
|
+
await bloc.add(event);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Clear event array
|
|
413
|
+
events.length = 0;
|
|
414
|
+
|
|
415
|
+
// Events should be garbage collectible after processing
|
|
416
|
+
expect((bloc as any)._eventQueue.length).toBe(0);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
});
|