@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
package/src/Bloc.ts
CHANGED
|
@@ -6,173 +6,182 @@ import { BlocEventConstraint } from './types';
|
|
|
6
6
|
// to access action.constructor and ensure proper event structure.
|
|
7
7
|
// P is for props, changed from any to unknown.
|
|
8
8
|
export abstract class Bloc<
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
S, // State type
|
|
10
|
+
A extends BlocEventConstraint = BlocEventConstraint, // Base Action/Event type with proper constraints
|
|
11
|
+
P = unknown, // Props type
|
|
12
12
|
> extends BlocBase<S, P> {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
13
|
+
// Stores handlers: Map<EventConstructor (subtype of A), HandlerFunction>
|
|
14
|
+
// The handler's event parameter will be correctly typed to the specific EventConstructor
|
|
15
|
+
// by the 'on' method's signature.
|
|
16
|
+
readonly eventHandlers: Map<
|
|
17
|
+
// Key: Constructor of a specific event E (where E extends A)
|
|
18
|
+
// TODO: 'any[]' is required for constructor arguments to allow flexible event instantiation.
|
|
19
|
+
// Using specific parameter types would break type inference for events with different
|
|
20
|
+
// constructor signatures. The 'any[]' enables polymorphic event handling while
|
|
21
|
+
// maintaining type safety through the generic constraint 'E extends A'.
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
+
new (...args: any[]) => A,
|
|
24
|
+
// Value: Handler function. 'event: A' is used here for the stored function type.
|
|
25
|
+
// The 'on' method ensures the specific handler (event: E) is correctly typed.
|
|
26
|
+
(event: A, emit: (newState: S) => void) => void | Promise<void>
|
|
27
|
+
> = new Map();
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* @internal
|
|
37
|
-
* Flag indicating if an event is currently being processed
|
|
38
|
-
*/
|
|
39
|
-
private _isProcessingEvent = false;
|
|
29
|
+
/**
|
|
30
|
+
* @internal
|
|
31
|
+
* Event queue to ensure sequential processing of async events
|
|
32
|
+
*/
|
|
33
|
+
private _eventQueue: A[] = [];
|
|
40
34
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
this.
|
|
66
|
-
|
|
67
|
-
handler as (event: A, emit: (newState: S) => void) => void | Promise<void>
|
|
68
|
-
);
|
|
35
|
+
/**
|
|
36
|
+
* @internal
|
|
37
|
+
* Flag indicating if an event is currently being processed
|
|
38
|
+
*/
|
|
39
|
+
private _isProcessingEvent = false;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Registers an event handler for a specific event type.
|
|
43
|
+
* This method is typically called in the constructor of a derived Bloc class.
|
|
44
|
+
* @param eventConstructor The constructor of the event to handle (e.g., LoadDataEvent).
|
|
45
|
+
* @param handler A function that processes the event and can emit new states.
|
|
46
|
+
* The 'event' parameter in the handler will be typed to the specific eventConstructor.
|
|
47
|
+
*/
|
|
48
|
+
protected on<E extends A>(
|
|
49
|
+
// TODO: 'any[]' is required for constructor arguments (see explanation above).
|
|
50
|
+
// This allows events with different constructor signatures to be handled uniformly.
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
52
|
+
eventConstructor: new (...args: any[]) => E,
|
|
53
|
+
handler: (event: E, emit: (newState: S) => void) => void | Promise<void>,
|
|
54
|
+
): void {
|
|
55
|
+
if (this.eventHandlers.has(eventConstructor)) {
|
|
56
|
+
// Using Blac.warn or a similar logging mechanism from BlocBase if available,
|
|
57
|
+
// otherwise console.warn. Assuming this._name and this._id are available from BlocBase.
|
|
58
|
+
Blac.warn(
|
|
59
|
+
`[Bloc ${this._name}:${String(this._id)}] Handler for event '${eventConstructor.name}' already registered. It will be overwritten.`,
|
|
60
|
+
);
|
|
69
61
|
}
|
|
62
|
+
// Cast the specific handler (event: E) to a more general (event: A) for storage.
|
|
63
|
+
// This is safe because E extends A. When the handler is called with an 'action' of type A,
|
|
64
|
+
// if it was originally registered for type E, 'action' must be an instance of E.
|
|
65
|
+
this.eventHandlers.set(
|
|
66
|
+
eventConstructor,
|
|
67
|
+
handler as (
|
|
68
|
+
event: A,
|
|
69
|
+
emit: (newState: S) => void,
|
|
70
|
+
) => void | Promise<void>,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
70
73
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// If not already processing, start processing the queue
|
|
81
|
-
if (!this._isProcessingEvent) {
|
|
82
|
-
await this._processEventQueue();
|
|
83
|
-
}
|
|
84
|
-
};
|
|
74
|
+
/**
|
|
75
|
+
* Dispatches an action/event to the Bloc.
|
|
76
|
+
* Events are queued and processed sequentially to prevent race conditions.
|
|
77
|
+
* @param action The action/event instance to be processed.
|
|
78
|
+
*/
|
|
79
|
+
public add = async (action: A): Promise<void> => {
|
|
80
|
+
// Add event to queue
|
|
81
|
+
this._eventQueue.push(action);
|
|
85
82
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const action = this._eventQueue.shift()!;
|
|
101
|
-
await this._processEvent(action);
|
|
102
|
-
}
|
|
103
|
-
} finally {
|
|
104
|
-
this._isProcessingEvent = false;
|
|
105
|
-
}
|
|
83
|
+
// If not already processing, start processing the queue
|
|
84
|
+
if (!this._isProcessingEvent) {
|
|
85
|
+
await this._processEventQueue();
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @internal
|
|
91
|
+
* Processes events from the queue sequentially
|
|
92
|
+
*/
|
|
93
|
+
private async _processEventQueue(): Promise<void> {
|
|
94
|
+
// Prevent concurrent processing
|
|
95
|
+
if (this._isProcessingEvent) {
|
|
96
|
+
return;
|
|
106
97
|
}
|
|
107
98
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
99
|
+
this._isProcessingEvent = true;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
while (this._eventQueue.length > 0) {
|
|
103
|
+
const action = this._eventQueue.shift()!;
|
|
104
|
+
await this._processEvent(action);
|
|
105
|
+
}
|
|
106
|
+
} finally {
|
|
107
|
+
this._isProcessingEvent = false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @internal
|
|
113
|
+
* Processes a single event
|
|
114
|
+
*/
|
|
115
|
+
private async _processEvent(action: A): Promise<void> {
|
|
116
|
+
// Using 'any[]' for constructor arguments for broader compatibility.
|
|
117
|
+
// TODO: Type assertion required to cast action.constructor to proper event constructor type.
|
|
118
|
+
// JavaScript's constructor property returns 'Function', but we need the specific event
|
|
119
|
+
// constructor type to look up handlers. This is safe because we validate the action
|
|
120
|
+
// extends the BlocEventConstraint interface.
|
|
121
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
122
|
+
const eventConstructor = action.constructor as new (...args: any[]) => A;
|
|
123
|
+
const handler = this.eventHandlers.get(eventConstructor);
|
|
124
|
+
|
|
125
|
+
if (handler) {
|
|
126
|
+
// Define the 'emit' function that handlers will use to update state.
|
|
127
|
+
// It captures the current state ('this.state') right before each emission
|
|
128
|
+
// to provide the correct 'previousState' to _pushState.
|
|
129
|
+
const emit = (newState: S): void => {
|
|
130
|
+
const previousState = this.state; // State just before this specific emission
|
|
131
|
+
// The 'action' passed to _pushState is the original action that triggered the handler,
|
|
132
|
+
// providing context for the state change (e.g., for logging or plugins).
|
|
133
|
+
this._pushState(newState, previousState, action);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
// Await the handler in case it's an async function (e.g., performs API calls).
|
|
138
|
+
// The 'action' is passed to the handler, and due to the way 'on' is typed,
|
|
139
|
+
// the 'event' parameter within the handler function will be correctly
|
|
140
|
+
// typed to its specific class (e.g., LoadMyFeatureData).
|
|
141
|
+
await handler(action, emit);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
// Enhanced error handling with better context
|
|
144
|
+
const constructorName =
|
|
145
|
+
(action.constructor as { name?: string }).name ||
|
|
146
|
+
'UnnamedConstructor';
|
|
147
|
+
const errorContext = {
|
|
148
|
+
blocName: this._name,
|
|
149
|
+
blocId: String(this._id),
|
|
150
|
+
eventType: constructorName,
|
|
151
|
+
currentState: this.state,
|
|
152
|
+
action: action,
|
|
153
|
+
timestamp: new Date().toISOString(),
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
Blac.error(
|
|
157
|
+
`[Bloc ${this._name}:${String(this._id)}] Error in event handler for '${constructorName}':`,
|
|
158
|
+
error,
|
|
159
|
+
'Context:',
|
|
160
|
+
errorContext,
|
|
161
|
+
);
|
|
121
162
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const previousState = this.state; // State just before this specific emission
|
|
128
|
-
// The 'action' passed to _pushState is the original action that triggered the handler,
|
|
129
|
-
// providing context for the state change (e.g., for logging or plugins).
|
|
130
|
-
this._pushState(newState, previousState, action);
|
|
131
|
-
};
|
|
163
|
+
// TODO: Consider implementing error boundary pattern
|
|
164
|
+
// For now, we log and continue, but applications may want to:
|
|
165
|
+
// 1. Emit an error state
|
|
166
|
+
// 2. Re-throw the error
|
|
167
|
+
// 3. Call an error handler callback
|
|
132
168
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// the 'event' parameter within the handler function will be correctly
|
|
137
|
-
// typed to its specific class (e.g., LoadMyFeatureData).
|
|
138
|
-
await handler(action, emit);
|
|
139
|
-
} catch (error) {
|
|
140
|
-
// Enhanced error handling with better context
|
|
141
|
-
const constructorName = (action.constructor as { name?: string }).name || 'UnnamedConstructor';
|
|
142
|
-
const errorContext = {
|
|
143
|
-
blocName: this._name,
|
|
144
|
-
blocId: String(this._id),
|
|
145
|
-
eventType: constructorName,
|
|
146
|
-
currentState: this.state,
|
|
147
|
-
action: action,
|
|
148
|
-
timestamp: new Date().toISOString()
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
Blac.error(
|
|
152
|
-
`[Bloc ${this._name}:${String(this._id)}] Error in event handler for '${constructorName}':`,
|
|
153
|
-
error,
|
|
154
|
-
"Context:", errorContext
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
// TODO: Consider implementing error boundary pattern
|
|
158
|
-
// For now, we log and continue, but applications may want to:
|
|
159
|
-
// 1. Emit an error state
|
|
160
|
-
// 2. Re-throw the error
|
|
161
|
-
// 3. Call an error handler callback
|
|
162
|
-
|
|
163
|
-
// Optional: Re-throw for critical errors (can be configured)
|
|
164
|
-
if (error instanceof Error && error.name === 'CriticalError') {
|
|
165
|
-
throw error;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
} else {
|
|
169
|
-
// Enhanced warning with more context
|
|
170
|
-
const constructorName = (action.constructor as { name?: string }).name || 'UnnamedConstructor';
|
|
171
|
-
Blac.warn(
|
|
172
|
-
`[Bloc ${this._name}:${String(this._id)}] No handler registered for action type: '${constructorName}'.`,
|
|
173
|
-
"Registered handlers:", Array.from(this.eventHandlers.keys()).map(k => k.name),
|
|
174
|
-
"Action was:", action
|
|
175
|
-
);
|
|
169
|
+
// Optional: Re-throw for critical errors (can be configured)
|
|
170
|
+
if (error instanceof Error && error.name === 'CriticalError') {
|
|
171
|
+
throw error;
|
|
176
172
|
}
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
// Enhanced warning with more context
|
|
176
|
+
const constructorName =
|
|
177
|
+
(action.constructor as { name?: string }).name || 'UnnamedConstructor';
|
|
178
|
+
Blac.warn(
|
|
179
|
+
`[Bloc ${this._name}:${String(this._id)}] No handler registered for action type: '${constructorName}'.`,
|
|
180
|
+
'Registered handlers:',
|
|
181
|
+
Array.from(this.eventHandlers.keys()).map((k) => k.name),
|
|
182
|
+
'Action was:',
|
|
183
|
+
action,
|
|
184
|
+
);
|
|
177
185
|
}
|
|
186
|
+
}
|
|
178
187
|
}
|