@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
package/src/BlocBase.ts CHANGED
@@ -1,589 +1,398 @@
1
- import { BlacObservable } from './BlacObserver';
2
1
  import { generateUUID } from './utils/uuid';
2
+ import { BlocPlugin } from './plugins/types';
3
+ import { BlocPluginRegistry } from './plugins/BlocPluginRegistry';
4
+ import { SubscriptionManager } from './subscription/SubscriptionManager';
5
+ import {
6
+ BlocLifecycleManager,
7
+ BlocLifecycleState,
8
+ StateTransitionResult,
9
+ } from './lifecycle/BlocLifecycle';
10
+ import { BatchingManager } from './utils/BatchingManager';
11
+ import type { Blac } from './Blac';
3
12
 
4
13
  export type BlocInstanceId = string | number | undefined;
5
- type DependencySelector<S> = (
6
- currentState: S,
7
- previousState: S | undefined,
8
- instance: any,
9
- ) => unknown[];
10
14
 
11
- /**
12
- * Enum representing the lifecycle states of a Bloc instance
13
- * Used for atomic state transitions to prevent race conditions
14
- */
15
- export enum BlocLifecycleState {
16
- ACTIVE = 'active',
17
- DISPOSAL_REQUESTED = 'disposal_requested',
18
- DISPOSING = 'disposing',
19
- DISPOSED = 'disposed',
20
- }
21
-
22
- /**
23
- * Result of an atomic state transition operation
24
- */
25
- export interface StateTransitionResult {
26
- success: boolean;
27
- currentState: BlocLifecycleState;
28
- previousState: BlocLifecycleState;
29
- }
30
-
31
- // Define an interface for the static properties expected on a Bloc/Cubit constructor
32
15
  interface BlocStaticProperties {
33
16
  isolated: boolean;
34
17
  keepAlive: boolean;
18
+ plugins?: BlocPlugin<any, any>[];
35
19
  }
36
20
 
37
21
  /**
38
- * Base class for both Blocs and Cubits that provides core state management functionality.
39
- * Handles state transitions, observer notifications, lifecycle management, and addon integration.
40
- *
41
- * @abstract This class should be extended, not instantiated directly
42
- * @template S The type of state managed by this Bloc
43
- * @template P The type of props that can be passed during instance creation (optional)
22
+ * Base class for both Blocs and Cubits using unified subscription model.
44
23
  */
45
- export abstract class BlocBase<S, P = unknown> {
24
+ export abstract class BlocBase<S> {
46
25
  public uid = generateUUID();
47
- /**
48
- * When true, every consumer will receive its own unique instance of this Bloc.
49
- * Use this when state should not be shared between components.
50
- * @default false
51
- */
26
+ blacInstance?: Blac;
27
+
52
28
  static isolated = false;
53
29
  get isIsolated() {
54
30
  return this._isolated;
55
31
  }
56
32
 
57
- /**
58
- * When true, the Bloc instance persists even when there are no active consumers.
59
- * Useful for maintaining state between component unmount/remount cycles.
60
- * @default false
61
- */
62
33
  static keepAlive = false;
63
34
  get isKeepAlive() {
64
35
  return this._keepAlive;
65
36
  }
66
37
 
67
- /**
68
- * Defines how dependencies are selected from the state for efficient updates.
69
- * When provided, observers will only be notified when selected dependencies change.
70
- */
71
- defaultDependencySelector: DependencySelector<S> | undefined;
72
-
73
- /**
74
- * @internal
75
- * Indicates if this specific Bloc instance is isolated from others of the same type.
76
- */
77
38
  public _isolated = false;
39
+ public _id: BlocInstanceId;
40
+ public _instanceRef?: string;
41
+ public _name: string;
78
42
 
79
43
  /**
80
- * @internal
81
- * Observable responsible for managing state listeners and notifying consumers.
44
+ * Unified subscription manager for all state notifications
82
45
  */
83
- public _observer: BlacObservable<S>;
46
+ public _subscriptionManager: SubscriptionManager<S>;
84
47
 
85
- /**
86
- * The unique identifier for this Bloc instance.
87
- * Defaults to the class name, but can be customized.
88
- */
89
- public _id: BlocInstanceId;
48
+ _state: S;
49
+ private _lifecycleManager = new BlocLifecycleManager();
50
+ private _batchingManager = new BatchingManager<S>();
51
+ _keepAlive = false;
52
+ public lastUpdate?: number;
90
53
 
91
- /**
92
- * @internal
93
- * Reference string used internally for tracking and debugging.
94
- */
95
- public _instanceRef?: string;
54
+ _plugins = new BlocPluginRegistry<S, any>();
96
55
 
97
- /**
98
- * @internal
99
- * Indicates if this specific Bloc instance should be kept alive when no consumers are present.
100
- */
101
- public _keepAlive = false;
56
+ onDispose?: () => void;
102
57
 
103
58
  /**
104
- * @readonly
105
- * Timestamp when this Bloc instance was created, useful for debugging and performance tracking.
59
+ * Creates a new BlocBase instance with unified subscription management
106
60
  */
107
- public readonly _createdAt = Date.now();
61
+ constructor(initialState: S) {
62
+ this._state = initialState;
63
+ this._id = this.constructor.name;
64
+ this._name = this.constructor.name;
108
65
 
109
- /**
110
- * @internal
111
- * Atomic disposal state to prevent race conditions
112
- */
113
- private _disposalState: BlocLifecycleState = BlocLifecycleState.ACTIVE;
66
+ // Initialize unified subscription system
67
+ this._subscriptionManager = new SubscriptionManager(this as any);
68
+
69
+ // Access static properties
70
+ const Constructor = this.constructor as typeof BlocBase &
71
+ BlocStaticProperties;
72
+
73
+ this._keepAlive =
74
+ typeof Constructor.keepAlive === 'boolean'
75
+ ? Constructor.keepAlive
76
+ : false;
77
+ this._isolated =
78
+ typeof Constructor.isolated === 'boolean' ? Constructor.isolated : false;
79
+
80
+ // Initialize plugin registry
81
+ this._plugins = new BlocPluginRegistry<S, any>();
82
+
83
+ // Register static plugins
84
+ if (Constructor.plugins && Array.isArray(Constructor.plugins)) {
85
+ for (const plugin of Constructor.plugins) {
86
+ this._plugins.add(plugin);
87
+ }
88
+ }
89
+
90
+ // Attach all plugins
91
+ this._plugins.attach(this);
92
+ }
114
93
 
115
94
  /**
116
- * @internal
117
- * Timestamp when disposal was requested (for React Strict Mode grace period)
95
+ * Returns the current state
118
96
  */
119
- private _disposalRequestTime: number = 0;
97
+ get state(): S {
98
+ if (this._lifecycleManager.isDisposed) {
99
+ return this._state; // Return last known state for disposed blocs
100
+ }
101
+ return this._state;
102
+ }
120
103
 
121
104
  /**
122
- * @internal
123
- * The current state of the Bloc.
105
+ * Subscribe to all state changes
124
106
  */
125
- public _state: S;
107
+ subscribe(callback: (state: S) => void): () => void {
108
+ return this._subscriptionManager.subscribe({
109
+ type: 'observer',
110
+ notify: (state) => callback(state as S),
111
+ });
112
+ }
126
113
 
127
114
  /**
128
- * @internal
129
- * The previous state of the Bloc, maintained for comparison and history.
115
+ * Subscribe with a selector for optimized updates
130
116
  */
131
- public _oldState: S | undefined;
117
+ subscribeWithSelector<T>(
118
+ selector: (state: S) => T,
119
+ callback: (value: T) => void,
120
+ equalityFn?: (a: T, b: T) => boolean,
121
+ ): () => void {
122
+ return this._subscriptionManager.subscribe({
123
+ type: 'consumer',
124
+ selector: selector as any,
125
+ equalityFn: equalityFn as any,
126
+ notify: (value) => callback(value as T),
127
+ });
128
+ }
132
129
 
133
130
  /**
134
- * Props passed during Bloc instance creation.
135
- * Can be used to configure or parameterize the Bloc's behavior.
131
+ * Subscribe with React component reference for automatic cleanup
136
132
  */
137
- public props: P | null = null;
133
+ subscribeComponent(
134
+ componentRef: WeakRef<object>,
135
+ callback: () => void,
136
+ ): () => void {
137
+ return this._subscriptionManager.subscribe({
138
+ type: 'consumer',
139
+ weakRef: componentRef,
140
+ notify: callback,
141
+ });
142
+ }
138
143
 
139
144
  /**
140
- * @internal
141
- * Flag to prevent batching race conditions
145
+ * Get current subscription count
142
146
  */
143
- private _batchingLock = false;
147
+ get subscriptionCount(): number {
148
+ return this._subscriptionManager.size;
149
+ }
144
150
 
145
151
  /**
146
- * @internal
147
- * Pending batched updates
152
+ * Track property access for a subscription
148
153
  */
149
- private _pendingUpdates: Array<{
150
- newState: S;
151
- oldState: S;
152
- action?: unknown;
153
- }> = [];
154
+ trackAccess(subscriptionId: string, path: string, value?: unknown): void {
155
+ this._subscriptionManager.trackAccess(subscriptionId, path, value);
156
+ }
154
157
 
155
158
  /**
156
- * @internal
157
- * Map of consumer IDs to their WeakRef objects for proper cleanup
159
+ * Emit a new state
158
160
  */
159
- private _consumerRefs = new Map<string, WeakRef<object>>();
161
+ protected emit(newState: S, action?: unknown): void {
162
+ this._pushState(newState, this._state, action);
163
+ }
160
164
 
161
165
  /**
162
- * @internal
163
- * Validates that all consumer references are still alive
164
- * Removes dead consumers automatically
166
+ * Internal state push method used by Bloc
165
167
  */
166
- _validateConsumers = (): void => {
167
- const deadConsumers: string[] = [];
168
-
169
- for (const [consumerId, weakRef] of this._consumerRefs) {
170
- if (weakRef.deref() === undefined) {
171
- deadConsumers.push(consumerId);
172
- }
168
+ _pushState(newState: S, oldState: S, action?: unknown): void {
169
+ // Validate state emission conditions
170
+ if (this._lifecycleManager.currentState !== BlocLifecycleState.ACTIVE) {
171
+ this.blacInstance?.error(
172
+ `[${this._name}:${this._id}] Attempted state update on ${this._lifecycleManager.currentState} bloc. Update ignored.`,
173
+ );
174
+ return;
173
175
  }
174
176
 
175
- // Clean up dead consumers
176
- for (const consumerId of deadConsumers) {
177
- this._consumers.delete(consumerId);
178
- this._consumerRefs.delete(consumerId);
177
+ if (newState === undefined) {
178
+ return; // Silent failure for undefined states
179
179
  }
180
180
 
181
- // Schedule disposal if no live consumers remain
182
- if (
183
- this._consumers.size === 0 &&
184
- !this._keepAlive &&
185
- this._disposalState === BlocLifecycleState.ACTIVE
186
- ) {
187
- this._scheduleDisposal();
188
- }
189
- };
181
+ this._state = newState;
190
182
 
191
- /**
192
- * Creates a new BlocBase instance with the given initial state.
193
- * Sets up the observer, registers with the Blac manager, and initializes addons.
194
- *
195
- * @param initialState The initial state value for this Bloc
196
- */
197
- constructor(initialState: S) {
198
- this._state = initialState;
199
- this._observer = new BlacObservable(this);
200
- this._id = this.constructor.name;
183
+ // Apply plugins
184
+ const transformedState = this._plugins.transformState(
185
+ oldState,
186
+ newState,
187
+ ) as S;
188
+ this._state = transformedState;
189
+
190
+ // Notify plugins of state change
191
+ this._plugins.notifyStateChange(oldState, transformedState);
192
+
193
+ // Notify system plugins of state change
194
+ this.blacInstance?.plugins.notifyStateChanged(
195
+ this as any,
196
+ oldState,
197
+ transformedState,
198
+ );
201
199
 
202
- // Access static properties safely with proper type checking
203
- const Constructor = this.constructor as typeof BlocBase &
204
- BlocStaticProperties;
200
+ // Handle batching
201
+ if (this._batchingManager.isCurrentlyBatching) {
202
+ this._batchingManager.addUpdate({
203
+ newState: transformedState,
204
+ oldState,
205
+ action,
206
+ });
207
+ return;
208
+ }
205
209
 
206
- // Validate that the static properties exist and are boolean
207
- this._keepAlive =
208
- typeof Constructor.keepAlive === 'boolean'
209
- ? Constructor.keepAlive
210
- : false;
211
- this._isolated =
212
- typeof Constructor.isolated === 'boolean' ? Constructor.isolated : false;
210
+ // Notify all subscriptions
211
+ this._subscriptionManager.notify(transformedState, oldState, action);
212
+ this.lastUpdate = Date.now();
213
+ }
214
+
215
+ _batchUpdates(callback: () => void): void {
216
+ this._batchingManager.batchUpdates(callback, (finalUpdate) => {
217
+ this._subscriptionManager.notify(
218
+ finalUpdate.newState,
219
+ finalUpdate.oldState,
220
+ finalUpdate.action,
221
+ );
222
+ });
213
223
  }
214
224
 
215
225
  /**
216
- * Returns the current state of the Bloc.
217
- * Use this getter to access the state in a read-only manner.
218
- * Returns the state even during transitional lifecycle states for React compatibility.
226
+ * Add a plugin
219
227
  */
220
- get state(): S {
221
- // Allow state access during all states except DISPOSED for React compatibility
222
- if (this._disposalState === BlocLifecycleState.DISPOSED) {
223
- // Return the last known state for disposed blocs to prevent crashes
224
- return this._state;
228
+ addPlugin(plugin: BlocPlugin<S, any>): void {
229
+ this._plugins.add(plugin);
230
+
231
+ // If plugins are already attached (bloc is active), attach the new plugin
232
+ if ((this._plugins as any).attached && plugin.onAttach) {
233
+ try {
234
+ plugin.onAttach(this);
235
+ } catch (error) {
236
+ console.error(`Plugin '${plugin.name}' error in onAttach:`, error);
237
+ }
225
238
  }
226
- return this._state;
227
239
  }
228
240
 
229
241
  /**
230
- * Returns whether this Bloc instance has been disposed.
231
- * @returns true if the bloc is in DISPOSED state
242
+ * Remove a plugin
232
243
  */
233
- get isDisposed(): boolean {
234
- return this._disposalState === BlocLifecycleState.DISPOSED;
244
+ removePlugin(plugin: BlocPlugin<S, any>): void {
245
+ this._plugins.remove(plugin.name);
235
246
  }
236
247
 
237
248
  /**
238
- * @internal
239
- * Returns the name of the Bloc class for identification and debugging.
249
+ * Get all plugins
240
250
  */
241
- get _name() {
242
- return this.constructor.name;
251
+ get plugins(): ReadonlyArray<BlocPlugin<S, any>> {
252
+ return this._plugins.getAll();
243
253
  }
244
254
 
245
255
  /**
246
- * @internal
247
- * Updates the Bloc instance's ID to a new value.
248
- * Only updates if the new ID is defined and different from the current one.
249
- *
250
- * @param id The new ID to assign to this Bloc instance
256
+ * Disposal management
251
257
  */
252
- _updateId = (id?: BlocInstanceId) => {
253
- const originalId = this._id;
254
- if (!id || id === originalId) return;
255
- this._id = id;
256
- };
258
+ get isDisposed(): boolean {
259
+ return this._lifecycleManager.isDisposed;
260
+ }
257
261
 
258
262
  /**
259
- * @internal
260
- * Performs atomic state transition using compare-and-swap semantics
261
- * @param expectedState The expected current state
262
- * @param newState The desired new state
263
- * @returns Result indicating success/failure and state information
263
+ * Atomic state transition for disposal
264
264
  */
265
265
  _atomicStateTransition(
266
266
  expectedState: BlocLifecycleState,
267
267
  newState: BlocLifecycleState,
268
268
  ): StateTransitionResult {
269
- if (this._disposalState === expectedState) {
270
- const previousState = this._disposalState;
271
- this._disposalState = newState;
272
-
273
- // Log state transition for debugging
274
- if ((globalThis as any).Blac?.enableLog) {
275
- (globalThis as any).Blac?.log(
276
- `[${this._name}:${this._id}] State transition: ${previousState} -> ${newState} (SUCCESS)`,
277
- );
278
- }
279
-
280
- return {
281
- success: true,
282
- currentState: newState,
283
- previousState,
284
- };
285
- }
286
-
287
- // Log failed transition attempt
288
- if ((globalThis as any).Blac?.enableLog) {
289
- (globalThis as any).Blac?.log(
290
- `[${this._name}:${this._id}] State transition failed: expected ${expectedState}, current ${this._disposalState}`,
291
- );
292
- }
293
-
294
- return {
295
- success: false,
296
- currentState: this._disposalState,
297
- previousState: expectedState,
298
- };
269
+ return this._lifecycleManager.atomicStateTransition(
270
+ expectedState,
271
+ newState,
272
+ );
299
273
  }
300
274
 
301
275
  /**
302
- * @internal
303
- * Cleans up resources and removes this Bloc from the system.
304
- * Notifies the Blac manager and clears all observers.
276
+ * Dispose the bloc and clean up resources
305
277
  */
306
- _dispose(): boolean {
307
- // Step 1: Attempt atomic transition to DISPOSING state from either ACTIVE or DISPOSAL_REQUESTED
308
- let transitionResult = this._atomicStateTransition(
278
+ async dispose(): Promise<void> {
279
+ const transitionResult = this._lifecycleManager.atomicStateTransition(
309
280
  BlocLifecycleState.ACTIVE,
310
281
  BlocLifecycleState.DISPOSING,
311
282
  );
312
283
 
313
- // If that failed, try from DISPOSAL_REQUESTED state
314
- if (!transitionResult.success) {
315
- transitionResult = this._atomicStateTransition(
316
- BlocLifecycleState.DISPOSAL_REQUESTED,
317
- BlocLifecycleState.DISPOSING,
318
- );
319
- }
320
-
321
284
  if (!transitionResult.success) {
322
- // Already disposing or disposed - idempotent operation
323
- return false;
285
+ if (
286
+ this._lifecycleManager.currentState ===
287
+ BlocLifecycleState.DISPOSAL_REQUESTED
288
+ ) {
289
+ this._lifecycleManager.atomicStateTransition(
290
+ BlocLifecycleState.DISPOSAL_REQUESTED,
291
+ BlocLifecycleState.DISPOSING,
292
+ );
293
+ } else {
294
+ return;
295
+ }
324
296
  }
325
297
 
326
298
  try {
327
- // Step 2: Perform cleanup operations
328
- this._consumers.clear();
329
- this._consumerRefs.clear();
330
- this._observer.clear();
299
+ // Clear subscriptions
300
+ this._subscriptionManager.clear();
331
301
 
332
- // Call user-defined disposal hook
333
- this.onDispose?.();
302
+ // Call disposal hook
303
+ if (this.onDispose) {
304
+ this.onDispose();
305
+ }
334
306
 
335
- // Step 3: Final state transition to DISPOSED
336
- const finalResult = this._atomicStateTransition(
307
+ // Notify plugins of disposal
308
+ for (const plugin of this._plugins.getAll()) {
309
+ try {
310
+ if ('onDispose' in plugin && typeof plugin.onDispose === 'function') {
311
+ plugin.onDispose(this as any);
312
+ }
313
+ } catch (error) {
314
+ console.error('Plugin disposal error:', error);
315
+ }
316
+ }
317
+
318
+ // Call disposal handler
319
+ const handler = this._lifecycleManager.getDisposalHandler();
320
+ if (handler) {
321
+ handler(this as any);
322
+ }
323
+ } finally {
324
+ this._lifecycleManager.atomicStateTransition(
337
325
  BlocLifecycleState.DISPOSING,
338
326
  BlocLifecycleState.DISPOSED,
339
327
  );
340
-
341
- return finalResult.success;
342
- } catch (error) {
343
- // Recovery: Reset state on cleanup failure
344
- this._disposalState = BlocLifecycleState.ACTIVE;
345
- throw error;
346
328
  }
347
329
  }
348
330
 
349
331
  /**
350
- * @internal
351
- * Optional function to be called when the Bloc is disposed.
332
+ * Schedule disposal when no subscriptions remain
352
333
  */
353
- onDispose?: () => void;
334
+ _scheduleDisposal(): void {
335
+ const shouldDispose =
336
+ this._subscriptionManager.size === 0 && !this._keepAlive;
354
337
 
355
- /**
356
- * @internal
357
- * Set of consumer IDs currently listening to this Bloc's state changes.
358
- */
359
- _consumers = new Set<string>();
360
-
361
- /**
362
- * @internal
363
- * Registers a new consumer to this Bloc instance.
364
- * Notifies the Blac manager that a consumer has been added.
365
- *
366
- * @param consumerId The unique ID of the consumer being added
367
- * @param consumerRef Optional reference to the consumer object for cleanup validation
368
- */
369
- _addConsumer = (consumerId: string, consumerRef?: object): boolean => {
370
- // Atomic state validation - only allow consumer addition in ACTIVE state
371
- if (this._disposalState !== BlocLifecycleState.ACTIVE) {
372
- return false; // Clear failure indication
373
- }
374
-
375
- // Prevent duplicate consumers
376
- if (this._consumers.has(consumerId)) return true;
377
-
378
- // Safe consumer addition
379
- this._consumers.add(consumerId);
380
-
381
- // Store WeakRef for proper memory management
382
- if (consumerRef) {
383
- this._consumerRefs.set(consumerId, new WeakRef(consumerRef));
338
+ if (!shouldDispose) {
339
+ return;
384
340
  }
385
341
 
386
- // @ts-ignore - Blac is available globally
387
- (globalThis as any).Blac?.log(
388
- `[${this._name}:${this._id}] Consumer added. Total consumers: ${this._consumers.size}`,
342
+ this._lifecycleManager.scheduleDisposal(
343
+ 16,
344
+ () => this._subscriptionManager.size === 0 && !this._keepAlive,
345
+ () => this.dispose(),
389
346
  );
390
-
391
- // this._blac.dispatchEvent(BlacLifecycleEvent.BLOC_CONSUMER_ADDED, this, { consumerId });
392
- return true;
393
- };
347
+ }
394
348
 
395
349
  /**
396
- * @internal
397
- * Unregisters a consumer from this Bloc instance.
398
- * Notifies the Blac manager that a consumer has been removed.
399
- *
400
- * @param consumerId The unique ID of the consumer being removed
350
+ * Set disposal handler
401
351
  */
402
- _removeConsumer = (consumerId: string) => {
403
- if (!this._consumers.has(consumerId)) return;
404
-
405
- this._consumers.delete(consumerId);
406
- this._consumerRefs.delete(consumerId);
407
- // this._blac.dispatchEvent(BlacLifecycleEvent.BLOC_CONSUMER_REMOVED, this, { consumerId });
408
-
409
- // @ts-ignore - Blac is available globally
410
- (globalThis as any).Blac?.log(
411
- `[${this._name}:${this._id}] Consumer removed. Remaining consumers: ${this._consumers.size}, keepAlive: ${this._keepAlive}`,
352
+ setDisposalHandler(handler: (bloc: BlocBase<unknown>) => void): void {
353
+ this._lifecycleManager.setDisposalHandler(
354
+ handler as (bloc: unknown) => void,
412
355
  );
356
+ }
413
357
 
414
- // If no consumers remain and not keep-alive, schedule disposal
358
+ /**
359
+ * Check if disposal should be scheduled (called by subscription manager)
360
+ */
361
+ checkDisposal(): void {
415
362
  if (
416
- this._consumers.size === 0 &&
363
+ this._subscriptionManager.size === 0 &&
417
364
  !this._keepAlive &&
418
- this._disposalState === BlocLifecycleState.ACTIVE
365
+ this._lifecycleManager.currentState === BlocLifecycleState.ACTIVE
419
366
  ) {
420
- // @ts-ignore - Blac is available globally
421
- (globalThis as any).Blac?.log(
422
- `[${this._name}:${this._id}] No consumers left and not keep-alive. Scheduling disposal.`,
423
- );
424
367
  this._scheduleDisposal();
425
368
  }
426
- };
427
-
428
- /**
429
- * @internal
430
- * Handler function for disposal (can be set by Blac manager)
431
- */
432
- private _disposalHandler?: (bloc: BlocBase<unknown>) => void;
433
-
434
- /**
435
- * @internal
436
- * Sets the disposal handler for this bloc
437
- */
438
- _setDisposalHandler(handler: (bloc: BlocBase<unknown>) => void) {
439
- this._disposalHandler = handler;
440
369
  }
441
370
 
442
371
  /**
443
- * @internal
444
- * Schedules disposal of this bloc instance if it has no consumers
445
- * Uses atomic state transitions to prevent race conditions
372
+ * Cancel disposal if bloc is in disposal_requested state
446
373
  */
447
- private _scheduleDisposal(): void {
448
- // Step 1: Atomic transition to DISPOSAL_REQUESTED
449
- const requestResult = this._atomicStateTransition(
450
- BlocLifecycleState.ACTIVE,
451
- BlocLifecycleState.DISPOSAL_REQUESTED,
374
+ _cancelDisposalIfRequested(): void {
375
+ this.blacInstance?.log(
376
+ `[${this._name}:${this._id}] _cancelDisposalIfRequested called. Current state: ${this._lifecycleManager.currentState}`,
452
377
  );
453
378
 
454
- if (!requestResult.success) {
455
- // Already requested, disposing, or disposed
456
- return;
457
- }
458
-
459
- // Step 2: Verify disposal conditions under atomic protection
460
- const shouldDispose = this._consumers.size === 0 && !this._keepAlive;
379
+ const success = this._lifecycleManager.cancelDisposal();
461
380
 
462
- if (!shouldDispose) {
463
- // Conditions no longer met, revert to active
464
- this._atomicStateTransition(
465
- BlocLifecycleState.DISPOSAL_REQUESTED,
466
- BlocLifecycleState.ACTIVE,
381
+ if (success) {
382
+ this.blacInstance?.log(
383
+ `[${this._name}:${this._id}] Disposal cancelled - new subscription added`,
467
384
  );
468
- return;
469
- }
470
-
471
- // Record disposal request time for tracking
472
- this._disposalRequestTime = Date.now();
473
-
474
- // Step 3: Defer disposal until after current execution completes
475
- // This allows React Strict Mode's immediate remount to cancel disposal
476
- queueMicrotask(() => {
477
- // Re-verify disposal conditions - React Strict Mode remount may have cancelled this
478
- const stillShouldDispose =
479
- this._consumers.size === 0 &&
480
- !this._keepAlive &&
481
- this._observer.size === 0 &&
482
- (this as any)._disposalState === BlocLifecycleState.DISPOSAL_REQUESTED;
483
-
484
- if (stillShouldDispose) {
485
- // No cancellation occurred, proceed with disposal
486
- if (this._disposalHandler) {
487
- this._disposalHandler(this as any);
488
- } else {
489
- this._dispose();
490
- }
491
- } else {
492
- // Disposal was cancelled (React Strict Mode remount), revert to active
493
- this._atomicStateTransition(
494
- BlocLifecycleState.DISPOSAL_REQUESTED,
495
- BlocLifecycleState.ACTIVE,
496
- );
497
- }
498
- });
499
- }
500
-
501
- lastUpdate = Date.now();
502
-
503
- /**
504
- * @internal
505
- * Flag to indicate if batching is enabled for this bloc
506
- */
507
- private _batchingEnabled = false;
508
-
509
- /**
510
- * @internal
511
- * Updates the state and notifies all observers of the change.
512
- *
513
- * @param newState The new state to be set
514
- * @param oldState The previous state for comparison
515
- * @param action Optional metadata about what caused the state change
516
- */
517
- _pushState = (newState: S, oldState: S, action?: unknown): void => {
518
- // Validate newState
519
- if (newState === undefined) {
520
- return;
521
- }
522
-
523
- // Validate action type if provided
524
- if (
525
- action !== undefined &&
526
- typeof action !== 'object' &&
527
- typeof action !== 'function'
385
+ } else if (
386
+ this._lifecycleManager.currentState ===
387
+ BlocLifecycleState.DISPOSAL_REQUESTED
528
388
  ) {
529
- return;
530
- }
531
-
532
- if (this._batchingEnabled) {
533
- // When batching, just accumulate the updates
534
- this._pendingUpdates.push({ newState, oldState, action });
535
-
536
- // Update internal state for consistency
537
- this._oldState = oldState;
538
- this._state = newState;
539
- return;
540
- }
541
-
542
- // Normal state update flow
543
- this._oldState = oldState;
544
- this._state = newState;
545
-
546
- // Notify observers of the state change
547
- this._observer.notify(newState, oldState, action);
548
- this.lastUpdate = Date.now();
549
- };
550
-
551
- /**
552
- * Enables batching for multiple state updates
553
- * @param batchFn Function to execute with batching enabled
554
- */
555
- batch = <T>(batchFn: () => T): T => {
556
- // Prevent batching race conditions
557
- if (this._batchingLock) {
558
- // If already batching, just execute the function without nesting batches
559
- return batchFn();
560
- }
561
-
562
- this._batchingLock = true;
563
- this._batchingEnabled = true;
564
- this._pendingUpdates = [];
565
-
566
- try {
567
- const result = batchFn();
568
-
569
- // Process all batched updates
570
- if (this._pendingUpdates.length > 0) {
571
- // Only notify once with the final state
572
- const finalUpdate =
573
- this._pendingUpdates[this._pendingUpdates.length - 1];
574
- this._observer.notify(
575
- finalUpdate.newState,
576
- finalUpdate.oldState,
577
- finalUpdate.action,
578
- );
579
- this.lastUpdate = Date.now();
580
- }
581
-
582
- return result;
583
- } finally {
584
- this._batchingEnabled = false;
585
- this._batchingLock = false;
586
- this._pendingUpdates = [];
389
+ this.blacInstance?.warn(
390
+ `[${this._name}:${this._id}] Failed to cancel disposal. Current state: ${this._lifecycleManager.currentState}`,
391
+ );
392
+ } else if (this._lifecycleManager.isDisposed) {
393
+ this.blacInstance?.error(
394
+ `[${this._name}:${this._id}] Cannot cancel disposal - bloc is already disposed`,
395
+ );
587
396
  }
588
- };
397
+ }
589
398
  }