@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
|
@@ -74,10 +74,10 @@ describe('BlacAdapter', () => {
|
|
|
74
74
|
it('should create adapter with proper initialization', () => {
|
|
75
75
|
const adapter = new BlacAdapter(
|
|
76
76
|
{ componentRef, blocConstructor: CounterCubit },
|
|
77
|
-
{
|
|
77
|
+
{ instanceId: 'test-counter' },
|
|
78
78
|
);
|
|
79
79
|
|
|
80
|
-
expect(adapter.id).toMatch(/^
|
|
80
|
+
expect(adapter.id).toMatch(/^adapter-/);
|
|
81
81
|
expect(adapter.blocConstructor).toBe(CounterCubit);
|
|
82
82
|
expect(adapter.componentRef).toBe(componentRef);
|
|
83
83
|
expect(adapter.blocInstance).toBeInstanceOf(CounterCubit);
|
|
@@ -87,36 +87,39 @@ describe('BlacAdapter', () => {
|
|
|
87
87
|
// Create first adapter
|
|
88
88
|
const adapter1 = new BlacAdapter(
|
|
89
89
|
{ componentRef, blocConstructor: CounterCubit },
|
|
90
|
-
{
|
|
90
|
+
{ instanceId: 'shared-counter' },
|
|
91
91
|
);
|
|
92
92
|
|
|
93
93
|
// Create second adapter with same id
|
|
94
94
|
const componentRef2 = { current: {} };
|
|
95
95
|
const adapter2 = new BlacAdapter(
|
|
96
96
|
{ componentRef: componentRef2, blocConstructor: CounterCubit },
|
|
97
|
-
{
|
|
97
|
+
{ instanceId: 'shared-counter' },
|
|
98
98
|
);
|
|
99
99
|
|
|
100
100
|
// Should share the same bloc instance
|
|
101
101
|
expect(adapter1.blocInstance).toBe(adapter2.blocInstance);
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
-
it('should pass
|
|
105
|
-
class PropsCubit extends Cubit<string
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
) {
|
|
104
|
+
it('should pass staticProps to bloc constructor', () => {
|
|
105
|
+
class PropsCubit extends Cubit<string> {
|
|
106
|
+
props?: { initialValue: string };
|
|
107
|
+
|
|
108
|
+
constructor(props?: { initialValue: string }) {
|
|
109
109
|
super(props?.initialValue || 'default');
|
|
110
|
+
this.props = props;
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
const adapter = new BlacAdapter(
|
|
114
115
|
{ componentRef, blocConstructor: PropsCubit as any },
|
|
115
|
-
{
|
|
116
|
+
{ staticProps: { initialValue: 'custom' } },
|
|
116
117
|
);
|
|
117
118
|
|
|
118
119
|
expect(adapter.blocInstance.state).toBe('custom');
|
|
119
|
-
expect(adapter.blocInstance.props).toEqual({
|
|
120
|
+
expect((adapter.blocInstance as PropsCubit).props).toEqual({
|
|
121
|
+
initialValue: 'custom',
|
|
122
|
+
});
|
|
120
123
|
});
|
|
121
124
|
});
|
|
122
125
|
|
|
@@ -199,16 +202,13 @@ describe('BlacAdapter', () => {
|
|
|
199
202
|
});
|
|
200
203
|
|
|
201
204
|
// Access state through proxy
|
|
202
|
-
const proxyState = adapter.
|
|
205
|
+
const proxyState = adapter.getStateProxy();
|
|
203
206
|
const name = proxyState.name;
|
|
204
207
|
const email = proxyState.profile.email;
|
|
205
208
|
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
);
|
|
210
|
-
expect(dependencies?.statePaths).toContain('name');
|
|
211
|
-
expect(dependencies?.statePaths).toContain('profile.email');
|
|
209
|
+
// Verify proxy state works correctly
|
|
210
|
+
expect(name).toBe('John');
|
|
211
|
+
expect(email).toBe('john@example.com');
|
|
212
212
|
});
|
|
213
213
|
|
|
214
214
|
it('should track class property access through proxy', () => {
|
|
@@ -232,16 +232,13 @@ describe('BlacAdapter', () => {
|
|
|
232
232
|
});
|
|
233
233
|
|
|
234
234
|
// Access getters through proxy
|
|
235
|
-
const proxyBloc = adapter.
|
|
235
|
+
const proxyBloc = adapter.getBlocProxy();
|
|
236
236
|
const doubled = proxyBloc.doubled;
|
|
237
237
|
const isPositive = proxyBloc.isPositive;
|
|
238
238
|
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
);
|
|
243
|
-
expect(dependencies?.classPaths).toContain('doubled');
|
|
244
|
-
expect(dependencies?.classPaths).toContain('isPositive');
|
|
239
|
+
// Verify proxy getters work correctly
|
|
240
|
+
expect(doubled).toBe(0);
|
|
241
|
+
expect(isPositive).toBe(false);
|
|
245
242
|
});
|
|
246
243
|
|
|
247
244
|
it('should only notify when tracked values change', () => {
|
|
@@ -251,21 +248,25 @@ describe('BlacAdapter', () => {
|
|
|
251
248
|
blocConstructor: UserCubit,
|
|
252
249
|
});
|
|
253
250
|
|
|
254
|
-
//
|
|
255
|
-
adapter.
|
|
256
|
-
const proxyState = adapter.getProxyState(adapter.blocInstance.state);
|
|
257
|
-
const name = proxyState.name; // Only track name
|
|
258
|
-
|
|
259
|
-
// Mark as rendered to enable dependency checking
|
|
260
|
-
adapter.updateLastNotified(componentRef.current);
|
|
251
|
+
// Mount the adapter first
|
|
252
|
+
adapter.mount();
|
|
261
253
|
|
|
254
|
+
// Create subscription with automatic tracking
|
|
262
255
|
const unsubscribe = adapter.createSubscription({ onChange });
|
|
263
256
|
|
|
257
|
+
// Access specific property through proxy to track it
|
|
258
|
+
const proxyState = adapter.getStateProxy();
|
|
259
|
+
const name = proxyState.name; // Track name property
|
|
260
|
+
|
|
261
|
+
// Clear onChange calls from initial subscription
|
|
262
|
+
onChange.mockClear();
|
|
263
|
+
|
|
264
264
|
// Change tracked property - should notify
|
|
265
265
|
adapter.blocInstance.updateName('Jane');
|
|
266
266
|
expect(onChange).toHaveBeenCalledTimes(1);
|
|
267
267
|
|
|
268
|
-
//
|
|
268
|
+
// For automatic tracking, only tracked properties trigger notifications
|
|
269
|
+
// Since we only accessed 'name', changing 'theme' should not notify
|
|
269
270
|
onChange.mockClear();
|
|
270
271
|
adapter.blocInstance.updateTheme('dark');
|
|
271
272
|
expect(onChange).not.toHaveBeenCalled();
|
|
@@ -280,14 +281,15 @@ describe('BlacAdapter', () => {
|
|
|
280
281
|
});
|
|
281
282
|
|
|
282
283
|
// Access nested property
|
|
283
|
-
const proxyState = adapter.
|
|
284
|
+
const proxyState = adapter.getStateProxy();
|
|
284
285
|
const theme = proxyState.profile.preferences.theme;
|
|
285
286
|
|
|
286
|
-
//
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
expect(
|
|
287
|
+
// Verify nested property access works
|
|
288
|
+
expect(theme).toBe('light');
|
|
289
|
+
|
|
290
|
+
// Verify nested proxy returns correct values
|
|
291
|
+
expect(proxyState.profile.email).toBe('john@example.com');
|
|
292
|
+
expect(proxyState.profile.preferences).toEqual({ theme: 'light' });
|
|
291
293
|
});
|
|
292
294
|
});
|
|
293
295
|
|
|
@@ -304,17 +306,16 @@ describe('BlacAdapter', () => {
|
|
|
304
306
|
// Mount
|
|
305
307
|
adapter.mount();
|
|
306
308
|
expect(onMount).toHaveBeenCalledWith(adapter.blocInstance);
|
|
307
|
-
expect(adapter.
|
|
308
|
-
expect(adapter.blocInstance._consumers.has(adapter.id)).toBe(true);
|
|
309
|
+
expect(adapter.mountTime).toBeGreaterThan(0);
|
|
309
310
|
|
|
310
|
-
// Mount again -
|
|
311
|
+
// Mount again - onMount is called each time
|
|
311
312
|
adapter.mount();
|
|
312
|
-
expect(onMount).toHaveBeenCalledTimes(
|
|
313
|
+
expect(onMount).toHaveBeenCalledTimes(2);
|
|
313
314
|
|
|
314
315
|
// Unmount
|
|
315
316
|
adapter.unmount();
|
|
316
317
|
expect(onUnmount).toHaveBeenCalledWith(adapter.blocInstance);
|
|
317
|
-
expect(adapter.
|
|
318
|
+
expect(adapter.unmountTime).toBeGreaterThan(0);
|
|
318
319
|
});
|
|
319
320
|
|
|
320
321
|
it('should handle onMount errors', () => {
|
|
@@ -371,7 +372,7 @@ describe('BlacAdapter', () => {
|
|
|
371
372
|
const onChange = vi.fn();
|
|
372
373
|
const unsubscribe = adapter.createSubscription({ onChange });
|
|
373
374
|
|
|
374
|
-
expect(adapter.blocInstance.
|
|
375
|
+
expect(adapter.blocInstance.subscriptionCount).toBe(1);
|
|
375
376
|
|
|
376
377
|
// Trigger state change
|
|
377
378
|
adapter.blocInstance.increment();
|
|
@@ -379,7 +380,7 @@ describe('BlacAdapter', () => {
|
|
|
379
380
|
|
|
380
381
|
// Cleanup
|
|
381
382
|
unsubscribe();
|
|
382
|
-
expect(adapter.blocInstance.
|
|
383
|
+
expect(adapter.blocInstance.subscriptionCount).toBe(0);
|
|
383
384
|
});
|
|
384
385
|
|
|
385
386
|
it('should handle first render without dependencies check', () => {
|
|
@@ -407,7 +408,7 @@ describe('BlacAdapter', () => {
|
|
|
407
408
|
{ dependencies: (bloc) => [bloc.state.name] },
|
|
408
409
|
);
|
|
409
410
|
|
|
410
|
-
const state = adapter.
|
|
411
|
+
const state = adapter.getStateProxy();
|
|
411
412
|
|
|
412
413
|
// Should be raw state, not proxy
|
|
413
414
|
expect(state).toBe(adapter.blocInstance.state);
|
|
@@ -419,7 +420,7 @@ describe('BlacAdapter', () => {
|
|
|
419
420
|
{ dependencies: (bloc) => [bloc.state] },
|
|
420
421
|
);
|
|
421
422
|
|
|
422
|
-
const blocInstance = adapter.
|
|
423
|
+
const blocInstance = adapter.getBlocProxy();
|
|
423
424
|
|
|
424
425
|
// Should be raw instance, not proxy
|
|
425
426
|
expect(blocInstance).toBe(adapter.blocInstance);
|
|
@@ -431,8 +432,8 @@ describe('BlacAdapter', () => {
|
|
|
431
432
|
blocConstructor: UserCubit,
|
|
432
433
|
});
|
|
433
434
|
|
|
434
|
-
const proxyState = adapter.
|
|
435
|
-
const proxyBloc = adapter.
|
|
435
|
+
const proxyState = adapter.getStateProxy();
|
|
436
|
+
const proxyBloc = adapter.getBlocProxy();
|
|
436
437
|
|
|
437
438
|
// Should be proxies
|
|
438
439
|
expect(proxyState).not.toBe(adapter.blocInstance.state);
|
|
@@ -441,7 +442,7 @@ describe('BlacAdapter', () => {
|
|
|
441
442
|
});
|
|
442
443
|
|
|
443
444
|
describe('Consumer Tracking Integration', () => {
|
|
444
|
-
it('should properly
|
|
445
|
+
it('should properly handle subscription lifecycle', () => {
|
|
445
446
|
const adapter = new BlacAdapter({
|
|
446
447
|
componentRef,
|
|
447
448
|
blocConstructor: CounterCubit,
|
|
@@ -449,42 +450,35 @@ describe('BlacAdapter', () => {
|
|
|
449
450
|
|
|
450
451
|
adapter.mount();
|
|
451
452
|
|
|
452
|
-
|
|
453
|
-
const
|
|
454
|
-
componentRef.current,
|
|
455
|
-
new Set(['state']),
|
|
456
|
-
);
|
|
457
|
-
expect(hasConsumer).toBe(true); // First render always notifies
|
|
453
|
+
const onChange = vi.fn();
|
|
454
|
+
const unsubscribe = adapter.createSubscription({ onChange });
|
|
458
455
|
|
|
459
|
-
//
|
|
460
|
-
adapter.
|
|
456
|
+
// State change should notify
|
|
457
|
+
adapter.blocInstance.increment();
|
|
458
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
461
459
|
|
|
462
|
-
//
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
);
|
|
467
|
-
expect(shouldNotify).toBe(false);
|
|
460
|
+
// After unsubscribe, no more notifications
|
|
461
|
+
unsubscribe();
|
|
462
|
+
adapter.blocInstance.increment();
|
|
463
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
468
464
|
});
|
|
469
465
|
|
|
470
|
-
it('should reset
|
|
466
|
+
it('should reset tracking state properly', () => {
|
|
471
467
|
const adapter = new BlacAdapter({
|
|
472
468
|
componentRef,
|
|
473
469
|
blocConstructor: UserCubit,
|
|
474
470
|
});
|
|
475
471
|
|
|
476
|
-
//
|
|
477
|
-
adapter.
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
const depsBefore = adapter.getConsumerDependencies(componentRef.current);
|
|
481
|
-
expect(depsBefore?.statePaths.length).toBe(2);
|
|
472
|
+
// Access state to potentially track
|
|
473
|
+
const proxyState = adapter.getStateProxy();
|
|
474
|
+
const name = proxyState.name;
|
|
482
475
|
|
|
483
|
-
// Reset tracking
|
|
484
|
-
adapter.
|
|
476
|
+
// Reset tracking clears internal state
|
|
477
|
+
adapter.resetTracking();
|
|
485
478
|
|
|
486
|
-
|
|
487
|
-
|
|
479
|
+
// Verify adapter is still functional after reset
|
|
480
|
+
const theme = proxyState.profile.preferences.theme;
|
|
481
|
+
expect(theme).toBe('light');
|
|
488
482
|
});
|
|
489
483
|
});
|
|
490
484
|
|
|
@@ -534,7 +528,7 @@ describe('BlacAdapter', () => {
|
|
|
534
528
|
adapter.unmount();
|
|
535
529
|
}
|
|
536
530
|
|
|
537
|
-
expect(adapter.blocInstance.
|
|
531
|
+
expect(adapter.blocInstance.subscriptionCount).toBe(0);
|
|
538
532
|
});
|
|
539
533
|
|
|
540
534
|
it('should handle value changes with Object.is semantics', () => {
|
|
@@ -191,7 +191,7 @@ describe('ProxyFactory', () => {
|
|
|
191
191
|
expect(userName).toBe('Jane');
|
|
192
192
|
|
|
193
193
|
// Array methods should work
|
|
194
|
-
const mapped = proxy.items.map((x) => x * 2);
|
|
194
|
+
const mapped = proxy.items.map((x: number) => x * 2);
|
|
195
195
|
expect(mapped).toEqual([2, 4, 6]);
|
|
196
196
|
});
|
|
197
197
|
|
package/src/adapter/index.ts
CHANGED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export enum ErrorCategory {
|
|
2
|
+
LIFECYCLE = 'LIFECYCLE',
|
|
3
|
+
STATE = 'STATE',
|
|
4
|
+
PLUGIN = 'PLUGIN',
|
|
5
|
+
VALIDATION = 'VALIDATION',
|
|
6
|
+
DISPOSAL = 'DISPOSAL',
|
|
7
|
+
CONSUMER = 'CONSUMER',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export enum ErrorSeverity {
|
|
11
|
+
FATAL = 'FATAL', // System cannot continue
|
|
12
|
+
ERROR = 'ERROR', // Operation failed but system continues
|
|
13
|
+
WARNING = 'WARNING', // Potential issue but operation succeeded
|
|
14
|
+
INFO = 'INFO', // Informational, not an error
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type BlacErrorContext = {
|
|
18
|
+
blocName?: string;
|
|
19
|
+
blocId?: string;
|
|
20
|
+
consumerId?: string;
|
|
21
|
+
pluginName?: string;
|
|
22
|
+
operation?: string;
|
|
23
|
+
metadata?: Record<string, unknown>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export class BlacError extends Error {
|
|
27
|
+
constructor(
|
|
28
|
+
message: string,
|
|
29
|
+
public readonly category: ErrorCategory,
|
|
30
|
+
public readonly severity: ErrorSeverity,
|
|
31
|
+
public readonly context?: BlacErrorContext,
|
|
32
|
+
public readonly cause?: unknown,
|
|
33
|
+
) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = 'BlacError';
|
|
36
|
+
|
|
37
|
+
// Ensure proper prototype chain
|
|
38
|
+
Object.setPrototypeOf(this, BlacError.prototype);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
toString(): string {
|
|
42
|
+
const contextStr = this.context
|
|
43
|
+
? ` [${Object.entries(this.context)
|
|
44
|
+
.filter(([_, v]) => v !== undefined)
|
|
45
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
46
|
+
.join(', ')}]`
|
|
47
|
+
: '';
|
|
48
|
+
|
|
49
|
+
return `${this.name} [${this.severity}/${this.category}]: ${this.message}${contextStr}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type ErrorHandler = (error: BlacError) => void;
|
|
54
|
+
|
|
55
|
+
export type ErrorHandlingStrategy = {
|
|
56
|
+
shouldPropagate: (error: BlacError) => boolean;
|
|
57
|
+
shouldLog: (error: BlacError) => boolean;
|
|
58
|
+
handle: (error: BlacError) => void;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const DefaultErrorStrategy: ErrorHandlingStrategy = {
|
|
62
|
+
shouldPropagate: (error) => error.severity === ErrorSeverity.FATAL,
|
|
63
|
+
shouldLog: (error) => error.severity !== ErrorSeverity.INFO,
|
|
64
|
+
handle: (error) => {
|
|
65
|
+
if (DefaultErrorStrategy.shouldLog(error)) {
|
|
66
|
+
console.error(error.toString(), error.cause);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BlacError,
|
|
3
|
+
ErrorHandler,
|
|
4
|
+
ErrorHandlingStrategy,
|
|
5
|
+
DefaultErrorStrategy,
|
|
6
|
+
} from './BlacError';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Centralized error management system for BlaC framework.
|
|
10
|
+
* Handles error processing, logging, and propagation strategies.
|
|
11
|
+
*/
|
|
12
|
+
export class ErrorManager {
|
|
13
|
+
private static instance: ErrorManager;
|
|
14
|
+
private handlers: Set<ErrorHandler> = new Set();
|
|
15
|
+
private strategy: ErrorHandlingStrategy = DefaultErrorStrategy;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get the singleton instance of ErrorManager
|
|
19
|
+
*/
|
|
20
|
+
static getInstance(): ErrorManager {
|
|
21
|
+
if (!ErrorManager.instance) {
|
|
22
|
+
ErrorManager.instance = new ErrorManager();
|
|
23
|
+
}
|
|
24
|
+
return ErrorManager.instance;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Set a custom error handling strategy
|
|
29
|
+
*/
|
|
30
|
+
setStrategy(strategy: ErrorHandlingStrategy): void {
|
|
31
|
+
this.strategy = strategy;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Add a custom error handler that will be called for all errors
|
|
36
|
+
*/
|
|
37
|
+
addHandler(handler: ErrorHandler): void {
|
|
38
|
+
this.handlers.add(handler);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Remove a previously added error handler
|
|
43
|
+
*/
|
|
44
|
+
removeHandler(handler: ErrorHandler): void {
|
|
45
|
+
this.handlers.delete(handler);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Process an error through all handlers and apply the configured strategy
|
|
50
|
+
*/
|
|
51
|
+
handle(error: BlacError): void {
|
|
52
|
+
// Always notify handlers first
|
|
53
|
+
this.handlers.forEach((handler) => {
|
|
54
|
+
try {
|
|
55
|
+
handler(error);
|
|
56
|
+
} catch (handlerError) {
|
|
57
|
+
// Prevent handler errors from breaking error handling
|
|
58
|
+
console.error('Error in error handler:', handlerError);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Apply strategy
|
|
63
|
+
this.strategy.handle(error);
|
|
64
|
+
|
|
65
|
+
// Propagate if needed
|
|
66
|
+
if (this.strategy.shouldPropagate(error)) {
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Convenience method for wrapping operations with error handling
|
|
73
|
+
*/
|
|
74
|
+
wrap<T>(operation: () => T, errorFactory: (error: unknown) => BlacError): T {
|
|
75
|
+
try {
|
|
76
|
+
return operation();
|
|
77
|
+
} catch (error) {
|
|
78
|
+
const blacError = errorFactory(error);
|
|
79
|
+
this.handle(blacError);
|
|
80
|
+
// Will only reach here if strategy says not to propagate
|
|
81
|
+
return undefined as T;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Async version of wrap
|
|
87
|
+
*/
|
|
88
|
+
async wrapAsync<T>(
|
|
89
|
+
operation: () => Promise<T>,
|
|
90
|
+
errorFactory: (error: unknown) => BlacError,
|
|
91
|
+
): Promise<T> {
|
|
92
|
+
try {
|
|
93
|
+
return await operation();
|
|
94
|
+
} catch (error) {
|
|
95
|
+
const blacError = errorFactory(error);
|
|
96
|
+
this.handle(blacError);
|
|
97
|
+
// Will only reach here if strategy says not to propagate
|
|
98
|
+
return undefined as T;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BlacError,
|
|
3
|
+
ErrorCategory,
|
|
4
|
+
ErrorSeverity,
|
|
5
|
+
BlacErrorContext,
|
|
6
|
+
} from './BlacError';
|
|
7
|
+
import { ErrorManager } from './ErrorManager';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Standard error handling helper for BlaC
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* - For fatal errors that should propagate: handleError.fatal(...)
|
|
14
|
+
* - For recoverable errors: handleError.error(...)
|
|
15
|
+
* - For warnings: handleError.warn(...)
|
|
16
|
+
*/
|
|
17
|
+
export const handleError = {
|
|
18
|
+
fatal: (
|
|
19
|
+
message: string,
|
|
20
|
+
category: ErrorCategory,
|
|
21
|
+
context?: BlacErrorContext,
|
|
22
|
+
cause?: unknown,
|
|
23
|
+
): never => {
|
|
24
|
+
const error = new BlacError(
|
|
25
|
+
message,
|
|
26
|
+
category,
|
|
27
|
+
ErrorSeverity.FATAL,
|
|
28
|
+
context,
|
|
29
|
+
cause,
|
|
30
|
+
);
|
|
31
|
+
ErrorManager.getInstance().handle(error);
|
|
32
|
+
// This will always throw due to FATAL severity
|
|
33
|
+
throw error;
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
error: (
|
|
37
|
+
message: string,
|
|
38
|
+
category: ErrorCategory,
|
|
39
|
+
context?: BlacErrorContext,
|
|
40
|
+
cause?: unknown,
|
|
41
|
+
): void => {
|
|
42
|
+
const error = new BlacError(
|
|
43
|
+
message,
|
|
44
|
+
category,
|
|
45
|
+
ErrorSeverity.ERROR,
|
|
46
|
+
context,
|
|
47
|
+
cause,
|
|
48
|
+
);
|
|
49
|
+
ErrorManager.getInstance().handle(error);
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
warn: (
|
|
53
|
+
message: string,
|
|
54
|
+
category: ErrorCategory,
|
|
55
|
+
context?: BlacErrorContext,
|
|
56
|
+
cause?: unknown,
|
|
57
|
+
): void => {
|
|
58
|
+
const error = new BlacError(
|
|
59
|
+
message,
|
|
60
|
+
category,
|
|
61
|
+
ErrorSeverity.WARNING,
|
|
62
|
+
context,
|
|
63
|
+
cause,
|
|
64
|
+
);
|
|
65
|
+
ErrorManager.getInstance().handle(error);
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
info: (
|
|
69
|
+
message: string,
|
|
70
|
+
category: ErrorCategory,
|
|
71
|
+
context?: BlacErrorContext,
|
|
72
|
+
): void => {
|
|
73
|
+
const error = new BlacError(message, category, ErrorSeverity.INFO, context);
|
|
74
|
+
ErrorManager.getInstance().handle(error);
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Wraps a function to catch and handle errors
|
|
79
|
+
*/
|
|
80
|
+
wrap: <T>(
|
|
81
|
+
fn: () => T,
|
|
82
|
+
category: ErrorCategory,
|
|
83
|
+
context?: BlacErrorContext,
|
|
84
|
+
severity: ErrorSeverity = ErrorSeverity.ERROR,
|
|
85
|
+
): T | undefined => {
|
|
86
|
+
try {
|
|
87
|
+
return fn();
|
|
88
|
+
} catch (cause) {
|
|
89
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
90
|
+
const error = new BlacError(message, category, severity, context, cause);
|
|
91
|
+
ErrorManager.getInstance().handle(error);
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Wraps an async function to catch and handle errors
|
|
98
|
+
*/
|
|
99
|
+
wrapAsync: async <T>(
|
|
100
|
+
fn: () => Promise<T>,
|
|
101
|
+
category: ErrorCategory,
|
|
102
|
+
context?: BlacErrorContext,
|
|
103
|
+
severity: ErrorSeverity = ErrorSeverity.ERROR,
|
|
104
|
+
): Promise<T | undefined> => {
|
|
105
|
+
try {
|
|
106
|
+
return await fn();
|
|
107
|
+
} catch (cause) {
|
|
108
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
109
|
+
const error = new BlacError(message, category, severity, context, cause);
|
|
110
|
+
ErrorManager.getInstance().handle(error);
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export {
|
|
2
|
+
BlacError,
|
|
3
|
+
ErrorCategory,
|
|
4
|
+
ErrorSeverity,
|
|
5
|
+
DefaultErrorStrategy,
|
|
6
|
+
} from './BlacError';
|
|
7
|
+
export type {
|
|
8
|
+
BlacErrorContext,
|
|
9
|
+
ErrorHandler,
|
|
10
|
+
ErrorHandlingStrategy,
|
|
11
|
+
} from './BlacError';
|
|
12
|
+
export { ErrorManager } from './ErrorManager';
|
|
13
|
+
export { handleError } from './handleError';
|
package/src/events.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event fired when props are updated on a Bloc or Cubit instance.
|
|
3
|
+
* This is primarily used internally by the framework to notify
|
|
4
|
+
* about prop changes in React components.
|
|
5
|
+
* @template P - The type of props being updated
|
|
6
|
+
*/
|
|
1
7
|
export class PropsUpdated<P = any> {
|
|
2
8
|
constructor(public readonly props: P) {}
|
|
3
|
-
}
|
|
9
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
export * from './Blac';
|
|
2
|
-
export * from './BlacObserver';
|
|
3
2
|
export * from './Bloc';
|
|
4
3
|
export * from './BlocBase';
|
|
5
4
|
export * from './Cubit';
|
|
6
5
|
export * from './types';
|
|
7
6
|
export * from './events';
|
|
7
|
+
export * from './subscription';
|
|
8
|
+
export * from './lifecycle';
|
|
8
9
|
|
|
9
10
|
// Utilities
|
|
10
11
|
export * from './utils/uuid';
|
|
11
12
|
export * from './utils/shallowEqual';
|
|
13
|
+
export * from './utils/generateInstanceId';
|
|
14
|
+
export * from './utils/RerenderLogger';
|
|
12
15
|
|
|
13
16
|
// Test utilities
|
|
14
17
|
export * from './testing';
|
|
@@ -18,3 +21,6 @@ export * from './adapter';
|
|
|
18
21
|
|
|
19
22
|
// Plugins
|
|
20
23
|
export * from './plugins';
|
|
24
|
+
|
|
25
|
+
// Error handling
|
|
26
|
+
export * from './errors';
|