@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.
Files changed (55) hide show
  1. package/dist/index.cjs.js +2 -2
  2. package/dist/index.cjs.js.map +1 -1
  3. package/dist/index.d.ts +739 -260
  4. package/dist/index.es.js +1924 -1237
  5. package/dist/index.es.js.map +1 -1
  6. package/package.json +6 -7
  7. package/src/Blac.ts +82 -69
  8. package/src/Bloc.ts +45 -3
  9. package/src/BlocBase.ts +263 -454
  10. package/src/Cubit.ts +1 -16
  11. package/src/__tests__/Blac.config.test.ts +10 -17
  12. package/src/__tests__/Bloc.event.test.ts +27 -52
  13. package/src/__tests__/BlocBase.subscription.test.ts +349 -0
  14. package/src/__tests__/Cubit.test.ts +39 -50
  15. package/src/__tests__/memory-leaks.test.ts +383 -0
  16. package/src/__tests__/plugins.test.ts +464 -0
  17. package/src/adapter/BlacAdapter.ts +347 -207
  18. package/src/adapter/ProxyFactory.ts +196 -255
  19. package/src/adapter/__tests__/BlacAdapter.test.ts +71 -77
  20. package/src/adapter/__tests__/ProxyFactory.test.ts +1 -1
  21. package/src/adapter/index.ts +0 -1
  22. package/src/errors/BlacError.ts +69 -0
  23. package/src/errors/ErrorManager.ts +101 -0
  24. package/src/errors/handleError.ts +114 -0
  25. package/src/errors/index.ts +13 -0
  26. package/src/events.ts +7 -1
  27. package/src/index.ts +10 -1
  28. package/src/lifecycle/BlocLifecycle.ts +156 -0
  29. package/src/lifecycle/index.ts +1 -0
  30. package/src/plugins/BlocPluginRegistry.ts +240 -0
  31. package/src/plugins/SystemPluginRegistry.ts +242 -0
  32. package/src/plugins/__tests__/BlocPluginRegistry.test.ts +450 -0
  33. package/src/plugins/__tests__/SystemPluginRegistry.test.ts +404 -0
  34. package/src/plugins/index.ts +8 -0
  35. package/src/plugins/types.ts +129 -0
  36. package/src/subscription/README.md +132 -0
  37. package/src/subscription/SubscriptionManager.ts +395 -0
  38. package/src/subscription/__tests__/SubscriptionManager.test.ts +355 -0
  39. package/src/subscription/index.ts +2 -0
  40. package/src/subscription/types.ts +90 -0
  41. package/src/testing.ts +31 -37
  42. package/src/types.ts +4 -21
  43. package/src/utils/BatchingManager.ts +59 -0
  44. package/src/utils/RerenderLogger.ts +265 -0
  45. package/src/utils/__tests__/generateInstanceId.test.ts +104 -0
  46. package/src/utils/__tests__/shallowEqual.test.ts +171 -0
  47. package/src/utils/__tests__/uuid.test.ts +144 -0
  48. package/src/utils/generateInstanceId.ts +54 -0
  49. package/src/utils/shallowEqual.ts +1 -1
  50. package/README.md +0 -329
  51. package/src/BlacObserver.ts +0 -186
  52. package/src/__tests__/BlocBase.lifecycle.test.ts +0 -441
  53. package/src/__tests__/props.test.ts +0 -256
  54. package/src/adapter/ConsumerTracker.ts +0 -241
  55. 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
+ }