@blac/core 2.0.0-rc-10 → 2.0.0-rc-13
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 +27 -0
- package/dist/index.cjs.js +2 -2
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +294 -11
- package/dist/index.es.js +869 -397
- package/dist/index.es.js.map +1 -1
- package/package.json +1 -1
- package/src/Blac.ts +94 -9
- package/src/BlacObserver.ts +3 -0
- package/src/Bloc.ts +43 -49
- package/src/BlocBase.ts +107 -4
- package/src/Cubit.ts +14 -0
- package/src/__tests__/Blac.config.test.ts +86 -0
- package/src/__tests__/plugins.test.ts +454 -0
- package/src/__tests__/props.test.ts +256 -0
- package/src/adapter/BlacAdapter.ts +73 -8
- package/src/events.ts +3 -0
- package/src/index.ts +5 -0
- package/src/plugins/BlocPluginRegistry.ts +220 -0
- package/src/plugins/SystemPluginRegistry.ts +192 -0
- package/src/plugins/index.ts +8 -0
- package/src/plugins/types.ts +100 -0
- package/src/types.ts +1 -10
- package/src/utils/shallowEqual.ts +40 -0
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { Blac } from '../Blac';
|
|
2
|
-
import { BlocBase } from '../BlocBase';
|
|
2
|
+
import { BlocBase, BlocLifecycleState } from '../BlocBase';
|
|
3
3
|
import { BlocConstructor, BlocState, InferPropsFromGeneric } from '../types';
|
|
4
|
+
import { PropsUpdated } from '../events';
|
|
4
5
|
import { generateUUID } from '../utils/uuid';
|
|
6
|
+
import { shallowEqual } from '../utils/shallowEqual';
|
|
5
7
|
import { ConsumerTracker, DependencyArray } from './ConsumerTracker';
|
|
6
8
|
import { ProxyFactory } from './ProxyFactory';
|
|
7
9
|
|
|
8
10
|
export interface AdapterOptions<B extends BlocBase<any>> {
|
|
9
11
|
id?: string;
|
|
10
12
|
dependencies?: (bloc: B) => unknown[];
|
|
11
|
-
props?:
|
|
13
|
+
props?: any;
|
|
12
14
|
onMount?: (bloc: B) => void;
|
|
13
15
|
onUnmount?: (bloc: B) => void;
|
|
14
16
|
}
|
|
@@ -26,6 +28,10 @@ export class BlacAdapter<B extends BlocConstructor<BlocBase<any>>> {
|
|
|
26
28
|
// Core components
|
|
27
29
|
private consumerTracker: ConsumerTracker;
|
|
28
30
|
|
|
31
|
+
// Props ownership tracking
|
|
32
|
+
private static propsOwners = new WeakMap<BlocBase<any>, string>();
|
|
33
|
+
private lastProps?: any;
|
|
34
|
+
|
|
29
35
|
unmountTime: number = 0;
|
|
30
36
|
mountTime: number = 0;
|
|
31
37
|
|
|
@@ -59,6 +65,11 @@ export class BlacAdapter<B extends BlocConstructor<BlocBase<any>>> {
|
|
|
59
65
|
if (this.isUsingDependencies && options?.dependencies) {
|
|
60
66
|
this.dependencyValues = options.dependencies(this.blocInstance);
|
|
61
67
|
}
|
|
68
|
+
|
|
69
|
+
// Handle initial props if provided
|
|
70
|
+
if (options?.props !== undefined) {
|
|
71
|
+
this.updateProps(options.props);
|
|
72
|
+
}
|
|
62
73
|
}
|
|
63
74
|
|
|
64
75
|
trackAccess(
|
|
@@ -133,7 +144,7 @@ export class BlacAdapter<B extends BlocConstructor<BlocBase<any>>> {
|
|
|
133
144
|
newState: BlocState<InstanceType<B>>,
|
|
134
145
|
oldState: BlocState<InstanceType<B>>,
|
|
135
146
|
) => {
|
|
136
|
-
//
|
|
147
|
+
// Case 1: Manual dependencies provided
|
|
137
148
|
if (this.isUsingDependencies && this.options?.dependencies) {
|
|
138
149
|
const newValues = this.options.dependencies(this.blocInstance);
|
|
139
150
|
const hasChanged = this.hasDependencyValuesChanged(
|
|
@@ -146,7 +157,15 @@ export class BlacAdapter<B extends BlocConstructor<BlocBase<any>>> {
|
|
|
146
157
|
}
|
|
147
158
|
|
|
148
159
|
this.dependencyValues = newValues;
|
|
149
|
-
}
|
|
160
|
+
}
|
|
161
|
+
// Case 2: Proxy tracking disabled globally (and no manual dependencies)
|
|
162
|
+
else if (!Blac.config.proxyDependencyTracking) {
|
|
163
|
+
// Always trigger re-render when proxy tracking is disabled
|
|
164
|
+
options.onChange();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
// Case 3: Proxy tracking enabled (default behavior)
|
|
168
|
+
else {
|
|
150
169
|
// Check if any tracked values have changed (proxy-based tracking)
|
|
151
170
|
const consumerInfo = this.consumerTracker.getConsumerInfo(
|
|
152
171
|
this.componentRef.current,
|
|
@@ -202,6 +221,11 @@ export class BlacAdapter<B extends BlocConstructor<BlocBase<any>>> {
|
|
|
202
221
|
this.consumerTracker.unregister(this.componentRef.current);
|
|
203
222
|
this.blocInstance._removeConsumer(this.id);
|
|
204
223
|
|
|
224
|
+
// Clear ownership if we own this bloc
|
|
225
|
+
if (BlacAdapter.propsOwners.get(this.blocInstance) === this.id) {
|
|
226
|
+
BlacAdapter.propsOwners.delete(this.blocInstance);
|
|
227
|
+
}
|
|
228
|
+
|
|
205
229
|
// Call onUnmount callback
|
|
206
230
|
if (this.options?.onUnmount) {
|
|
207
231
|
try {
|
|
@@ -215,8 +239,9 @@ export class BlacAdapter<B extends BlocConstructor<BlocBase<any>>> {
|
|
|
215
239
|
getProxyState = (
|
|
216
240
|
state: BlocState<InstanceType<B>>,
|
|
217
241
|
): BlocState<InstanceType<B>> => {
|
|
218
|
-
if
|
|
219
|
-
|
|
242
|
+
// Return raw state if proxy tracking is disabled globally or using manual dependencies
|
|
243
|
+
if (this.isUsingDependencies || !Blac.config.proxyDependencyTracking) {
|
|
244
|
+
return state;
|
|
220
245
|
}
|
|
221
246
|
|
|
222
247
|
return ProxyFactory.getProxyState({
|
|
@@ -227,8 +252,9 @@ export class BlacAdapter<B extends BlocConstructor<BlocBase<any>>> {
|
|
|
227
252
|
};
|
|
228
253
|
|
|
229
254
|
getProxyBlocInstance = (): InstanceType<B> => {
|
|
230
|
-
if
|
|
231
|
-
|
|
255
|
+
// Return raw instance if proxy tracking is disabled globally or using manual dependencies
|
|
256
|
+
if (this.isUsingDependencies || !Blac.config.proxyDependencyTracking) {
|
|
257
|
+
return this.blocInstance;
|
|
232
258
|
}
|
|
233
259
|
|
|
234
260
|
return ProxyFactory.getProxyBlocInstance({
|
|
@@ -243,6 +269,45 @@ export class BlacAdapter<B extends BlocConstructor<BlocBase<any>>> {
|
|
|
243
269
|
return this.hasMounted;
|
|
244
270
|
}
|
|
245
271
|
|
|
272
|
+
updateProps(props: any): void {
|
|
273
|
+
const bloc = this.blocInstance;
|
|
274
|
+
|
|
275
|
+
// Check ownership
|
|
276
|
+
if (!BlacAdapter.propsOwners.has(bloc)) {
|
|
277
|
+
// First adapter with props becomes the owner
|
|
278
|
+
BlacAdapter.propsOwners.set(bloc, this.id);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (BlacAdapter.propsOwners.get(bloc) !== this.id) {
|
|
282
|
+
Blac.warn(
|
|
283
|
+
`[BlacAdapter] Attempted to set props on ${bloc.constructor.name} from non-owner adapter`
|
|
284
|
+
);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Disposal safety
|
|
289
|
+
if ((bloc as any)._disposalState === BlocLifecycleState.DISPOSED ||
|
|
290
|
+
(bloc as any)._disposalState === BlocLifecycleState.DISPOSING) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Check if props have changed
|
|
295
|
+
if (shallowEqual(this.lastProps, props)) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Update props based on bloc type
|
|
300
|
+
if ('add' in bloc && typeof bloc.add === 'function') {
|
|
301
|
+
// Bloc: dispatch PropsUpdated event
|
|
302
|
+
(bloc as any).add(new PropsUpdated(props));
|
|
303
|
+
} else if ('_updateProps' in bloc && typeof (bloc as any)._updateProps === 'function') {
|
|
304
|
+
// Cubit: direct props update
|
|
305
|
+
(bloc as any)._updateProps(props);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
this.lastProps = props;
|
|
309
|
+
}
|
|
310
|
+
|
|
246
311
|
private hasDependencyValuesChanged(
|
|
247
312
|
prev: unknown[] | undefined,
|
|
248
313
|
next: unknown[],
|
package/src/events.ts
ADDED
package/src/index.ts
CHANGED
|
@@ -4,12 +4,17 @@ export * from './Bloc';
|
|
|
4
4
|
export * from './BlocBase';
|
|
5
5
|
export * from './Cubit';
|
|
6
6
|
export * from './types';
|
|
7
|
+
export * from './events';
|
|
7
8
|
|
|
8
9
|
// Utilities
|
|
9
10
|
export * from './utils/uuid';
|
|
11
|
+
export * from './utils/shallowEqual';
|
|
10
12
|
|
|
11
13
|
// Test utilities
|
|
12
14
|
export * from './testing';
|
|
13
15
|
|
|
14
16
|
// Adapter
|
|
15
17
|
export * from './adapter';
|
|
18
|
+
|
|
19
|
+
// Plugins
|
|
20
|
+
export * from './plugins';
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { BlocPlugin, PluginRegistry, ErrorContext } from '../core/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Registry for bloc-specific plugins
|
|
5
|
+
*/
|
|
6
|
+
export class BlocPluginRegistry<TState, TEvent = never> implements PluginRegistry<BlocPlugin<TState, TEvent>> {
|
|
7
|
+
private plugins = new Map<string, BlocPlugin<TState, TEvent>>();
|
|
8
|
+
private executionOrder: string[] = [];
|
|
9
|
+
private attached = false;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Add a bloc plugin
|
|
13
|
+
*/
|
|
14
|
+
add(plugin: BlocPlugin<TState, TEvent>): void {
|
|
15
|
+
if (this.plugins.has(plugin.name)) {
|
|
16
|
+
throw new Error(`Plugin '${plugin.name}' is already registered`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Validate capabilities
|
|
20
|
+
if (plugin.capabilities) {
|
|
21
|
+
this.validateCapabilities(plugin);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.plugins.set(plugin.name, plugin);
|
|
25
|
+
this.executionOrder.push(plugin.name);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Remove a bloc plugin
|
|
30
|
+
*/
|
|
31
|
+
remove(pluginName: string): boolean {
|
|
32
|
+
const plugin = this.plugins.get(pluginName);
|
|
33
|
+
if (!plugin) return false;
|
|
34
|
+
|
|
35
|
+
// Call onDetach if attached
|
|
36
|
+
if (this.attached && plugin.onDetach) {
|
|
37
|
+
try {
|
|
38
|
+
plugin.onDetach();
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error(`Plugin '${pluginName}' error in onDetach:`, error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.plugins.delete(pluginName);
|
|
45
|
+
this.executionOrder = this.executionOrder.filter(name => name !== pluginName);
|
|
46
|
+
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get a plugin by name
|
|
52
|
+
*/
|
|
53
|
+
get(pluginName: string): BlocPlugin<TState, TEvent> | undefined {
|
|
54
|
+
return this.plugins.get(pluginName);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get all plugins in execution order
|
|
59
|
+
*/
|
|
60
|
+
getAll(): ReadonlyArray<BlocPlugin<TState, TEvent>> {
|
|
61
|
+
return this.executionOrder.map(name => this.plugins.get(name)!);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Clear all plugins
|
|
66
|
+
*/
|
|
67
|
+
clear(): void {
|
|
68
|
+
// Detach all plugins first
|
|
69
|
+
if (this.attached) {
|
|
70
|
+
for (const plugin of this.getAll()) {
|
|
71
|
+
if (plugin.onDetach) {
|
|
72
|
+
try {
|
|
73
|
+
plugin.onDetach();
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error(`Plugin '${plugin.name}' error in onDetach:`, error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.plugins.clear();
|
|
82
|
+
this.executionOrder = [];
|
|
83
|
+
this.attached = false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Attach all plugins to a bloc
|
|
88
|
+
*/
|
|
89
|
+
attach(bloc: any): void {
|
|
90
|
+
if (this.attached) {
|
|
91
|
+
throw new Error('Plugins are already attached');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const plugin of this.getAll()) {
|
|
95
|
+
if (plugin.onAttach) {
|
|
96
|
+
try {
|
|
97
|
+
plugin.onAttach(bloc);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error(`Plugin '${plugin.name}' error in onAttach:`, error);
|
|
100
|
+
// Remove failing plugin
|
|
101
|
+
this.remove(plugin.name);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this.attached = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Transform state through all plugins
|
|
111
|
+
*/
|
|
112
|
+
transformState(previousState: TState, nextState: TState): TState {
|
|
113
|
+
let transformedState = nextState;
|
|
114
|
+
|
|
115
|
+
for (const plugin of this.getAll()) {
|
|
116
|
+
if (plugin.transformState && this.canTransformState(plugin)) {
|
|
117
|
+
try {
|
|
118
|
+
transformedState = plugin.transformState(previousState, transformedState);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error(`Plugin '${plugin.name}' error in transformState:`, error);
|
|
121
|
+
// Continue with untransformed state
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return transformedState;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Transform event through all plugins
|
|
131
|
+
*/
|
|
132
|
+
transformEvent(event: TEvent): TEvent | null {
|
|
133
|
+
let transformedEvent: TEvent | null = event;
|
|
134
|
+
|
|
135
|
+
for (const plugin of this.getAll()) {
|
|
136
|
+
if (transformedEvent === null) break;
|
|
137
|
+
|
|
138
|
+
if (plugin.transformEvent && this.canInterceptEvents(plugin)) {
|
|
139
|
+
try {
|
|
140
|
+
transformedEvent = plugin.transformEvent(transformedEvent);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error(`Plugin '${plugin.name}' error in transformEvent:`, error);
|
|
143
|
+
// Continue with untransformed event
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return transformedEvent;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Notify plugins of state change
|
|
153
|
+
*/
|
|
154
|
+
notifyStateChange(previousState: TState, currentState: TState): void {
|
|
155
|
+
for (const plugin of this.getAll()) {
|
|
156
|
+
if (plugin.onStateChange && this.canReadState(plugin)) {
|
|
157
|
+
try {
|
|
158
|
+
plugin.onStateChange(previousState, currentState);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error(`Plugin '${plugin.name}' error in onStateChange:`, error);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Notify plugins of event
|
|
168
|
+
*/
|
|
169
|
+
notifyEvent(event: TEvent): void {
|
|
170
|
+
for (const plugin of this.getAll()) {
|
|
171
|
+
if (plugin.onEvent && this.canReadState(plugin)) {
|
|
172
|
+
try {
|
|
173
|
+
plugin.onEvent(event);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.error(`Plugin '${plugin.name}' error in onEvent:`, error);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Notify plugins of error
|
|
183
|
+
*/
|
|
184
|
+
notifyError(error: Error, context: ErrorContext): void {
|
|
185
|
+
for (const plugin of this.getAll()) {
|
|
186
|
+
if (plugin.onError) {
|
|
187
|
+
try {
|
|
188
|
+
plugin.onError(error, context);
|
|
189
|
+
} catch (hookError) {
|
|
190
|
+
console.error(`Plugin '${plugin.name}' error in onError:`, hookError);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private validateCapabilities(plugin: BlocPlugin<TState, TEvent>): void {
|
|
197
|
+
const caps = plugin.capabilities!;
|
|
198
|
+
|
|
199
|
+
// Validate logical constraints
|
|
200
|
+
if (caps.transformState && !caps.readState) {
|
|
201
|
+
throw new Error(`Plugin '${plugin.name}': transformState requires readState capability`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (caps.interceptEvents && !caps.readState) {
|
|
205
|
+
throw new Error(`Plugin '${plugin.name}': interceptEvents requires readState capability`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private canReadState(plugin: BlocPlugin<TState, TEvent>): boolean {
|
|
210
|
+
return !plugin.capabilities || plugin.capabilities.readState !== false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private canTransformState(plugin: BlocPlugin<TState, TEvent>): boolean {
|
|
214
|
+
return !plugin.capabilities || plugin.capabilities.transformState === true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private canInterceptEvents(plugin: BlocPlugin<TState, TEvent>): boolean {
|
|
218
|
+
return !plugin.capabilities || plugin.capabilities.interceptEvents === true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { BlacPlugin, PluginRegistry, PluginMetrics, ErrorContext } from '../core/types';
|
|
2
|
+
import { BlocBase } from '../BlocBase';
|
|
3
|
+
import { Bloc } from '../Bloc';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Registry for system-wide plugins
|
|
7
|
+
*/
|
|
8
|
+
export class SystemPluginRegistry implements PluginRegistry<BlacPlugin> {
|
|
9
|
+
private plugins = new Map<string, BlacPlugin>();
|
|
10
|
+
private metrics = new Map<string, Map<string, PluginMetrics>>();
|
|
11
|
+
private executionOrder: string[] = [];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Add a system plugin
|
|
15
|
+
*/
|
|
16
|
+
add(plugin: BlacPlugin): void {
|
|
17
|
+
if (this.plugins.has(plugin.name)) {
|
|
18
|
+
throw new Error(`Plugin '${plugin.name}' is already registered`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
this.plugins.set(plugin.name, plugin);
|
|
22
|
+
this.executionOrder.push(plugin.name);
|
|
23
|
+
this.initializeMetrics(plugin.name);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Remove a system plugin
|
|
28
|
+
*/
|
|
29
|
+
remove(pluginName: string): boolean {
|
|
30
|
+
const plugin = this.plugins.get(pluginName);
|
|
31
|
+
if (!plugin) return false;
|
|
32
|
+
|
|
33
|
+
this.plugins.delete(pluginName);
|
|
34
|
+
this.metrics.delete(pluginName);
|
|
35
|
+
this.executionOrder = this.executionOrder.filter(name => name !== pluginName);
|
|
36
|
+
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get a plugin by name
|
|
42
|
+
*/
|
|
43
|
+
get(pluginName: string): BlacPlugin | undefined {
|
|
44
|
+
return this.plugins.get(pluginName);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get all plugins in execution order
|
|
49
|
+
*/
|
|
50
|
+
getAll(): ReadonlyArray<BlacPlugin> {
|
|
51
|
+
return this.executionOrder.map(name => this.plugins.get(name)!);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Clear all plugins
|
|
56
|
+
*/
|
|
57
|
+
clear(): void {
|
|
58
|
+
this.plugins.clear();
|
|
59
|
+
this.metrics.clear();
|
|
60
|
+
this.executionOrder = [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Execute a hook on all plugins
|
|
65
|
+
*/
|
|
66
|
+
executeHook(
|
|
67
|
+
hookName: keyof BlacPlugin,
|
|
68
|
+
args: unknown[],
|
|
69
|
+
errorHandler?: (error: Error, plugin: BlacPlugin) => void
|
|
70
|
+
): void {
|
|
71
|
+
for (const pluginName of this.executionOrder) {
|
|
72
|
+
const plugin = this.plugins.get(pluginName)!;
|
|
73
|
+
const hook = plugin[hookName] as Function | undefined;
|
|
74
|
+
|
|
75
|
+
if (typeof hook !== 'function') continue;
|
|
76
|
+
|
|
77
|
+
const startTime = performance.now();
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
hook.apply(plugin, args);
|
|
81
|
+
this.recordSuccess(pluginName, hookName as string, startTime);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
this.recordError(pluginName, hookName as string, error as Error);
|
|
84
|
+
|
|
85
|
+
if (errorHandler) {
|
|
86
|
+
errorHandler(error as Error, plugin);
|
|
87
|
+
} else {
|
|
88
|
+
// Default: log and continue
|
|
89
|
+
console.error(`Plugin '${pluginName}' error in hook '${hookName as string}':`, error);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Bootstrap all plugins
|
|
97
|
+
*/
|
|
98
|
+
bootstrap(): void {
|
|
99
|
+
this.executeHook('beforeBootstrap', []);
|
|
100
|
+
this.executeHook('afterBootstrap', []);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Shutdown all plugins
|
|
105
|
+
*/
|
|
106
|
+
shutdown(): void {
|
|
107
|
+
this.executeHook('beforeShutdown', []);
|
|
108
|
+
this.executeHook('afterShutdown', []);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Notify plugins of bloc creation
|
|
113
|
+
*/
|
|
114
|
+
notifyBlocCreated(bloc: BlocBase<any>): void {
|
|
115
|
+
this.executeHook('onBlocCreated', [bloc]);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Notify plugins of bloc disposal
|
|
120
|
+
*/
|
|
121
|
+
notifyBlocDisposed(bloc: BlocBase<any>): void {
|
|
122
|
+
this.executeHook('onBlocDisposed', [bloc]);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Notify plugins of state change
|
|
127
|
+
*/
|
|
128
|
+
notifyStateChanged(bloc: BlocBase<any>, previousState: any, currentState: any): void {
|
|
129
|
+
this.executeHook('onStateChanged', [bloc, previousState, currentState]);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Notify plugins of event addition
|
|
134
|
+
*/
|
|
135
|
+
notifyEventAdded(bloc: Bloc<any, any>, event: any): void {
|
|
136
|
+
this.executeHook('onEventAdded', [bloc, event]);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Notify plugins of errors
|
|
141
|
+
*/
|
|
142
|
+
notifyError(error: Error, bloc: BlocBase<unknown>, context: ErrorContext): void {
|
|
143
|
+
this.executeHook('onError', [error, bloc, context], (hookError, plugin) => {
|
|
144
|
+
// Double fault protection - if error handler fails, just log
|
|
145
|
+
console.error(`Plugin '${plugin.name}' error handler failed:`, hookError);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get metrics for a plugin
|
|
151
|
+
*/
|
|
152
|
+
getMetrics(pluginName: string): Map<string, PluginMetrics> | undefined {
|
|
153
|
+
return this.metrics.get(pluginName);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private initializeMetrics(pluginName: string): void {
|
|
157
|
+
this.metrics.set(pluginName, new Map());
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private recordSuccess(pluginName: string, hookName: string, startTime: number): void {
|
|
161
|
+
const pluginMetrics = this.metrics.get(pluginName)!;
|
|
162
|
+
const hookMetrics = pluginMetrics.get(hookName) || {
|
|
163
|
+
executionTime: 0,
|
|
164
|
+
executionCount: 0,
|
|
165
|
+
errorCount: 0
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const executionTime = performance.now() - startTime;
|
|
169
|
+
|
|
170
|
+
pluginMetrics.set(hookName, {
|
|
171
|
+
...hookMetrics,
|
|
172
|
+
executionTime: hookMetrics.executionTime + executionTime,
|
|
173
|
+
executionCount: hookMetrics.executionCount + 1,
|
|
174
|
+
lastExecutionTime: executionTime
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private recordError(pluginName: string, hookName: string, error: Error): void {
|
|
179
|
+
const pluginMetrics = this.metrics.get(pluginName)!;
|
|
180
|
+
const hookMetrics = pluginMetrics.get(hookName) || {
|
|
181
|
+
executionTime: 0,
|
|
182
|
+
executionCount: 0,
|
|
183
|
+
errorCount: 0
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
pluginMetrics.set(hookName, {
|
|
187
|
+
...hookMetrics,
|
|
188
|
+
errorCount: hookMetrics.errorCount + 1,
|
|
189
|
+
lastError: error
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { BlocBase } from '../BlocBase';
|
|
2
|
+
import { Bloc } from '../Bloc';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Error context provided to error handlers
|
|
6
|
+
*/
|
|
7
|
+
export interface ErrorContext {
|
|
8
|
+
readonly phase: 'initialization' | 'state-change' | 'event-processing' | 'disposal';
|
|
9
|
+
readonly operation: string;
|
|
10
|
+
readonly metadata?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Plugin capabilities for security model
|
|
15
|
+
*/
|
|
16
|
+
export interface PluginCapabilities {
|
|
17
|
+
readonly readState: boolean;
|
|
18
|
+
readonly transformState: boolean;
|
|
19
|
+
readonly interceptEvents: boolean;
|
|
20
|
+
readonly persistData: boolean;
|
|
21
|
+
readonly accessMetadata: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Base interface for all plugins
|
|
26
|
+
*/
|
|
27
|
+
export interface Plugin {
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly version: string;
|
|
30
|
+
readonly capabilities?: PluginCapabilities;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* System-wide plugin that observes all blocs
|
|
35
|
+
*/
|
|
36
|
+
export interface BlacPlugin extends Plugin {
|
|
37
|
+
// Lifecycle hooks - all synchronous
|
|
38
|
+
beforeBootstrap?(): void;
|
|
39
|
+
afterBootstrap?(): void;
|
|
40
|
+
beforeShutdown?(): void;
|
|
41
|
+
afterShutdown?(): void;
|
|
42
|
+
|
|
43
|
+
// System-wide observations
|
|
44
|
+
onBlocCreated?(bloc: BlocBase<any>): void;
|
|
45
|
+
onBlocDisposed?(bloc: BlocBase<any>): void;
|
|
46
|
+
onStateChanged?(bloc: BlocBase<any>, previousState: any, currentState: any): void;
|
|
47
|
+
onEventAdded?(bloc: Bloc<any, any>, event: any): void;
|
|
48
|
+
onError?(error: Error, bloc: BlocBase<unknown>, context: ErrorContext): void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Bloc-specific plugin that can transform behavior
|
|
53
|
+
*/
|
|
54
|
+
export interface BlocPlugin<TState = any, TEvent = never> extends Plugin {
|
|
55
|
+
// Transform hooks - can modify data
|
|
56
|
+
transformState?(previousState: TState, nextState: TState): TState;
|
|
57
|
+
transformEvent?(event: TEvent): TEvent | null;
|
|
58
|
+
|
|
59
|
+
// Lifecycle hooks
|
|
60
|
+
onAttach?(bloc: BlocBase<TState, any>): void;
|
|
61
|
+
onDetach?(): void;
|
|
62
|
+
|
|
63
|
+
// Observation hooks
|
|
64
|
+
onStateChange?(previousState: TState, currentState: TState): void;
|
|
65
|
+
onEvent?(event: TEvent): void;
|
|
66
|
+
onError?(error: Error, context: ErrorContext): void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Plugin execution context for performance monitoring
|
|
71
|
+
*/
|
|
72
|
+
export interface PluginExecutionContext {
|
|
73
|
+
readonly pluginName: string;
|
|
74
|
+
readonly hookName: string;
|
|
75
|
+
readonly startTime: number;
|
|
76
|
+
readonly blocName?: string;
|
|
77
|
+
readonly blocId?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Plugin metrics for monitoring
|
|
82
|
+
*/
|
|
83
|
+
export interface PluginMetrics {
|
|
84
|
+
readonly executionTime: number;
|
|
85
|
+
readonly executionCount: number;
|
|
86
|
+
readonly errorCount: number;
|
|
87
|
+
readonly lastError?: Error;
|
|
88
|
+
readonly lastExecutionTime?: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Plugin registry interface
|
|
93
|
+
*/
|
|
94
|
+
export interface PluginRegistry<T extends Plugin> {
|
|
95
|
+
add(plugin: T): void;
|
|
96
|
+
remove(pluginName: string): boolean;
|
|
97
|
+
get(pluginName: string): T | undefined;
|
|
98
|
+
getAll(): ReadonlyArray<T>;
|
|
99
|
+
clear(): void;
|
|
100
|
+
}
|