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