@blac/core 2.0.0-rc-11 → 2.0.0-rc-14
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 +739 -260
- package/dist/index.es.js +1924 -1237
- package/dist/index.es.js.map +1 -1
- package/package.json +6 -7
- package/src/Blac.ts +82 -69
- package/src/Bloc.ts +45 -3
- package/src/BlocBase.ts +263 -454
- 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 +464 -0
- 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 +10 -1
- package/src/lifecycle/BlocLifecycle.ts +156 -0
- package/src/lifecycle/index.ts +1 -0
- package/src/plugins/BlocPluginRegistry.ts +240 -0
- package/src/plugins/SystemPluginRegistry.ts +242 -0
- package/src/plugins/__tests__/BlocPluginRegistry.test.ts +450 -0
- package/src/plugins/__tests__/SystemPluginRegistry.test.ts +404 -0
- package/src/plugins/index.ts +8 -0
- package/src/plugins/types.ts +129 -0
- 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/src/BlacObserver.ts +0 -186
- 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
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enum representing the lifecycle states of a Bloc instance
|
|
3
|
+
*/
|
|
4
|
+
export enum BlocLifecycleState {
|
|
5
|
+
ACTIVE = 'active',
|
|
6
|
+
DISPOSAL_REQUESTED = 'disposal_requested',
|
|
7
|
+
DISPOSING = 'disposing',
|
|
8
|
+
DISPOSED = 'disposed',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface StateTransitionResult {
|
|
12
|
+
success: boolean;
|
|
13
|
+
currentState: BlocLifecycleState;
|
|
14
|
+
previousState: BlocLifecycleState;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Manages the lifecycle state transitions for Bloc instances.
|
|
19
|
+
* Ensures atomic state transitions to prevent race conditions during disposal.
|
|
20
|
+
*/
|
|
21
|
+
export class BlocLifecycleManager {
|
|
22
|
+
private disposalState = BlocLifecycleState.ACTIVE;
|
|
23
|
+
private disposalLock = false;
|
|
24
|
+
private disposalTimer?: NodeJS.Timeout | number;
|
|
25
|
+
private disposalHandler?: (bloc: unknown) => void;
|
|
26
|
+
|
|
27
|
+
get currentState(): BlocLifecycleState {
|
|
28
|
+
return this.disposalState;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get isDisposed(): boolean {
|
|
32
|
+
return this.disposalState === BlocLifecycleState.DISPOSED;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Atomic state transition for disposal
|
|
37
|
+
*/
|
|
38
|
+
atomicStateTransition(
|
|
39
|
+
expectedState: BlocLifecycleState,
|
|
40
|
+
newState: BlocLifecycleState,
|
|
41
|
+
): StateTransitionResult {
|
|
42
|
+
if (this.disposalLock) {
|
|
43
|
+
return {
|
|
44
|
+
success: false,
|
|
45
|
+
currentState: this.disposalState,
|
|
46
|
+
previousState: this.disposalState,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.disposalLock = true;
|
|
51
|
+
try {
|
|
52
|
+
if (this.disposalState !== expectedState) {
|
|
53
|
+
return {
|
|
54
|
+
success: false,
|
|
55
|
+
currentState: this.disposalState,
|
|
56
|
+
previousState: this.disposalState,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const previousState = this.disposalState;
|
|
61
|
+
this.disposalState = newState;
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
success: true,
|
|
65
|
+
currentState: newState,
|
|
66
|
+
previousState,
|
|
67
|
+
};
|
|
68
|
+
} finally {
|
|
69
|
+
this.disposalLock = false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Schedule disposal after a delay
|
|
75
|
+
*/
|
|
76
|
+
scheduleDisposal(
|
|
77
|
+
delay: number,
|
|
78
|
+
canDispose: () => boolean,
|
|
79
|
+
onDispose: () => void,
|
|
80
|
+
): void {
|
|
81
|
+
// Cancel any existing disposal timer
|
|
82
|
+
if (this.disposalTimer) {
|
|
83
|
+
clearTimeout(this.disposalTimer as NodeJS.Timeout);
|
|
84
|
+
this.disposalTimer = undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const transitionResult = this.atomicStateTransition(
|
|
88
|
+
BlocLifecycleState.ACTIVE,
|
|
89
|
+
BlocLifecycleState.DISPOSAL_REQUESTED,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
if (!transitionResult.success) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.disposalTimer = setTimeout(() => {
|
|
97
|
+
if (
|
|
98
|
+
canDispose() &&
|
|
99
|
+
this.disposalState === BlocLifecycleState.DISPOSAL_REQUESTED
|
|
100
|
+
) {
|
|
101
|
+
onDispose();
|
|
102
|
+
} else {
|
|
103
|
+
this.atomicStateTransition(
|
|
104
|
+
BlocLifecycleState.DISPOSAL_REQUESTED,
|
|
105
|
+
BlocLifecycleState.ACTIVE,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}, delay);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Cancel disposal if in disposal requested state
|
|
113
|
+
*/
|
|
114
|
+
cancelDisposal(): boolean {
|
|
115
|
+
if (this.disposalState === BlocLifecycleState.DISPOSAL_REQUESTED) {
|
|
116
|
+
// Cancel disposal timer
|
|
117
|
+
if (this.disposalTimer) {
|
|
118
|
+
clearTimeout(this.disposalTimer as NodeJS.Timeout);
|
|
119
|
+
this.disposalTimer = undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Transition back to active state
|
|
123
|
+
const result = this.atomicStateTransition(
|
|
124
|
+
BlocLifecycleState.DISPOSAL_REQUESTED,
|
|
125
|
+
BlocLifecycleState.ACTIVE,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return result.success;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Set disposal handler
|
|
135
|
+
*/
|
|
136
|
+
setDisposalHandler(handler: (bloc: unknown) => void): void {
|
|
137
|
+
this.disposalHandler = handler;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get disposal handler
|
|
142
|
+
*/
|
|
143
|
+
getDisposalHandler(): ((bloc: unknown) => void) | undefined {
|
|
144
|
+
return this.disposalHandler;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Clear disposal timer
|
|
149
|
+
*/
|
|
150
|
+
clearDisposalTimer(): void {
|
|
151
|
+
if (this.disposalTimer) {
|
|
152
|
+
clearTimeout(this.disposalTimer as NodeJS.Timeout);
|
|
153
|
+
this.disposalTimer = undefined;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './BlocLifecycle';
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { BlocPlugin, PluginRegistry, ErrorContext } from './types';
|
|
2
|
+
import { handleError } from '../errors/handleError';
|
|
3
|
+
import { ErrorCategory } from '../errors/BlacError';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Registry for bloc-specific plugins
|
|
7
|
+
*/
|
|
8
|
+
export class BlocPluginRegistry<TState, TEvent = never>
|
|
9
|
+
implements PluginRegistry<BlocPlugin<TState, TEvent>>
|
|
10
|
+
{
|
|
11
|
+
private plugins = new Map<string, BlocPlugin<TState, TEvent>>();
|
|
12
|
+
private executionOrder: string[] = [];
|
|
13
|
+
private attached = false;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Add a bloc plugin
|
|
17
|
+
*/
|
|
18
|
+
add(plugin: BlocPlugin<TState, TEvent>): void {
|
|
19
|
+
if (this.plugins.has(plugin.name)) {
|
|
20
|
+
throw new Error(`Plugin '${plugin.name}' is already registered`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Validate capabilities
|
|
24
|
+
if (plugin.capabilities) {
|
|
25
|
+
this.validateCapabilities(plugin);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
this.plugins.set(plugin.name, plugin);
|
|
29
|
+
this.executionOrder.push(plugin.name);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Remove a bloc plugin
|
|
34
|
+
*/
|
|
35
|
+
remove(pluginName: string): boolean {
|
|
36
|
+
const plugin = this.plugins.get(pluginName);
|
|
37
|
+
if (!plugin) return false;
|
|
38
|
+
|
|
39
|
+
// Call onDetach if attached
|
|
40
|
+
if (this.attached && plugin.onDetach) {
|
|
41
|
+
handleError.wrap(() => plugin.onDetach!(), ErrorCategory.PLUGIN, {
|
|
42
|
+
pluginName,
|
|
43
|
+
operation: 'onDetach',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.plugins.delete(pluginName);
|
|
48
|
+
this.executionOrder = this.executionOrder.filter(
|
|
49
|
+
(name) => name !== pluginName,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get a plugin by name
|
|
57
|
+
*/
|
|
58
|
+
get(pluginName: string): BlocPlugin<TState, TEvent> | undefined {
|
|
59
|
+
return this.plugins.get(pluginName);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get all plugins in execution order
|
|
64
|
+
*/
|
|
65
|
+
getAll(): ReadonlyArray<BlocPlugin<TState, TEvent>> {
|
|
66
|
+
return this.executionOrder.map((name) => this.plugins.get(name)!);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Clear all plugins
|
|
71
|
+
*/
|
|
72
|
+
clear(): void {
|
|
73
|
+
// Detach all plugins first
|
|
74
|
+
if (this.attached) {
|
|
75
|
+
for (const plugin of this.getAll()) {
|
|
76
|
+
if (plugin.onDetach) {
|
|
77
|
+
handleError.wrap(() => plugin.onDetach!(), ErrorCategory.PLUGIN, {
|
|
78
|
+
pluginName: plugin.name,
|
|
79
|
+
operation: 'onDetach',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.plugins.clear();
|
|
86
|
+
this.executionOrder = [];
|
|
87
|
+
this.attached = false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Attach all plugins to a bloc
|
|
92
|
+
*/
|
|
93
|
+
attach(bloc: any): void {
|
|
94
|
+
if (this.attached) {
|
|
95
|
+
throw new Error('Plugins are already attached');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const plugin of this.getAll()) {
|
|
99
|
+
if (plugin.onAttach) {
|
|
100
|
+
try {
|
|
101
|
+
plugin.onAttach(bloc);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error(`Plugin '${plugin.name}' error in onAttach:`, error);
|
|
104
|
+
// Remove failing plugin
|
|
105
|
+
this.remove(plugin.name);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this.attached = true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Transform state through all plugins
|
|
115
|
+
*/
|
|
116
|
+
transformState(previousState: TState, nextState: TState): TState {
|
|
117
|
+
let transformedState = nextState;
|
|
118
|
+
|
|
119
|
+
for (const plugin of this.getAll()) {
|
|
120
|
+
if (plugin.transformState && this.canTransformState(plugin)) {
|
|
121
|
+
try {
|
|
122
|
+
transformedState = plugin.transformState(
|
|
123
|
+
previousState,
|
|
124
|
+
transformedState,
|
|
125
|
+
);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error(
|
|
128
|
+
`Plugin '${plugin.name}' error in transformState:`,
|
|
129
|
+
error,
|
|
130
|
+
);
|
|
131
|
+
// Continue with untransformed state
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return transformedState;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Transform event through all plugins
|
|
141
|
+
*/
|
|
142
|
+
transformEvent(event: TEvent): TEvent | null {
|
|
143
|
+
let transformedEvent: TEvent | null = event;
|
|
144
|
+
|
|
145
|
+
for (const plugin of this.getAll()) {
|
|
146
|
+
if (transformedEvent === null) break;
|
|
147
|
+
|
|
148
|
+
if (plugin.transformEvent && this.canInterceptEvents(plugin)) {
|
|
149
|
+
try {
|
|
150
|
+
transformedEvent = plugin.transformEvent(transformedEvent);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error(
|
|
153
|
+
`Plugin '${plugin.name}' error in transformEvent:`,
|
|
154
|
+
error,
|
|
155
|
+
);
|
|
156
|
+
// Continue with untransformed event
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return transformedEvent;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Notify plugins of state change
|
|
166
|
+
*/
|
|
167
|
+
notifyStateChange(previousState: TState, currentState: TState): void {
|
|
168
|
+
for (const plugin of this.getAll()) {
|
|
169
|
+
if (plugin.onStateChange && this.canReadState(plugin)) {
|
|
170
|
+
try {
|
|
171
|
+
plugin.onStateChange(previousState, currentState);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error(
|
|
174
|
+
`Plugin '${plugin.name}' error in onStateChange:`,
|
|
175
|
+
error,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Notify plugins of event
|
|
184
|
+
*/
|
|
185
|
+
notifyEvent(event: TEvent): void {
|
|
186
|
+
for (const plugin of this.getAll()) {
|
|
187
|
+
if (plugin.onEvent && this.canReadState(plugin)) {
|
|
188
|
+
try {
|
|
189
|
+
plugin.onEvent(event);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.error(`Plugin '${plugin.name}' error in onEvent:`, error);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Notify plugins of error
|
|
199
|
+
*/
|
|
200
|
+
notifyError(error: Error, context: ErrorContext): void {
|
|
201
|
+
for (const plugin of this.getAll()) {
|
|
202
|
+
if (plugin.onError) {
|
|
203
|
+
try {
|
|
204
|
+
plugin.onError(error, context);
|
|
205
|
+
} catch (hookError) {
|
|
206
|
+
console.error(`Plugin '${plugin.name}' error in onError:`, hookError);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private validateCapabilities(plugin: BlocPlugin<TState, TEvent>): void {
|
|
213
|
+
const caps = plugin.capabilities!;
|
|
214
|
+
|
|
215
|
+
// Validate logical constraints
|
|
216
|
+
if (caps.transformState && !caps.readState) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`Plugin '${plugin.name}': transformState requires readState capability`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (caps.interceptEvents && !caps.readState) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
`Plugin '${plugin.name}': interceptEvents requires readState capability`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private canReadState(plugin: BlocPlugin<TState, TEvent>): boolean {
|
|
230
|
+
return !plugin.capabilities || plugin.capabilities.readState !== false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private canTransformState(plugin: BlocPlugin<TState, TEvent>): boolean {
|
|
234
|
+
return !plugin.capabilities || plugin.capabilities.transformState === true;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private canInterceptEvents(plugin: BlocPlugin<TState, TEvent>): boolean {
|
|
238
|
+
return !plugin.capabilities || plugin.capabilities.interceptEvents === true;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BlacPlugin,
|
|
3
|
+
PluginRegistry,
|
|
4
|
+
PluginMetrics,
|
|
5
|
+
ErrorContext,
|
|
6
|
+
AdapterMetadata,
|
|
7
|
+
} from './types';
|
|
8
|
+
import { BlocBase } from '../BlocBase';
|
|
9
|
+
import { Bloc } from '../Bloc';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Registry for system-wide plugins
|
|
13
|
+
*/
|
|
14
|
+
export class SystemPluginRegistry implements PluginRegistry<BlacPlugin> {
|
|
15
|
+
private plugins = new Map<string, BlacPlugin>();
|
|
16
|
+
private metrics = new Map<string, Map<string, PluginMetrics>>();
|
|
17
|
+
private executionOrder: string[] = [];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Add a system plugin
|
|
21
|
+
*/
|
|
22
|
+
add(plugin: BlacPlugin): void {
|
|
23
|
+
if (this.plugins.has(plugin.name)) {
|
|
24
|
+
throw new Error(`Plugin '${plugin.name}' is already registered`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
this.plugins.set(plugin.name, plugin);
|
|
28
|
+
this.executionOrder.push(plugin.name);
|
|
29
|
+
this.initializeMetrics(plugin.name);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Remove a system plugin
|
|
34
|
+
*/
|
|
35
|
+
remove(pluginName: string): boolean {
|
|
36
|
+
const plugin = this.plugins.get(pluginName);
|
|
37
|
+
if (!plugin) return false;
|
|
38
|
+
|
|
39
|
+
this.plugins.delete(pluginName);
|
|
40
|
+
this.metrics.delete(pluginName);
|
|
41
|
+
this.executionOrder = this.executionOrder.filter(
|
|
42
|
+
(name) => name !== pluginName,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get a plugin by name
|
|
50
|
+
*/
|
|
51
|
+
get(pluginName: string): BlacPlugin | undefined {
|
|
52
|
+
return this.plugins.get(pluginName);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get all plugins in execution order
|
|
57
|
+
*/
|
|
58
|
+
getAll(): ReadonlyArray<BlacPlugin> {
|
|
59
|
+
return this.executionOrder.map((name) => this.plugins.get(name)!);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Clear all plugins
|
|
64
|
+
*/
|
|
65
|
+
clear(): void {
|
|
66
|
+
this.plugins.clear();
|
|
67
|
+
this.metrics.clear();
|
|
68
|
+
this.executionOrder = [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Execute a hook on all plugins
|
|
73
|
+
*/
|
|
74
|
+
executeHook(
|
|
75
|
+
hookName: keyof BlacPlugin,
|
|
76
|
+
args: unknown[],
|
|
77
|
+
errorHandler?: (error: Error, plugin: BlacPlugin) => void,
|
|
78
|
+
): void {
|
|
79
|
+
for (const pluginName of this.executionOrder) {
|
|
80
|
+
const plugin = this.plugins.get(pluginName)!;
|
|
81
|
+
const hook = plugin[hookName] as Function | undefined;
|
|
82
|
+
|
|
83
|
+
if (typeof hook !== 'function') continue;
|
|
84
|
+
|
|
85
|
+
const startTime = performance.now();
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
hook.apply(plugin, args);
|
|
89
|
+
this.recordSuccess(pluginName, hookName as string, startTime);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
this.recordError(pluginName, hookName as string, error as Error);
|
|
92
|
+
|
|
93
|
+
if (errorHandler) {
|
|
94
|
+
errorHandler(error as Error, plugin);
|
|
95
|
+
} else {
|
|
96
|
+
// Default: log and continue
|
|
97
|
+
console.error(
|
|
98
|
+
`Plugin '${pluginName}' error in hook '${hookName as string}':`,
|
|
99
|
+
error,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Bootstrap all plugins
|
|
108
|
+
*/
|
|
109
|
+
bootstrap(): void {
|
|
110
|
+
this.executeHook('beforeBootstrap', []);
|
|
111
|
+
this.executeHook('afterBootstrap', []);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Shutdown all plugins
|
|
116
|
+
*/
|
|
117
|
+
shutdown(): void {
|
|
118
|
+
this.executeHook('beforeShutdown', []);
|
|
119
|
+
this.executeHook('afterShutdown', []);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Notify plugins of bloc creation
|
|
124
|
+
*/
|
|
125
|
+
notifyBlocCreated(bloc: BlocBase<any>): void {
|
|
126
|
+
this.executeHook('onBlocCreated', [bloc]);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Notify plugins of bloc disposal
|
|
131
|
+
*/
|
|
132
|
+
notifyBlocDisposed(bloc: BlocBase<any>): void {
|
|
133
|
+
this.executeHook('onBlocDisposed', [bloc]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Notify plugins of state change
|
|
138
|
+
*/
|
|
139
|
+
notifyStateChanged(
|
|
140
|
+
bloc: BlocBase<any>,
|
|
141
|
+
previousState: any,
|
|
142
|
+
currentState: any,
|
|
143
|
+
): void {
|
|
144
|
+
this.executeHook('onStateChanged', [bloc, previousState, currentState]);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Notify plugins of event addition
|
|
149
|
+
*/
|
|
150
|
+
notifyEventAdded(bloc: Bloc<any, any>, event: any): void {
|
|
151
|
+
this.executeHook('onEventAdded', [bloc, event]);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Notify plugins of errors
|
|
156
|
+
*/
|
|
157
|
+
notifyError(
|
|
158
|
+
error: Error,
|
|
159
|
+
bloc: BlocBase<unknown>,
|
|
160
|
+
context: ErrorContext,
|
|
161
|
+
): void {
|
|
162
|
+
this.executeHook('onError', [error, bloc, context], (hookError, plugin) => {
|
|
163
|
+
// Double fault protection - if error handler fails, just log
|
|
164
|
+
console.error(`Plugin '${plugin.name}' error handler failed:`, hookError);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Notify plugins of adapter lifecycle events
|
|
170
|
+
*/
|
|
171
|
+
notifyAdapterCreated(adapter: any, metadata: AdapterMetadata): void {
|
|
172
|
+
this.executeHook('onAdapterCreated', [adapter, metadata]);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
notifyAdapterMount(adapter: any, metadata: AdapterMetadata): void {
|
|
176
|
+
this.executeHook('onAdapterMount', [adapter, metadata]);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
notifyAdapterUnmount(adapter: any, metadata: AdapterMetadata): void {
|
|
180
|
+
this.executeHook('onAdapterUnmount', [adapter, metadata]);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
notifyAdapterRender(adapter: any, metadata: AdapterMetadata): void {
|
|
184
|
+
this.executeHook('onAdapterRender', [adapter, metadata]);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
notifyAdapterDisposed(adapter: any, metadata: AdapterMetadata): void {
|
|
188
|
+
this.executeHook('onAdapterDisposed', [adapter, metadata]);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get metrics for a plugin
|
|
193
|
+
*/
|
|
194
|
+
getMetrics(pluginName: string): Map<string, PluginMetrics> | undefined {
|
|
195
|
+
return this.metrics.get(pluginName);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private initializeMetrics(pluginName: string): void {
|
|
199
|
+
this.metrics.set(pluginName, new Map());
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private recordSuccess(
|
|
203
|
+
pluginName: string,
|
|
204
|
+
hookName: string,
|
|
205
|
+
startTime: number,
|
|
206
|
+
): void {
|
|
207
|
+
const pluginMetrics = this.metrics.get(pluginName)!;
|
|
208
|
+
const hookMetrics = pluginMetrics.get(hookName) || {
|
|
209
|
+
executionTime: 0,
|
|
210
|
+
executionCount: 0,
|
|
211
|
+
errorCount: 0,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const executionTime = performance.now() - startTime;
|
|
215
|
+
|
|
216
|
+
pluginMetrics.set(hookName, {
|
|
217
|
+
...hookMetrics,
|
|
218
|
+
executionTime: hookMetrics.executionTime + executionTime,
|
|
219
|
+
executionCount: hookMetrics.executionCount + 1,
|
|
220
|
+
lastExecutionTime: executionTime,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private recordError(
|
|
225
|
+
pluginName: string,
|
|
226
|
+
hookName: string,
|
|
227
|
+
error: Error,
|
|
228
|
+
): void {
|
|
229
|
+
const pluginMetrics = this.metrics.get(pluginName)!;
|
|
230
|
+
const hookMetrics = pluginMetrics.get(hookName) || {
|
|
231
|
+
executionTime: 0,
|
|
232
|
+
executionCount: 0,
|
|
233
|
+
errorCount: 0,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
pluginMetrics.set(hookName, {
|
|
237
|
+
...hookMetrics,
|
|
238
|
+
errorCount: hookMetrics.errorCount + 1,
|
|
239
|
+
lastError: error,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|