@blac/core 2.0.0-rc-5 → 2.0.0-rc-6
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/README.md +42 -0
- package/dist/index.cjs.js +10 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +134 -8
- package/dist/index.es.js +446 -206
- package/dist/index.es.js.map +1 -1
- package/package.json +1 -1
- package/src/Blac.ts +48 -17
- package/src/BlacObserver.ts +33 -19
- package/src/Bloc.ts +82 -21
- package/src/BlocBase.ts +115 -47
- package/src/index.ts +7 -0
- package/src/testing.ts +239 -0
- package/src/types.ts +25 -2
- package/src/utils/uuid.ts +22 -0
package/src/Bloc.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { Blac } from './Blac';
|
|
2
2
|
import { BlocBase } from './BlocBase';
|
|
3
|
+
import { BlocEventConstraint } from './types';
|
|
3
4
|
|
|
4
|
-
// A should be the base type for all events this Bloc handles and must
|
|
5
|
-
// to access action.constructor
|
|
5
|
+
// A should be the base type for all events this Bloc handles and must extend BlocEventConstraint
|
|
6
|
+
// to access action.constructor and ensure proper event structure.
|
|
6
7
|
// P is for props, changed from any to unknown.
|
|
7
8
|
export abstract class Bloc<
|
|
8
9
|
S, // State type
|
|
9
|
-
A extends
|
|
10
|
+
A extends BlocEventConstraint = BlocEventConstraint, // Base Action/Event type with proper constraints
|
|
10
11
|
P = unknown // Props type
|
|
11
12
|
> extends BlocBase<S, P> {
|
|
12
13
|
// Stores handlers: Map<EventConstructor (subtype of A), HandlerFunction>
|
|
@@ -22,6 +23,18 @@ export abstract class Bloc<
|
|
|
22
23
|
(event: A, emit: (newState: S) => void) => void | Promise<void>
|
|
23
24
|
> = new Map();
|
|
24
25
|
|
|
26
|
+
/**
|
|
27
|
+
* @internal
|
|
28
|
+
* Event queue to ensure sequential processing of async events
|
|
29
|
+
*/
|
|
30
|
+
private _eventQueue: A[] = [];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @internal
|
|
34
|
+
* Flag indicating if an event is currently being processed
|
|
35
|
+
*/
|
|
36
|
+
private _isProcessingEvent = false;
|
|
37
|
+
|
|
25
38
|
/**
|
|
26
39
|
* Registers an event handler for a specific event type.
|
|
27
40
|
* This method is typically called in the constructor of a derived Bloc class.
|
|
@@ -53,11 +66,46 @@ export abstract class Bloc<
|
|
|
53
66
|
|
|
54
67
|
/**
|
|
55
68
|
* Dispatches an action/event to the Bloc.
|
|
56
|
-
*
|
|
57
|
-
* Asynchronous handlers are awaited.
|
|
69
|
+
* Events are queued and processed sequentially to prevent race conditions.
|
|
58
70
|
* @param action The action/event instance to be processed.
|
|
59
71
|
*/
|
|
60
72
|
public add = async (action: A): Promise<void> => {
|
|
73
|
+
// Add event to queue
|
|
74
|
+
this._eventQueue.push(action);
|
|
75
|
+
|
|
76
|
+
// If not already processing, start processing the queue
|
|
77
|
+
if (!this._isProcessingEvent) {
|
|
78
|
+
await this._processEventQueue();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @internal
|
|
84
|
+
* Processes events from the queue sequentially
|
|
85
|
+
*/
|
|
86
|
+
private async _processEventQueue(): Promise<void> {
|
|
87
|
+
// Prevent concurrent processing
|
|
88
|
+
if (this._isProcessingEvent) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this._isProcessingEvent = true;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
while (this._eventQueue.length > 0) {
|
|
96
|
+
const action = this._eventQueue.shift()!;
|
|
97
|
+
await this._processEvent(action);
|
|
98
|
+
}
|
|
99
|
+
} finally {
|
|
100
|
+
this._isProcessingEvent = false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @internal
|
|
106
|
+
* Processes a single event
|
|
107
|
+
*/
|
|
108
|
+
private async _processEvent(action: A): Promise<void> {
|
|
61
109
|
// Using 'any[]' for constructor arguments for broader compatibility.
|
|
62
110
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
63
111
|
const eventConstructor = action.constructor as new (...args: any[]) => A;
|
|
@@ -81,29 +129,42 @@ export abstract class Bloc<
|
|
|
81
129
|
// typed to its specific class (e.g., LoadMyFeatureData).
|
|
82
130
|
await handler(action, emit);
|
|
83
131
|
} catch (error) {
|
|
84
|
-
//
|
|
132
|
+
// Enhanced error handling with better context
|
|
133
|
+
const constructorName = (action.constructor as { name?: string }).name || 'UnnamedConstructor';
|
|
134
|
+
const errorContext = {
|
|
135
|
+
blocName: this._name,
|
|
136
|
+
blocId: String(this._id),
|
|
137
|
+
eventType: constructorName,
|
|
138
|
+
currentState: this.state,
|
|
139
|
+
action: action,
|
|
140
|
+
timestamp: new Date().toISOString()
|
|
141
|
+
};
|
|
142
|
+
|
|
85
143
|
Blac.error(
|
|
86
|
-
`[Bloc ${this._name}:${String(this._id)}] Error in event handler for '${
|
|
144
|
+
`[Bloc ${this._name}:${String(this._id)}] Error in event handler for '${constructorName}':`,
|
|
87
145
|
error,
|
|
88
|
-
"
|
|
146
|
+
"Context:", errorContext
|
|
89
147
|
);
|
|
90
|
-
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
148
|
+
|
|
149
|
+
// TODO: Consider implementing error boundary pattern
|
|
150
|
+
// For now, we log and continue, but applications may want to:
|
|
151
|
+
// 1. Emit an error state
|
|
152
|
+
// 2. Re-throw the error
|
|
153
|
+
// 3. Call an error handler callback
|
|
154
|
+
|
|
155
|
+
// Optional: Re-throw for critical errors (can be configured)
|
|
156
|
+
if (error instanceof Error && error.name === 'CriticalError') {
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
95
159
|
}
|
|
96
160
|
} else {
|
|
97
|
-
//
|
|
98
|
-
// If linting still complains, it might be overly strict for this common pattern.
|
|
161
|
+
// Enhanced warning with more context
|
|
99
162
|
const constructorName = (action.constructor as { name?: string }).name || 'UnnamedConstructor';
|
|
100
163
|
Blac.warn(
|
|
101
|
-
`[Bloc ${this._name}:${String(this._id)}] No handler registered for action type: '${constructorName}'
|
|
102
|
-
|
|
164
|
+
`[Bloc ${this._name}:${String(this._id)}] No handler registered for action type: '${constructorName}'.`,
|
|
165
|
+
"Registered handlers:", Array.from(this.eventHandlers.keys()).map(k => k.name),
|
|
166
|
+
"Action was:", action
|
|
103
167
|
);
|
|
104
|
-
// If no handler is found, the action is effectively ignored.
|
|
105
|
-
// Consider if this is the desired behavior or if an error should be thrown
|
|
106
|
-
// or a default handler should be invoked.
|
|
107
168
|
}
|
|
108
|
-
}
|
|
169
|
+
}
|
|
109
170
|
}
|
package/src/BlocBase.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BlacObservable } from './BlacObserver';
|
|
2
|
-
import {
|
|
2
|
+
import { generateUUID } from './utils/uuid';
|
|
3
3
|
|
|
4
4
|
export type BlocInstanceId = string | number | undefined;
|
|
5
5
|
type DependencySelector<S> = (newState: S) => unknown[][];
|
|
@@ -22,7 +22,7 @@ export abstract class BlocBase<
|
|
|
22
22
|
S,
|
|
23
23
|
P = unknown
|
|
24
24
|
> {
|
|
25
|
-
public uid =
|
|
25
|
+
public uid = generateUUID();
|
|
26
26
|
/**
|
|
27
27
|
* When true, every consumer will receive its own unique instance of this Bloc.
|
|
28
28
|
* Use this when state should not be shared between components.
|
|
@@ -85,6 +85,12 @@ export abstract class BlocBase<
|
|
|
85
85
|
*/
|
|
86
86
|
public readonly _createdAt = Date.now();
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* @internal
|
|
90
|
+
* Disposal state to prevent race conditions
|
|
91
|
+
*/
|
|
92
|
+
private _disposalState: 'active' | 'disposing' | 'disposed' = 'active';
|
|
93
|
+
|
|
88
94
|
/**
|
|
89
95
|
* @internal
|
|
90
96
|
* The current state of the Bloc.
|
|
@@ -103,6 +109,50 @@ export abstract class BlocBase<
|
|
|
103
109
|
*/
|
|
104
110
|
public props: P | null = null;
|
|
105
111
|
|
|
112
|
+
/**
|
|
113
|
+
* @internal
|
|
114
|
+
* Flag to prevent batching race conditions
|
|
115
|
+
*/
|
|
116
|
+
private _batchingLock = false;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @internal
|
|
120
|
+
* Pending batched updates
|
|
121
|
+
*/
|
|
122
|
+
private _pendingUpdates: Array<{newState: S, oldState: S, action?: unknown}> = [];
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @internal
|
|
126
|
+
* Map of consumer IDs to their WeakRef objects for proper cleanup
|
|
127
|
+
*/
|
|
128
|
+
private _consumerRefs = new Map<string, WeakRef<object>>();
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @internal
|
|
132
|
+
* Validates that all consumer references are still alive
|
|
133
|
+
* Removes dead consumers automatically
|
|
134
|
+
*/
|
|
135
|
+
_validateConsumers = (): void => {
|
|
136
|
+
const deadConsumers: string[] = [];
|
|
137
|
+
|
|
138
|
+
for (const [consumerId, weakRef] of this._consumerRefs) {
|
|
139
|
+
if (weakRef.deref() === undefined) {
|
|
140
|
+
deadConsumers.push(consumerId);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Clean up dead consumers
|
|
145
|
+
for (const consumerId of deadConsumers) {
|
|
146
|
+
this._consumers.delete(consumerId);
|
|
147
|
+
this._consumerRefs.delete(consumerId);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Schedule disposal if no live consumers remain
|
|
151
|
+
if (this._consumers.size === 0 && !this._keepAlive && this._disposalState === 'active') {
|
|
152
|
+
this._scheduleDisposal();
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
106
156
|
/**
|
|
107
157
|
* Creates a new BlocBase instance with the given initial state.
|
|
108
158
|
* Sets up the observer, registers with the Blac manager, and initializes addons.
|
|
@@ -157,8 +207,15 @@ export abstract class BlocBase<
|
|
|
157
207
|
* Notifies the Blac manager and clears all observers.
|
|
158
208
|
*/
|
|
159
209
|
_dispose() {
|
|
160
|
-
//
|
|
210
|
+
// Prevent re-entrant disposal using atomic state change
|
|
211
|
+
if (this._disposalState !== 'active') {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
this._disposalState = 'disposing';
|
|
215
|
+
|
|
216
|
+
// Clear all consumers and their references
|
|
161
217
|
this._consumers.clear();
|
|
218
|
+
this._consumerRefs.clear();
|
|
162
219
|
|
|
163
220
|
// Clear observer subscriptions
|
|
164
221
|
this._observer.clear();
|
|
@@ -166,6 +223,9 @@ export abstract class BlocBase<
|
|
|
166
223
|
// Call user-defined disposal hook
|
|
167
224
|
this.onDispose?.();
|
|
168
225
|
|
|
226
|
+
// Mark as fully disposed
|
|
227
|
+
this._disposalState = 'disposed';
|
|
228
|
+
|
|
169
229
|
// The Blac manager will handle removal from registry
|
|
170
230
|
// This method is called by Blac.disposeBloc, so we don't need to call it again
|
|
171
231
|
}
|
|
@@ -182,12 +242,6 @@ export abstract class BlocBase<
|
|
|
182
242
|
*/
|
|
183
243
|
_consumers = new Set<string>();
|
|
184
244
|
|
|
185
|
-
/**
|
|
186
|
-
* @internal
|
|
187
|
-
* WeakSet to track consumer references for cleanup validation
|
|
188
|
-
*/
|
|
189
|
-
private _consumerRefs = new WeakSet<object>();
|
|
190
|
-
|
|
191
245
|
/**
|
|
192
246
|
* @internal
|
|
193
247
|
* Registers a new consumer to this Bloc instance.
|
|
@@ -197,11 +251,17 @@ export abstract class BlocBase<
|
|
|
197
251
|
* @param consumerRef Optional reference to the consumer object for cleanup validation
|
|
198
252
|
*/
|
|
199
253
|
_addConsumer = (consumerId: string, consumerRef?: object) => {
|
|
254
|
+
// Prevent adding consumers to disposed blocs
|
|
255
|
+
if (this._disposalState !== 'active') {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
200
259
|
if (this._consumers.has(consumerId)) return;
|
|
201
260
|
this._consumers.add(consumerId);
|
|
202
261
|
|
|
262
|
+
// Store WeakRef for proper memory management
|
|
203
263
|
if (consumerRef) {
|
|
204
|
-
this._consumerRefs.
|
|
264
|
+
this._consumerRefs.set(consumerId, new WeakRef(consumerRef));
|
|
205
265
|
}
|
|
206
266
|
|
|
207
267
|
// this._blac.dispatchEvent(BlacLifecycleEvent.BLOC_CONSUMER_ADDED, this, { consumerId });
|
|
@@ -216,11 +276,13 @@ export abstract class BlocBase<
|
|
|
216
276
|
*/
|
|
217
277
|
_removeConsumer = (consumerId: string) => {
|
|
218
278
|
if (!this._consumers.has(consumerId)) return;
|
|
279
|
+
|
|
219
280
|
this._consumers.delete(consumerId);
|
|
281
|
+
this._consumerRefs.delete(consumerId);
|
|
220
282
|
// this._blac.dispatchEvent(BlacLifecycleEvent.BLOC_CONSUMER_REMOVED, this, { consumerId });
|
|
221
283
|
|
|
222
284
|
// If no consumers remain and not keep-alive, schedule disposal
|
|
223
|
-
if (this._consumers.size === 0 && !this._keepAlive) {
|
|
285
|
+
if (this._consumers.size === 0 && !this._keepAlive && this._disposalState === 'active') {
|
|
224
286
|
this._scheduleDisposal();
|
|
225
287
|
}
|
|
226
288
|
};
|
|
@@ -244,16 +306,19 @@ export abstract class BlocBase<
|
|
|
244
306
|
* Schedules disposal of this bloc instance if it has no consumers
|
|
245
307
|
*/
|
|
246
308
|
private _scheduleDisposal() {
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
309
|
+
// Prevent multiple disposal attempts
|
|
310
|
+
if (this._disposalState !== 'active') {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Double-check conditions before disposal
|
|
315
|
+
if (this._consumers.size === 0 && !this._keepAlive) {
|
|
316
|
+
if (this._disposalHandler) {
|
|
317
|
+
this._disposalHandler(this as any);
|
|
318
|
+
} else {
|
|
319
|
+
this._dispose();
|
|
255
320
|
}
|
|
256
|
-
}
|
|
321
|
+
}
|
|
257
322
|
}
|
|
258
323
|
|
|
259
324
|
lastUpdate = Date.now();
|
|
@@ -264,12 +329,6 @@ export abstract class BlocBase<
|
|
|
264
329
|
*/
|
|
265
330
|
private _batchingEnabled = false;
|
|
266
331
|
|
|
267
|
-
/**
|
|
268
|
-
* @internal
|
|
269
|
-
* Pending state updates when batching is enabled
|
|
270
|
-
*/
|
|
271
|
-
private _pendingUpdates: Array<{newState: S, oldState: S, action?: unknown}> = [];
|
|
272
|
-
|
|
273
332
|
/**
|
|
274
333
|
* @internal
|
|
275
334
|
* Updates the state and notifies all observers of the change.
|
|
@@ -279,28 +338,33 @@ export abstract class BlocBase<
|
|
|
279
338
|
* @param action Optional metadata about what caused the state change
|
|
280
339
|
*/
|
|
281
340
|
_pushState = (newState: S, oldState: S, action?: unknown): void => {
|
|
282
|
-
//
|
|
341
|
+
// Validate newState
|
|
283
342
|
if (newState === undefined) {
|
|
284
343
|
console.warn('BlocBase._pushState: newState is undefined', this);
|
|
285
344
|
return;
|
|
286
345
|
}
|
|
287
|
-
|
|
346
|
+
|
|
288
347
|
// Validate action type if provided
|
|
289
|
-
if (action !== undefined && action !==
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
console.warn('BlocBase._pushState: Invalid action type', this, action);
|
|
293
|
-
}
|
|
348
|
+
if (action !== undefined && typeof action !== 'object' && typeof action !== 'function') {
|
|
349
|
+
console.warn('BlocBase._pushState: Invalid action type', this, action);
|
|
350
|
+
return;
|
|
294
351
|
}
|
|
295
352
|
|
|
296
|
-
// If batching is enabled, queue the update
|
|
297
353
|
if (this._batchingEnabled) {
|
|
354
|
+
// When batching, just accumulate the updates
|
|
298
355
|
this._pendingUpdates.push({ newState, oldState, action });
|
|
356
|
+
|
|
357
|
+
// Update internal state for consistency
|
|
358
|
+
this._oldState = oldState;
|
|
359
|
+
this._state = newState;
|
|
299
360
|
return;
|
|
300
361
|
}
|
|
301
|
-
|
|
362
|
+
|
|
363
|
+
// Normal state update flow
|
|
302
364
|
this._oldState = oldState;
|
|
303
365
|
this._state = newState;
|
|
366
|
+
|
|
367
|
+
// Notify observers of the state change
|
|
304
368
|
this._observer.notify(newState, oldState, action);
|
|
305
369
|
this.lastUpdate = Date.now();
|
|
306
370
|
};
|
|
@@ -310,28 +374,32 @@ export abstract class BlocBase<
|
|
|
310
374
|
* @param batchFn Function to execute with batching enabled
|
|
311
375
|
*/
|
|
312
376
|
batch = <T>(batchFn: () => T): T => {
|
|
313
|
-
|
|
314
|
-
this.
|
|
377
|
+
// Prevent batching race conditions
|
|
378
|
+
if (this._batchingLock) {
|
|
379
|
+
// If already batching, just execute the function without nesting batches
|
|
380
|
+
return batchFn();
|
|
381
|
+
}
|
|
315
382
|
|
|
383
|
+
this._batchingLock = true;
|
|
384
|
+
this._batchingEnabled = true;
|
|
385
|
+
this._pendingUpdates = [];
|
|
386
|
+
|
|
316
387
|
try {
|
|
317
388
|
const result = batchFn();
|
|
318
389
|
|
|
319
|
-
// Process all
|
|
390
|
+
// Process all batched updates
|
|
320
391
|
if (this._pendingUpdates.length > 0) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
this.
|
|
324
|
-
|
|
325
|
-
// Notify with the final state
|
|
326
|
-
this._observer.notify(lastUpdate.newState, this._oldState, lastUpdate.action);
|
|
392
|
+
// Only notify once with the final state
|
|
393
|
+
const finalUpdate = this._pendingUpdates[this._pendingUpdates.length - 1];
|
|
394
|
+
this._observer.notify(finalUpdate.newState, finalUpdate.oldState, finalUpdate.action);
|
|
327
395
|
this.lastUpdate = Date.now();
|
|
328
|
-
|
|
329
|
-
this._pendingUpdates = [];
|
|
330
396
|
}
|
|
331
397
|
|
|
332
398
|
return result;
|
|
333
399
|
} finally {
|
|
334
|
-
this._batchingEnabled =
|
|
400
|
+
this._batchingEnabled = false;
|
|
401
|
+
this._batchingLock = false;
|
|
402
|
+
this._pendingUpdates = [];
|
|
335
403
|
}
|
|
336
404
|
};
|
|
337
405
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
export * from './Blac';
|
|
2
|
+
export * from './BlacEvent';
|
|
2
3
|
export * from './BlacObserver';
|
|
3
4
|
export * from './Bloc';
|
|
4
5
|
export * from './BlocBase';
|
|
5
6
|
export * from './Cubit';
|
|
6
7
|
export * from './types';
|
|
7
8
|
|
|
9
|
+
// Utilities
|
|
10
|
+
export * from './utils/uuid';
|
|
11
|
+
|
|
12
|
+
// Test utilities
|
|
13
|
+
export * from './testing';
|
|
14
|
+
|
package/src/testing.ts
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { Blac } from './Blac';
|
|
2
|
+
import { Bloc } from './Bloc';
|
|
3
|
+
import { BlocBase } from './BlocBase';
|
|
4
|
+
import { Cubit } from './Cubit';
|
|
5
|
+
import { BlocEventConstraint } from './types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Test utilities for Blac state management
|
|
9
|
+
*/
|
|
10
|
+
export class BlocTest {
|
|
11
|
+
private static originalInstance: Blac;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Sets up a clean test environment
|
|
15
|
+
*/
|
|
16
|
+
static setUp(): void {
|
|
17
|
+
this.originalInstance = Blac.instance;
|
|
18
|
+
Blac.resetInstance();
|
|
19
|
+
Blac.enableLog = false; // Disable logging in tests by default
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Tears down the test environment and restores original state
|
|
24
|
+
*/
|
|
25
|
+
static tearDown(): void {
|
|
26
|
+
Blac.resetInstance();
|
|
27
|
+
// Note: Cannot restore original instance due to singleton pattern
|
|
28
|
+
// Tests should use setUp/tearDown properly to manage state
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates a test bloc with automatic cleanup
|
|
33
|
+
*/
|
|
34
|
+
static createBloc<T extends BlocBase<any>>(
|
|
35
|
+
BlocClass: new (...args: any[]) => T,
|
|
36
|
+
...args: any[]
|
|
37
|
+
): T {
|
|
38
|
+
const bloc = new BlocClass(...args);
|
|
39
|
+
Blac.activateBloc(bloc);
|
|
40
|
+
return bloc;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Waits for a bloc to emit a specific state
|
|
45
|
+
*/
|
|
46
|
+
static async waitForState<T extends BlocBase<S>, S>(
|
|
47
|
+
bloc: T,
|
|
48
|
+
predicate: (state: S) => boolean,
|
|
49
|
+
timeout = 5000
|
|
50
|
+
): Promise<S> {
|
|
51
|
+
return new Promise<S>((resolve, reject) => {
|
|
52
|
+
const timeoutId = setTimeout(() => {
|
|
53
|
+
reject(new Error(`Timeout waiting for state matching predicate after ${timeout}ms`));
|
|
54
|
+
}, timeout);
|
|
55
|
+
|
|
56
|
+
const unsubscribe = bloc._observer.subscribe({
|
|
57
|
+
id: `test-waiter-${crypto.randomUUID()}`,
|
|
58
|
+
fn: (newState: S) => {
|
|
59
|
+
if (predicate(newState)) {
|
|
60
|
+
clearTimeout(timeoutId);
|
|
61
|
+
unsubscribe();
|
|
62
|
+
resolve(newState);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Check current state immediately
|
|
68
|
+
if (predicate(bloc.state)) {
|
|
69
|
+
clearTimeout(timeoutId);
|
|
70
|
+
unsubscribe();
|
|
71
|
+
resolve(bloc.state);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Expects a bloc to emit specific states in order
|
|
78
|
+
*/
|
|
79
|
+
static async expectStates<T extends BlocBase<S>, S>(
|
|
80
|
+
bloc: T,
|
|
81
|
+
expectedStates: S[],
|
|
82
|
+
timeout = 5000
|
|
83
|
+
): Promise<void> {
|
|
84
|
+
const receivedStates: S[] = [];
|
|
85
|
+
|
|
86
|
+
return new Promise<void>((resolve, reject) => {
|
|
87
|
+
const timeoutId = setTimeout(() => {
|
|
88
|
+
reject(new Error(
|
|
89
|
+
`Timeout waiting for states. Expected: ${JSON.stringify(expectedStates)}, ` +
|
|
90
|
+
`Received: ${JSON.stringify(receivedStates)}`
|
|
91
|
+
));
|
|
92
|
+
}, timeout);
|
|
93
|
+
|
|
94
|
+
const unsubscribe = bloc._observer.subscribe({
|
|
95
|
+
id: `test-expecter-${crypto.randomUUID()}`,
|
|
96
|
+
fn: (newState: S) => {
|
|
97
|
+
receivedStates.push(newState);
|
|
98
|
+
|
|
99
|
+
// Check if we have all expected states
|
|
100
|
+
if (receivedStates.length === expectedStates.length) {
|
|
101
|
+
clearTimeout(timeoutId);
|
|
102
|
+
unsubscribe();
|
|
103
|
+
|
|
104
|
+
// Verify all states match using deep equality
|
|
105
|
+
for (let i = 0; i < expectedStates.length; i++) {
|
|
106
|
+
const expected = expectedStates[i];
|
|
107
|
+
const received = receivedStates[i];
|
|
108
|
+
|
|
109
|
+
// Use JSON comparison for deep equality
|
|
110
|
+
if (JSON.stringify(expected) !== JSON.stringify(received)) {
|
|
111
|
+
reject(new Error(
|
|
112
|
+
`State mismatch at index ${i}. Expected: ${JSON.stringify(expected)}, ` +
|
|
113
|
+
`Received: ${JSON.stringify(received)}`
|
|
114
|
+
));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
resolve();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Mock Bloc for testing
|
|
129
|
+
*/
|
|
130
|
+
export class MockBloc<S, A extends BlocEventConstraint = BlocEventConstraint> extends Bloc<S, A> {
|
|
131
|
+
private mockHandlers = new Map<string, (event: A, emit: (newState: S) => void) => void | Promise<void>>();
|
|
132
|
+
|
|
133
|
+
constructor(initialState: S) {
|
|
134
|
+
super(initialState);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Mock an event handler for testing
|
|
139
|
+
*/
|
|
140
|
+
mockEventHandler<E extends A>(
|
|
141
|
+
eventConstructor: new (...args: any[]) => E,
|
|
142
|
+
handler: (event: E, emit: (newState: S) => void) => void | Promise<void>
|
|
143
|
+
): void {
|
|
144
|
+
// Use the on method to register the mock handler
|
|
145
|
+
this.on(eventConstructor, handler);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get the number of registered handlers
|
|
150
|
+
*/
|
|
151
|
+
getHandlerCount(): number {
|
|
152
|
+
return this.eventHandlers.size;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Check if a handler is registered for an event type
|
|
157
|
+
*/
|
|
158
|
+
hasHandler(eventConstructor: new (...args: any[]) => A): boolean {
|
|
159
|
+
return this.eventHandlers.has(eventConstructor);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Mock Cubit for testing
|
|
165
|
+
*/
|
|
166
|
+
export class MockCubit<S> extends Cubit<S> {
|
|
167
|
+
private stateHistory: S[] = [];
|
|
168
|
+
|
|
169
|
+
constructor(initialState: S) {
|
|
170
|
+
super(initialState);
|
|
171
|
+
this.stateHistory.push(initialState);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Override emit to track state history
|
|
176
|
+
*/
|
|
177
|
+
emit(newState: S): void {
|
|
178
|
+
this.stateHistory.push(newState);
|
|
179
|
+
super.emit(newState);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get the history of all states
|
|
184
|
+
*/
|
|
185
|
+
getStateHistory(): S[] {
|
|
186
|
+
return [...this.stateHistory];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Clear state history
|
|
191
|
+
*/
|
|
192
|
+
clearStateHistory(): void {
|
|
193
|
+
this.stateHistory = [this.state];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Memory leak detector for tests
|
|
199
|
+
*/
|
|
200
|
+
export class MemoryLeakDetector {
|
|
201
|
+
private initialStats: ReturnType<typeof Blac.getMemoryStats>;
|
|
202
|
+
|
|
203
|
+
constructor() {
|
|
204
|
+
this.initialStats = Blac.getMemoryStats();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Check for memory leaks and return a report
|
|
209
|
+
*/
|
|
210
|
+
checkForLeaks(): {
|
|
211
|
+
hasLeaks: boolean;
|
|
212
|
+
report: string;
|
|
213
|
+
stats: ReturnType<typeof Blac.getMemoryStats>;
|
|
214
|
+
} {
|
|
215
|
+
const currentStats = Blac.getMemoryStats();
|
|
216
|
+
const hasLeaks = (
|
|
217
|
+
currentStats.registeredBlocs > this.initialStats.registeredBlocs ||
|
|
218
|
+
currentStats.isolatedBlocs > this.initialStats.isolatedBlocs ||
|
|
219
|
+
currentStats.keepAliveBlocs > this.initialStats.keepAliveBlocs
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const report = `
|
|
223
|
+
Memory Leak Detection Report:
|
|
224
|
+
- Initial registered blocs: ${this.initialStats.registeredBlocs}
|
|
225
|
+
- Current registered blocs: ${currentStats.registeredBlocs}
|
|
226
|
+
- Initial isolated blocs: ${this.initialStats.isolatedBlocs}
|
|
227
|
+
- Current isolated blocs: ${currentStats.isolatedBlocs}
|
|
228
|
+
- Initial keep-alive blocs: ${this.initialStats.keepAliveBlocs}
|
|
229
|
+
- Current keep-alive blocs: ${currentStats.keepAliveBlocs}
|
|
230
|
+
- Potential leaks detected: ${hasLeaks ? 'YES' : 'NO'}
|
|
231
|
+
`.trim();
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
hasLeaks,
|
|
235
|
+
report,
|
|
236
|
+
stats: currentStats
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|