@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.
Files changed (54) hide show
  1. package/package.json +10 -15
  2. package/src/Blac.ts +38 -71
  3. package/src/Bloc.ts +8 -9
  4. package/src/BlocBase.ts +246 -540
  5. package/src/Cubit.ts +1 -16
  6. package/src/__tests__/Blac.config.test.ts +10 -17
  7. package/src/__tests__/Bloc.event.test.ts +27 -52
  8. package/src/__tests__/BlocBase.subscription.test.ts +349 -0
  9. package/src/__tests__/Cubit.test.ts +39 -50
  10. package/src/__tests__/memory-leaks.test.ts +383 -0
  11. package/src/__tests__/plugins.test.ts +107 -97
  12. package/src/adapter/BlacAdapter.ts +347 -207
  13. package/src/adapter/ProxyFactory.ts +196 -255
  14. package/src/adapter/__tests__/BlacAdapter.test.ts +71 -77
  15. package/src/adapter/__tests__/ProxyFactory.test.ts +1 -1
  16. package/src/adapter/index.ts +0 -1
  17. package/src/errors/BlacError.ts +69 -0
  18. package/src/errors/ErrorManager.ts +101 -0
  19. package/src/errors/handleError.ts +114 -0
  20. package/src/errors/index.ts +13 -0
  21. package/src/events.ts +7 -1
  22. package/src/index.ts +7 -1
  23. package/src/lifecycle/BlocLifecycle.ts +156 -0
  24. package/src/lifecycle/index.ts +1 -0
  25. package/src/plugins/BlocPluginRegistry.ts +71 -51
  26. package/src/plugins/SystemPluginRegistry.ts +63 -13
  27. package/src/plugins/__tests__/BlocPluginRegistry.test.ts +450 -0
  28. package/src/plugins/__tests__/SystemPluginRegistry.test.ts +404 -0
  29. package/src/plugins/types.ts +32 -3
  30. package/src/subscription/README.md +132 -0
  31. package/src/subscription/SubscriptionManager.ts +395 -0
  32. package/src/subscription/__tests__/SubscriptionManager.test.ts +355 -0
  33. package/src/subscription/index.ts +2 -0
  34. package/src/subscription/types.ts +90 -0
  35. package/src/testing.ts +31 -37
  36. package/src/types.ts +4 -21
  37. package/src/utils/BatchingManager.ts +59 -0
  38. package/src/utils/RerenderLogger.ts +265 -0
  39. package/src/utils/__tests__/generateInstanceId.test.ts +104 -0
  40. package/src/utils/__tests__/shallowEqual.test.ts +171 -0
  41. package/src/utils/__tests__/uuid.test.ts +144 -0
  42. package/src/utils/generateInstanceId.ts +54 -0
  43. package/src/utils/shallowEqual.ts +1 -1
  44. package/README.md +0 -329
  45. package/dist/index.cjs.js +0 -11
  46. package/dist/index.cjs.js.map +0 -1
  47. package/dist/index.d.ts +0 -1078
  48. package/dist/index.es.js +0 -1992
  49. package/dist/index.es.js.map +0 -1
  50. package/src/BlacObserver.ts +0 -189
  51. package/src/__tests__/BlocBase.lifecycle.test.ts +0 -441
  52. package/src/__tests__/props.test.ts +0 -256
  53. package/src/adapter/ConsumerTracker.ts +0 -241
  54. 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';
@@ -1,13 +1,17 @@
1
- import { BlocPlugin, PluginRegistry, ErrorContext } from '../core/types';
1
+ import { BlocPlugin, PluginRegistry, ErrorContext } from './types';
2
+ import { handleError } from '../errors/handleError';
3
+ import { ErrorCategory } from '../errors/BlacError';
2
4
 
3
5
  /**
4
6
  * Registry for bloc-specific plugins
5
7
  */
6
- export class BlocPluginRegistry<TState, TEvent = never> implements PluginRegistry<BlocPlugin<TState, TEvent>> {
8
+ export class BlocPluginRegistry<TState, TEvent = never>
9
+ implements PluginRegistry<BlocPlugin<TState, TEvent>>
10
+ {
7
11
  private plugins = new Map<string, BlocPlugin<TState, TEvent>>();
8
12
  private executionOrder: string[] = [];
9
13
  private attached = false;
10
-
14
+
11
15
  /**
12
16
  * Add a bloc plugin
13
17
  */
@@ -15,52 +19,53 @@ export class BlocPluginRegistry<TState, TEvent = never> implements PluginRegistr
15
19
  if (this.plugins.has(plugin.name)) {
16
20
  throw new Error(`Plugin '${plugin.name}' is already registered`);
17
21
  }
18
-
22
+
19
23
  // Validate capabilities
20
24
  if (plugin.capabilities) {
21
25
  this.validateCapabilities(plugin);
22
26
  }
23
-
27
+
24
28
  this.plugins.set(plugin.name, plugin);
25
29
  this.executionOrder.push(plugin.name);
26
30
  }
27
-
31
+
28
32
  /**
29
33
  * Remove a bloc plugin
30
34
  */
31
35
  remove(pluginName: string): boolean {
32
36
  const plugin = this.plugins.get(pluginName);
33
37
  if (!plugin) return false;
34
-
38
+
35
39
  // Call onDetach if attached
36
40
  if (this.attached && plugin.onDetach) {
37
- try {
38
- plugin.onDetach();
39
- } catch (error) {
40
- console.error(`Plugin '${pluginName}' error in onDetach:`, error);
41
- }
41
+ handleError.wrap(() => plugin.onDetach!(), ErrorCategory.PLUGIN, {
42
+ pluginName,
43
+ operation: 'onDetach',
44
+ });
42
45
  }
43
-
46
+
44
47
  this.plugins.delete(pluginName);
45
- this.executionOrder = this.executionOrder.filter(name => name !== pluginName);
46
-
48
+ this.executionOrder = this.executionOrder.filter(
49
+ (name) => name !== pluginName,
50
+ );
51
+
47
52
  return true;
48
53
  }
49
-
54
+
50
55
  /**
51
56
  * Get a plugin by name
52
57
  */
53
58
  get(pluginName: string): BlocPlugin<TState, TEvent> | undefined {
54
59
  return this.plugins.get(pluginName);
55
60
  }
56
-
61
+
57
62
  /**
58
63
  * Get all plugins in execution order
59
64
  */
60
65
  getAll(): ReadonlyArray<BlocPlugin<TState, TEvent>> {
61
- return this.executionOrder.map(name => this.plugins.get(name)!);
66
+ return this.executionOrder.map((name) => this.plugins.get(name)!);
62
67
  }
63
-
68
+
64
69
  /**
65
70
  * Clear all plugins
66
71
  */
@@ -69,20 +74,19 @@ export class BlocPluginRegistry<TState, TEvent = never> implements PluginRegistr
69
74
  if (this.attached) {
70
75
  for (const plugin of this.getAll()) {
71
76
  if (plugin.onDetach) {
72
- try {
73
- plugin.onDetach();
74
- } catch (error) {
75
- console.error(`Plugin '${plugin.name}' error in onDetach:`, error);
76
- }
77
+ handleError.wrap(() => plugin.onDetach!(), ErrorCategory.PLUGIN, {
78
+ pluginName: plugin.name,
79
+ operation: 'onDetach',
80
+ });
77
81
  }
78
82
  }
79
83
  }
80
-
84
+
81
85
  this.plugins.clear();
82
86
  this.executionOrder = [];
83
87
  this.attached = false;
84
88
  }
85
-
89
+
86
90
  /**
87
91
  * Attach all plugins to a bloc
88
92
  */
@@ -90,7 +94,7 @@ export class BlocPluginRegistry<TState, TEvent = never> implements PluginRegistr
90
94
  if (this.attached) {
91
95
  throw new Error('Plugins are already attached');
92
96
  }
93
-
97
+
94
98
  for (const plugin of this.getAll()) {
95
99
  if (plugin.onAttach) {
96
100
  try {
@@ -102,52 +106,61 @@ export class BlocPluginRegistry<TState, TEvent = never> implements PluginRegistr
102
106
  }
103
107
  }
104
108
  }
105
-
109
+
106
110
  this.attached = true;
107
111
  }
108
-
112
+
109
113
  /**
110
114
  * Transform state through all plugins
111
115
  */
112
116
  transformState(previousState: TState, nextState: TState): TState {
113
117
  let transformedState = nextState;
114
-
118
+
115
119
  for (const plugin of this.getAll()) {
116
120
  if (plugin.transformState && this.canTransformState(plugin)) {
117
121
  try {
118
- transformedState = plugin.transformState(previousState, transformedState);
122
+ transformedState = plugin.transformState(
123
+ previousState,
124
+ transformedState,
125
+ );
119
126
  } catch (error) {
120
- console.error(`Plugin '${plugin.name}' error in transformState:`, error);
127
+ console.error(
128
+ `Plugin '${plugin.name}' error in transformState:`,
129
+ error,
130
+ );
121
131
  // Continue with untransformed state
122
132
  }
123
133
  }
124
134
  }
125
-
135
+
126
136
  return transformedState;
127
137
  }
128
-
138
+
129
139
  /**
130
140
  * Transform event through all plugins
131
141
  */
132
142
  transformEvent(event: TEvent): TEvent | null {
133
143
  let transformedEvent: TEvent | null = event;
134
-
144
+
135
145
  for (const plugin of this.getAll()) {
136
146
  if (transformedEvent === null) break;
137
-
147
+
138
148
  if (plugin.transformEvent && this.canInterceptEvents(plugin)) {
139
149
  try {
140
150
  transformedEvent = plugin.transformEvent(transformedEvent);
141
151
  } catch (error) {
142
- console.error(`Plugin '${plugin.name}' error in transformEvent:`, error);
152
+ console.error(
153
+ `Plugin '${plugin.name}' error in transformEvent:`,
154
+ error,
155
+ );
143
156
  // Continue with untransformed event
144
157
  }
145
158
  }
146
159
  }
147
-
160
+
148
161
  return transformedEvent;
149
162
  }
150
-
163
+
151
164
  /**
152
165
  * Notify plugins of state change
153
166
  */
@@ -157,12 +170,15 @@ export class BlocPluginRegistry<TState, TEvent = never> implements PluginRegistr
157
170
  try {
158
171
  plugin.onStateChange(previousState, currentState);
159
172
  } catch (error) {
160
- console.error(`Plugin '${plugin.name}' error in onStateChange:`, error);
173
+ console.error(
174
+ `Plugin '${plugin.name}' error in onStateChange:`,
175
+ error,
176
+ );
161
177
  }
162
178
  }
163
179
  }
164
180
  }
165
-
181
+
166
182
  /**
167
183
  * Notify plugins of event
168
184
  */
@@ -177,7 +193,7 @@ export class BlocPluginRegistry<TState, TEvent = never> implements PluginRegistr
177
193
  }
178
194
  }
179
195
  }
180
-
196
+
181
197
  /**
182
198
  * Notify plugins of error
183
199
  */
@@ -192,29 +208,33 @@ export class BlocPluginRegistry<TState, TEvent = never> implements PluginRegistr
192
208
  }
193
209
  }
194
210
  }
195
-
211
+
196
212
  private validateCapabilities(plugin: BlocPlugin<TState, TEvent>): void {
197
213
  const caps = plugin.capabilities!;
198
-
214
+
199
215
  // Validate logical constraints
200
216
  if (caps.transformState && !caps.readState) {
201
- throw new Error(`Plugin '${plugin.name}': transformState requires readState capability`);
217
+ throw new Error(
218
+ `Plugin '${plugin.name}': transformState requires readState capability`,
219
+ );
202
220
  }
203
-
221
+
204
222
  if (caps.interceptEvents && !caps.readState) {
205
- throw new Error(`Plugin '${plugin.name}': interceptEvents requires readState capability`);
223
+ throw new Error(
224
+ `Plugin '${plugin.name}': interceptEvents requires readState capability`,
225
+ );
206
226
  }
207
227
  }
208
-
228
+
209
229
  private canReadState(plugin: BlocPlugin<TState, TEvent>): boolean {
210
230
  return !plugin.capabilities || plugin.capabilities.readState !== false;
211
231
  }
212
-
232
+
213
233
  private canTransformState(plugin: BlocPlugin<TState, TEvent>): boolean {
214
234
  return !plugin.capabilities || plugin.capabilities.transformState === true;
215
235
  }
216
-
236
+
217
237
  private canInterceptEvents(plugin: BlocPlugin<TState, TEvent>): boolean {
218
238
  return !plugin.capabilities || plugin.capabilities.interceptEvents === true;
219
239
  }
220
- }
240
+ }
@@ -1,4 +1,10 @@
1
- import { BlacPlugin, PluginRegistry, PluginMetrics, ErrorContext } from '../core/types';
1
+ import {
2
+ BlacPlugin,
3
+ PluginRegistry,
4
+ PluginMetrics,
5
+ ErrorContext,
6
+ AdapterMetadata,
7
+ } from './types';
2
8
  import { BlocBase } from '../BlocBase';
3
9
  import { Bloc } from '../Bloc';
4
10
 
@@ -32,7 +38,9 @@ export class SystemPluginRegistry implements PluginRegistry<BlacPlugin> {
32
38
 
33
39
  this.plugins.delete(pluginName);
34
40
  this.metrics.delete(pluginName);
35
- this.executionOrder = this.executionOrder.filter(name => name !== pluginName);
41
+ this.executionOrder = this.executionOrder.filter(
42
+ (name) => name !== pluginName,
43
+ );
36
44
 
37
45
  return true;
38
46
  }
@@ -48,7 +56,7 @@ export class SystemPluginRegistry implements PluginRegistry<BlacPlugin> {
48
56
  * Get all plugins in execution order
49
57
  */
50
58
  getAll(): ReadonlyArray<BlacPlugin> {
51
- return this.executionOrder.map(name => this.plugins.get(name)!);
59
+ return this.executionOrder.map((name) => this.plugins.get(name)!);
52
60
  }
53
61
 
54
62
  /**
@@ -66,7 +74,7 @@ export class SystemPluginRegistry implements PluginRegistry<BlacPlugin> {
66
74
  executeHook(
67
75
  hookName: keyof BlacPlugin,
68
76
  args: unknown[],
69
- errorHandler?: (error: Error, plugin: BlacPlugin) => void
77
+ errorHandler?: (error: Error, plugin: BlacPlugin) => void,
70
78
  ): void {
71
79
  for (const pluginName of this.executionOrder) {
72
80
  const plugin = this.plugins.get(pluginName)!;
@@ -86,7 +94,10 @@ export class SystemPluginRegistry implements PluginRegistry<BlacPlugin> {
86
94
  errorHandler(error as Error, plugin);
87
95
  } else {
88
96
  // Default: log and continue
89
- console.error(`Plugin '${pluginName}' error in hook '${hookName as string}':`, error);
97
+ console.error(
98
+ `Plugin '${pluginName}' error in hook '${hookName as string}':`,
99
+ error,
100
+ );
90
101
  }
91
102
  }
92
103
  }
@@ -125,7 +136,11 @@ export class SystemPluginRegistry implements PluginRegistry<BlacPlugin> {
125
136
  /**
126
137
  * Notify plugins of state change
127
138
  */
128
- notifyStateChanged(bloc: BlocBase<any>, previousState: any, currentState: any): void {
139
+ notifyStateChanged(
140
+ bloc: BlocBase<any>,
141
+ previousState: any,
142
+ currentState: any,
143
+ ): void {
129
144
  this.executeHook('onStateChanged', [bloc, previousState, currentState]);
130
145
  }
131
146
 
@@ -139,13 +154,40 @@ export class SystemPluginRegistry implements PluginRegistry<BlacPlugin> {
139
154
  /**
140
155
  * Notify plugins of errors
141
156
  */
142
- notifyError(error: Error, bloc: BlocBase<unknown>, context: ErrorContext): void {
157
+ notifyError(
158
+ error: Error,
159
+ bloc: BlocBase<unknown>,
160
+ context: ErrorContext,
161
+ ): void {
143
162
  this.executeHook('onError', [error, bloc, context], (hookError, plugin) => {
144
163
  // Double fault protection - if error handler fails, just log
145
164
  console.error(`Plugin '${plugin.name}' error handler failed:`, hookError);
146
165
  });
147
166
  }
148
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
+
149
191
  /**
150
192
  * Get metrics for a plugin
151
193
  */
@@ -157,12 +199,16 @@ export class SystemPluginRegistry implements PluginRegistry<BlacPlugin> {
157
199
  this.metrics.set(pluginName, new Map());
158
200
  }
159
201
 
160
- private recordSuccess(pluginName: string, hookName: string, startTime: number): void {
202
+ private recordSuccess(
203
+ pluginName: string,
204
+ hookName: string,
205
+ startTime: number,
206
+ ): void {
161
207
  const pluginMetrics = this.metrics.get(pluginName)!;
162
208
  const hookMetrics = pluginMetrics.get(hookName) || {
163
209
  executionTime: 0,
164
210
  executionCount: 0,
165
- errorCount: 0
211
+ errorCount: 0,
166
212
  };
167
213
 
168
214
  const executionTime = performance.now() - startTime;
@@ -171,22 +217,26 @@ export class SystemPluginRegistry implements PluginRegistry<BlacPlugin> {
171
217
  ...hookMetrics,
172
218
  executionTime: hookMetrics.executionTime + executionTime,
173
219
  executionCount: hookMetrics.executionCount + 1,
174
- lastExecutionTime: executionTime
220
+ lastExecutionTime: executionTime,
175
221
  });
176
222
  }
177
223
 
178
- private recordError(pluginName: string, hookName: string, error: Error): void {
224
+ private recordError(
225
+ pluginName: string,
226
+ hookName: string,
227
+ error: Error,
228
+ ): void {
179
229
  const pluginMetrics = this.metrics.get(pluginName)!;
180
230
  const hookMetrics = pluginMetrics.get(hookName) || {
181
231
  executionTime: 0,
182
232
  executionCount: 0,
183
- errorCount: 0
233
+ errorCount: 0,
184
234
  };
185
235
 
186
236
  pluginMetrics.set(hookName, {
187
237
  ...hookMetrics,
188
238
  errorCount: hookMetrics.errorCount + 1,
189
- lastError: error
239
+ lastError: error,
190
240
  });
191
241
  }
192
242
  }