@blac/core 2.0.0-rc-3 → 2.0.0-rc-5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Blac.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ // TODO: Remove this eslint disable once any types are properly replaced
2
3
  import { BlocBase, BlocInstanceId } from "./BlocBase";
3
4
  import {
4
5
  BlocBaseAbstract,
@@ -16,7 +17,7 @@ export interface BlacConfig {
16
17
  exposeBlacInstance?: boolean;
17
18
  }
18
19
 
19
- export interface GetBlocOptions<B extends BlocBase<any>> {
20
+ export interface GetBlocOptions<B extends BlocBase<unknown>> {
20
21
  id?: string;
21
22
  selector?: BlocHookDependencyArrayFn<BlocState<B>>;
22
23
  props?: InferPropsFromGeneric<B>;
@@ -25,9 +26,61 @@ export interface GetBlocOptions<B extends BlocBase<any>> {
25
26
  throwIfNotFound?: boolean;
26
27
  }
27
28
 
29
+ /**
30
+ * Interface for Blac instance management
31
+ */
32
+ export interface BlacInstanceManager {
33
+ getInstance(): Blac;
34
+ setInstance(instance: Blac): void;
35
+ resetInstance(): void;
36
+ }
37
+
38
+ /**
39
+ * Default singleton implementation of BlacInstanceManager
40
+ */
41
+ class SingletonBlacManager implements BlacInstanceManager {
42
+ private static _instance: Blac;
43
+
44
+ getInstance(): Blac {
45
+ if (!SingletonBlacManager._instance) {
46
+ SingletonBlacManager._instance = new Blac({ __unsafe_ignore_singleton: true });
47
+ }
48
+ return SingletonBlacManager._instance;
49
+ }
50
+
51
+ setInstance(instance: Blac): void {
52
+ SingletonBlacManager._instance = instance;
53
+ }
54
+
55
+ resetInstance(): void {
56
+ const oldInstance = SingletonBlacManager._instance;
57
+ SingletonBlacManager._instance = new Blac({ __unsafe_ignore_singleton: true });
58
+
59
+ // Transfer any keep-alive blocs to the new instance
60
+ if (oldInstance) {
61
+ for (const bloc of oldInstance.keepAliveBlocs) {
62
+ SingletonBlacManager._instance.keepAliveBlocs.add(bloc);
63
+ SingletonBlacManager._instance.uidRegistry.set(bloc.uid, bloc);
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Global instance manager (can be replaced for testing or different patterns)
71
+ */
72
+ let instanceManager: BlacInstanceManager = new SingletonBlacManager();
73
+
74
+ /**
75
+ * Sets a custom instance manager (useful for testing or advanced patterns)
76
+ */
77
+ export function setBlacInstanceManager(manager: BlacInstanceManager): void {
78
+ instanceManager = manager;
79
+ }
80
+
28
81
  /**
29
82
  * Main Blac class that manages the state management system.
30
- * Implements a singleton pattern to ensure only one instance exists.
83
+ * Can work with singleton pattern or dependency injection.
31
84
  * Handles bloc lifecycle, and instance tracking.
32
85
  *
33
86
  * Key responsibilities:
@@ -36,15 +89,25 @@ export interface GetBlocOptions<B extends BlocBase<any>> {
36
89
  * - Providing logging and debugging capabilities
37
90
  */
38
91
  export class Blac {
39
- /** The singleton instance of Blac */
40
- static instance: Blac = new Blac();
92
+ /** @deprecated Use getInstance() instead */
93
+ static get instance(): Blac {
94
+ return instanceManager.getInstance();
95
+ }
41
96
  /** Timestamp when the instance was created */
42
97
  createdAt = Date.now();
43
- static getAllBlocs = Blac.instance.getAllBlocs;
98
+ static get getAllBlocs() {
99
+ return Blac.instance.getAllBlocs;
100
+ }
44
101
  /** Map storing all registered bloc instances by their class name and ID */
45
- blocInstanceMap: Map<string, BlocBase<any>> = new Map();
102
+ blocInstanceMap: Map<string, BlocBase<unknown>> = new Map();
46
103
  /** Map storing isolated bloc instances grouped by their constructor */
47
- isolatedBlocMap: Map<BlocConstructor<any>, BlocBase<any>[]> = new Map();
104
+ isolatedBlocMap: Map<BlocConstructor<any>, BlocBase<unknown>[]> = new Map();
105
+ /** Map for O(1) lookup of isolated blocs by UID */
106
+ private isolatedBlocIndex: Map<string, BlocBase<unknown>> = new Map();
107
+ /** Map tracking UIDs to prevent memory leaks */
108
+ uidRegistry: Map<string, BlocBase<unknown>> = new Map();
109
+ /** Set of keep-alive blocs for controlled cleanup */
110
+ keepAliveBlocs: Set<BlocBase<unknown>> = new Set();
48
111
  /** Flag to control whether changes should be posted to document */
49
112
  postChangesToDocument = false;
50
113
 
@@ -57,7 +120,7 @@ export class Blac {
57
120
  if (!__unsafe_ignore_singleton) {
58
121
  return Blac.instance;
59
122
  }
60
- Blac.instance = this;
123
+ instanceManager.setInstance(this);
61
124
  }
62
125
 
63
126
  /** Flag to enable/disable logging */
@@ -70,14 +133,14 @@ export class Blac {
70
133
  log = (...args: unknown[]) => {
71
134
  if (Blac.enableLog) console.warn(`☢️ [Blac ${this.createdAt.toString()}]`, ...args);
72
135
  };
73
- static log = Blac.instance.log;
136
+ static get log() { return Blac.instance.log; }
74
137
 
75
138
  /**
76
- * Gets the singleton instance of Blac
139
+ * Gets the current Blac instance
77
140
  * @returns The Blac instance
78
141
  */
79
142
  static getInstance(): Blac {
80
- return Blac.instance;
143
+ return instanceManager.getInstance();
81
144
  }
82
145
 
83
146
 
@@ -91,7 +154,7 @@ export class Blac {
91
154
  console.warn(`🚨 [Blac ${String(Blac.instance.createdAt)}]`, message, ...args);
92
155
  }
93
156
  };
94
- static warn = Blac.instance.warn;
157
+ static get warn() { return Blac.instance.warn; }
95
158
  /**
96
159
  * Logs an error message
97
160
  * @param message - Error message
@@ -102,7 +165,7 @@ export class Blac {
102
165
  console.error(`🚨 [Blac ${String(Blac.instance.createdAt)}]`, message, ...args);
103
166
  }
104
167
  };
105
- static error = Blac.instance.error;
168
+ static get error() { return Blac.instance.error; }
106
169
 
107
170
  /**
108
171
  * Resets the Blac instance to a new one, disposing non-keepAlive blocs
@@ -116,30 +179,35 @@ export class Blac {
116
179
  const oldIsolatedBlocMap = new Map(this.isolatedBlocMap);
117
180
 
118
181
  oldBlocInstanceMap.forEach((bloc) => {
119
- bloc._dispose();
182
+ if (!bloc._keepAlive) {
183
+ bloc._dispose();
184
+ }
120
185
  });
121
186
 
122
187
  oldIsolatedBlocMap.forEach((blocArray) => {
123
188
  blocArray.forEach((bloc) => {
124
- bloc._dispose();
189
+ if (!bloc._keepAlive) {
190
+ bloc._dispose();
191
+ }
125
192
  });
126
193
  });
127
194
 
128
195
  this.blocInstanceMap.clear();
129
196
  this.isolatedBlocMap.clear();
197
+ this.isolatedBlocIndex.clear();
130
198
 
131
- // Create and assign the new instance
132
- Blac.instance = new Blac({
133
- __unsafe_ignore_singleton: true,
134
- });
199
+ // Use instance manager to reset
200
+ instanceManager.resetInstance();
135
201
  }
136
- static resetInstance = Blac.instance.resetInstance;
202
+ static resetInstance = (): void => {
203
+ instanceManager.resetInstance();
204
+ };
137
205
 
138
206
  /**
139
207
  * Disposes of a bloc instance by removing it from the appropriate registry
140
208
  * @param bloc - The bloc instance to dispose
141
209
  */
142
- disposeBloc = (bloc: BlocBase<any>): void => {
210
+ disposeBloc = (bloc: BlocBase<unknown>): void => {
143
211
  const base = bloc.constructor as unknown as BlocBaseAbstract;
144
212
  const key = this.createBlocInstanceMapKey(bloc._name, bloc._id);
145
213
  this.log(`[${bloc._name}:${String(bloc._id)}] disposeBloc called. Isolated: ${String(base.isolated)}`);
@@ -167,18 +235,32 @@ export class Blac {
167
235
  * Unregister a bloc instance from the main registry
168
236
  * @param bloc - The bloc instance to unregister
169
237
  */
170
- unregisterBlocInstance(bloc: BlocBase<any>): void {
238
+ unregisterBlocInstance(bloc: BlocBase<unknown>): void {
171
239
  const key = this.createBlocInstanceMapKey(bloc._name, bloc._id);
172
240
  this.blocInstanceMap.delete(key);
241
+
242
+ // Clean up UID tracking
243
+ this.uidRegistry.delete(bloc.uid);
244
+
245
+ // Remove from keep-alive set
246
+ this.keepAliveBlocs.delete(bloc);
173
247
  }
174
248
 
175
249
  /**
176
250
  * Registers a bloc instance in the main registry
177
251
  * @param bloc - The bloc instance to register
178
252
  */
179
- registerBlocInstance(bloc: BlocBase<any>): void {
253
+ registerBlocInstance(bloc: BlocBase<unknown>): void {
180
254
  const key = this.createBlocInstanceMapKey(bloc._name, bloc._id);
181
255
  this.blocInstanceMap.set(key, bloc);
256
+
257
+ // Track UID for cleanup
258
+ this.uidRegistry.set(bloc.uid, bloc);
259
+
260
+ // Track keep-alive blocs
261
+ if (bloc._keepAlive) {
262
+ this.keepAliveBlocs.add(bloc);
263
+ }
182
264
  }
183
265
 
184
266
  /**
@@ -187,7 +269,7 @@ export class Blac {
187
269
  * @param id - The instance ID
188
270
  * @returns The found bloc instance or undefined if not found
189
271
  */
190
- findRegisteredBlocInstance<B extends BlocConstructor<unknown>>(
272
+ findRegisteredBlocInstance<B extends BlocConstructor<any>>(
191
273
  blocClass: B,
192
274
  id: BlocInstanceId,
193
275
  ): InstanceType<B> | undefined {
@@ -203,39 +285,59 @@ export class Blac {
203
285
  * Registers an isolated bloc instance in the isolated registry
204
286
  * @param bloc - The isolated bloc instance to register
205
287
  */
206
- registerIsolatedBlocInstance(bloc: BlocBase<any>): void {
207
- const blocClass = bloc.constructor as BlocConstructor<unknown>;
288
+ registerIsolatedBlocInstance(bloc: BlocBase<unknown>): void {
289
+ const blocClass = bloc.constructor as BlocConstructor<any>;
208
290
  const blocs = this.isolatedBlocMap.get(blocClass);
209
291
  if (blocs) {
210
292
  blocs.push(bloc);
211
293
  } else {
212
294
  this.isolatedBlocMap.set(blocClass, [bloc]);
213
295
  }
296
+
297
+ // Add to isolated index for O(1) lookups
298
+ this.isolatedBlocIndex.set(bloc.uid, bloc);
299
+
300
+ // Track UID for cleanup
301
+ this.uidRegistry.set(bloc.uid, bloc);
302
+
303
+ // Track keep-alive blocs
304
+ if (bloc._keepAlive) {
305
+ this.keepAliveBlocs.add(bloc);
306
+ }
214
307
  }
215
308
 
216
309
  /**
217
310
  * Unregister an isolated bloc instance from the isolated registry
218
311
  * @param bloc - The isolated bloc instance to unregister
219
312
  */
220
- unregisterIsolatedBlocInstance(bloc: BlocBase<any>): void {
313
+ unregisterIsolatedBlocInstance(bloc: BlocBase<unknown>): void {
221
314
  const blocClass = bloc.constructor;
222
- const blocs = this.isolatedBlocMap.get(blocClass as BlocConstructor<unknown>);
315
+ const blocs = this.isolatedBlocMap.get(blocClass as BlocConstructor<any>);
223
316
  if (blocs) {
224
- const index = blocs.findIndex((b) => b._id === bloc._id);
317
+ const index = blocs.findIndex((b) => b.uid === bloc.uid);
225
318
  if (index !== -1) {
226
319
  blocs.splice(index, 1);
227
320
  }
228
321
 
229
322
  if (blocs.length === 0) {
230
- this.isolatedBlocMap.delete(blocClass as BlocConstructor<unknown>);
323
+ this.isolatedBlocMap.delete(blocClass as BlocConstructor<any>);
231
324
  }
232
325
  }
326
+
327
+ // Remove from isolated index
328
+ this.isolatedBlocIndex.delete(bloc.uid);
329
+
330
+ // Clean up UID tracking
331
+ this.uidRegistry.delete(bloc.uid);
332
+
333
+ // Remove from keep-alive set
334
+ this.keepAliveBlocs.delete(bloc);
233
335
  }
234
336
 
235
337
  /**
236
- * Finds an isolated bloc instance by its class and ID
338
+ * Finds an isolated bloc instance by its class and ID (O(n) lookup)
237
339
  */
238
- findIsolatedBlocInstance<B extends BlocConstructor<unknown>>(
340
+ findIsolatedBlocInstance<B extends BlocConstructor<any>>(
239
341
  blocClass: B,
240
342
  id: BlocInstanceId,
241
343
  ): InstanceType<B> | undefined {
@@ -246,11 +348,20 @@ export class Blac {
246
348
  if (!blocs) {
247
349
  return undefined;
248
350
  }
249
- // Fix: Find the specific bloc by ID within the isolated array
351
+ // Find the specific bloc by ID within the isolated array
250
352
  const found = blocs.find((b) => b._id === id) as InstanceType<B> | undefined;
251
353
  return found;
252
354
  }
253
355
 
356
+ /**
357
+ * Finds an isolated bloc instance by UID (O(1) lookup)
358
+ */
359
+ findIsolatedBlocInstanceByUid<B extends BlocBase<unknown>>(
360
+ uid: string
361
+ ): B | undefined {
362
+ return this.isolatedBlocIndex.get(uid) as B | undefined;
363
+ }
364
+
254
365
  /**
255
366
  * Creates a new bloc instance and registers it in the appropriate registry
256
367
  * @param blocClass - The bloc class to instantiate
@@ -265,22 +376,25 @@ export class Blac {
265
376
  options: GetBlocOptions<InstanceType<B>> = {},
266
377
  ): InstanceType<B> {
267
378
  const { props, instanceRef } = options;
268
- const newBloc = new blocClass(props as never) as InstanceType<BlocConstructor<BlocBase<unknown>>>;
379
+ const newBloc = new blocClass(props as never) as InstanceType<B>;
269
380
  newBloc._instanceRef = instanceRef;
270
381
  newBloc.props = props || null;
271
382
  newBloc._updateId(id);
272
383
 
384
+ // Set up disposal handler to break circular dependency
385
+ newBloc._setDisposalHandler((bloc) => this.disposeBloc(bloc));
386
+
273
387
  if (newBloc.isIsolated) {
274
388
  this.registerIsolatedBlocInstance(newBloc);
275
- return newBloc as InstanceType<B>;
389
+ return newBloc;
276
390
  }
277
391
 
278
392
  this.registerBlocInstance(newBloc);
279
- return newBloc as InstanceType<B>;
393
+ return newBloc;
280
394
  }
281
395
 
282
396
 
283
- activateBloc = (bloc: BlocBase<any>): void => {
397
+ activateBloc = (bloc: BlocBase<unknown>): void => {
284
398
  const base = bloc.constructor as unknown as BlocConstructor<BlocBase<any>>;
285
399
  const isIsolated = bloc.isIsolated;
286
400
 
@@ -296,7 +410,7 @@ export class Blac {
296
410
  this.registerBlocInstance(bloc);
297
411
  }
298
412
  };
299
- static activateBloc = Blac.instance.activateBloc;
413
+ static get activateBloc() { return Blac.instance.activateBloc; }
300
414
 
301
415
  /**
302
416
  * Gets or creates a bloc instance based on the provided class and options.
@@ -348,7 +462,7 @@ export class Blac {
348
462
  this.log(`[${blocClass.name}:${String(blocId)}] (getBloc) No existing instance found. Creating new one.`, options, bloc);
349
463
  return bloc;
350
464
  };
351
- static getBloc = Blac.instance.getBloc;
465
+ static get getBloc() { return Blac.instance.getBloc; }
352
466
 
353
467
  /**
354
468
  * Gets a bloc instance or throws an error if it doesn't exist
@@ -358,7 +472,7 @@ export class Blac {
358
472
  * - props: Properties to pass to the bloc constructor
359
473
  * - instanceRef: Optional reference string for the instance
360
474
  */
361
- getBlocOrThrow = <B extends BlocConstructor<unknown>>(
475
+ getBlocOrThrow = <B extends BlocConstructor<any>>(
362
476
  blocClass: B,
363
477
  options: {
364
478
  id?: BlocInstanceId;
@@ -378,7 +492,7 @@ export class Blac {
378
492
  }
379
493
  throw new Error(`Bloc ${blocClass.name} not found`);
380
494
  };
381
- static getBlocOrThrow = Blac.instance.getBlocOrThrow;
495
+ static get getBlocOrThrow() { return Blac.instance.getBlocOrThrow; }
382
496
 
383
497
  /**
384
498
  * Gets all instances of a specific bloc class
@@ -387,7 +501,7 @@ export class Blac {
387
501
  * - searchIsolated: Whether to search in isolated blocs (defaults to bloc's isolated property)
388
502
  * @returns Array of matching bloc instances
389
503
  */
390
- getAllBlocs = <B extends BlocConstructor<unknown>>(
504
+ getAllBlocs = <B extends BlocConstructor<any>>(
391
505
  blocClass: B,
392
506
  options: {
393
507
  searchIsolated?: boolean;
@@ -413,4 +527,83 @@ export class Blac {
413
527
 
414
528
  return results;
415
529
  };
530
+
531
+ /**
532
+ * Disposes all keep-alive blocs of a specific type
533
+ * @param blocClass - The bloc class to dispose
534
+ */
535
+ disposeKeepAliveBlocs = <B extends BlocConstructor<any>>(
536
+ blocClass?: B
537
+ ): void => {
538
+ const toDispose: BlocBase<unknown>[] = [];
539
+
540
+ for (const bloc of this.keepAliveBlocs) {
541
+ if (!blocClass || bloc.constructor === blocClass) {
542
+ toDispose.push(bloc);
543
+ }
544
+ }
545
+
546
+ toDispose.forEach(bloc => bloc._dispose());
547
+ };
548
+ static get disposeKeepAliveBlocs() { return Blac.instance.disposeKeepAliveBlocs; }
549
+
550
+ /**
551
+ * Disposes all blocs matching a pattern
552
+ * @param predicate - Function to test each bloc for disposal
553
+ */
554
+ disposeBlocs = (predicate: (bloc: BlocBase<unknown>) => boolean): void => {
555
+ const toDispose: BlocBase<unknown>[] = [];
556
+
557
+ // Check registered blocs
558
+ for (const bloc of this.blocInstanceMap.values()) {
559
+ if (predicate(bloc)) {
560
+ toDispose.push(bloc);
561
+ }
562
+ }
563
+
564
+ // Check isolated blocs
565
+ for (const blocs of this.isolatedBlocMap.values()) {
566
+ for (const bloc of blocs) {
567
+ if (predicate(bloc)) {
568
+ toDispose.push(bloc);
569
+ }
570
+ }
571
+ }
572
+
573
+ toDispose.forEach(bloc => bloc._dispose());
574
+ };
575
+ static get disposeBlocs() { return Blac.instance.disposeBlocs; }
576
+
577
+ /**
578
+ * Gets memory usage statistics for debugging
579
+ */
580
+ getMemoryStats = () => {
581
+ return {
582
+ totalBlocs: this.uidRegistry.size,
583
+ registeredBlocs: this.blocInstanceMap.size,
584
+ isolatedBlocs: Array.from(this.isolatedBlocMap.values()).reduce((sum, arr) => sum + arr.length, 0),
585
+ keepAliveBlocs: this.keepAliveBlocs.size,
586
+ isolatedBlocTypes: this.isolatedBlocMap.size,
587
+ };
588
+ };
589
+ static get getMemoryStats() { return Blac.instance.getMemoryStats; }
590
+
591
+ /**
592
+ * Validates consumer references and cleans up orphaned consumers
593
+ */
594
+ validateConsumers = (): void => {
595
+ for (const bloc of this.uidRegistry.values()) {
596
+ // This would need to be called periodically to clean up orphaned consumers
597
+ // Implementation depends on how we want to handle consumer validation
598
+ if (bloc._consumers.size === 0 && !bloc._keepAlive) {
599
+ // Schedule disposal for blocs with no consumers
600
+ setTimeout(() => {
601
+ if (bloc._consumers.size === 0 && !bloc._keepAlive) {
602
+ bloc._dispose();
603
+ }
604
+ }, 1000); // Give a grace period
605
+ }
606
+ }
607
+ };
608
+ static get validateConsumers() { return Blac.instance.validateConsumers; }
416
609
  }
package/src/BlocBase.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { Blac } from './Blac';
2
1
  import { BlacObservable } from './BlacObserver';
3
2
  import { BlocConstructor } from './types';
4
3
 
@@ -62,12 +61,6 @@ export abstract class BlocBase<
62
61
  */
63
62
  public _observer: BlacObservable<S>;
64
63
 
65
- /**
66
- * @internal
67
- * Reference to the global Blac manager instance.
68
- */
69
- public _blac = Blac.getInstance();
70
-
71
64
  /**
72
65
  * The unique identifier for this Bloc instance.
73
66
  * Defaults to the class name, but can be customized.
@@ -119,14 +112,14 @@ export abstract class BlocBase<
119
112
  constructor(initialState: S) {
120
113
  this._state = initialState;
121
114
  this._observer = new BlacObservable(this);
122
- Blac.log('Bloc Created', this)
123
115
  this._id = this.constructor.name;
124
116
 
125
- // Use a type assertion for the constructor to access static properties safely
126
- const constructorWithStaticProps = this.constructor as BlocConstructor<this> & BlocStaticProperties;
127
-
128
- this._keepAlive = constructorWithStaticProps.keepAlive;
129
- this._isolated = constructorWithStaticProps.isolated;
117
+ // Access static properties safely with proper type checking
118
+ const Constructor = this.constructor as typeof BlocBase & BlocStaticProperties;
119
+
120
+ // Validate that the static properties exist and are boolean
121
+ this._keepAlive = typeof Constructor.keepAlive === 'boolean' ? Constructor.keepAlive : false;
122
+ this._isolated = typeof Constructor.isolated === 'boolean' ? Constructor.isolated : false;
130
123
  }
131
124
 
132
125
  /**
@@ -164,11 +157,17 @@ export abstract class BlocBase<
164
157
  * Notifies the Blac manager and clears all observers.
165
158
  */
166
159
  _dispose() {
160
+ // Clear all consumers
161
+ this._consumers.clear();
162
+
163
+ // Clear observer subscriptions
167
164
  this._observer.clear();
165
+
166
+ // Call user-defined disposal hook
168
167
  this.onDispose?.();
169
- // this._blac.dispatchEvent(BlacLifecycleEvent.BLOC_DISPOSED, this);
170
- Blac.log('BlocBase._dispose', this);
171
- Blac.instance.disposeBloc(this);
168
+
169
+ // The Blac manager will handle removal from registry
170
+ // This method is called by Blac.disposeBloc, so we don't need to call it again
172
171
  }
173
172
 
174
173
  /**
@@ -183,18 +182,29 @@ export abstract class BlocBase<
183
182
  */
184
183
  _consumers = new Set<string>();
185
184
 
185
+ /**
186
+ * @internal
187
+ * WeakSet to track consumer references for cleanup validation
188
+ */
189
+ private _consumerRefs = new WeakSet<object>();
190
+
186
191
  /**
187
192
  * @internal
188
193
  * Registers a new consumer to this Bloc instance.
189
194
  * Notifies the Blac manager that a consumer has been added.
190
195
  *
191
196
  * @param consumerId The unique ID of the consumer being added
197
+ * @param consumerRef Optional reference to the consumer object for cleanup validation
192
198
  */
193
- _addConsumer = (consumerId: string) => {
199
+ _addConsumer = (consumerId: string, consumerRef?: object) => {
194
200
  if (this._consumers.has(consumerId)) return;
195
201
  this._consumers.add(consumerId);
202
+
203
+ if (consumerRef) {
204
+ this._consumerRefs.add(consumerRef);
205
+ }
206
+
196
207
  // this._blac.dispatchEvent(BlacLifecycleEvent.BLOC_CONSUMER_ADDED, this, { consumerId });
197
- Blac.log('BlocBase._addConsumer', this, consumerId);
198
208
  };
199
209
 
200
210
  /**
@@ -208,11 +218,58 @@ export abstract class BlocBase<
208
218
  if (!this._consumers.has(consumerId)) return;
209
219
  this._consumers.delete(consumerId);
210
220
  // this._blac.dispatchEvent(BlacLifecycleEvent.BLOC_CONSUMER_REMOVED, this, { consumerId });
211
- Blac.log('BlocBase._removeConsumer', this, consumerId);
221
+
222
+ // If no consumers remain and not keep-alive, schedule disposal
223
+ if (this._consumers.size === 0 && !this._keepAlive) {
224
+ this._scheduleDisposal();
225
+ }
212
226
  };
213
227
 
228
+ /**
229
+ * @internal
230
+ * Handler function for disposal (can be set by Blac manager)
231
+ */
232
+ private _disposalHandler?: (bloc: BlocBase<unknown>) => void;
233
+
234
+ /**
235
+ * @internal
236
+ * Sets the disposal handler for this bloc
237
+ */
238
+ _setDisposalHandler(handler: (bloc: BlocBase<unknown>) => void) {
239
+ this._disposalHandler = handler;
240
+ }
241
+
242
+ /**
243
+ * @internal
244
+ * Schedules disposal of this bloc instance if it has no consumers
245
+ */
246
+ private _scheduleDisposal() {
247
+ // Use setTimeout to avoid disposal during render cycles
248
+ setTimeout(() => {
249
+ if (this._consumers.size === 0 && !this._keepAlive) {
250
+ if (this._disposalHandler) {
251
+ this._disposalHandler(this as any);
252
+ } else {
253
+ this._dispose();
254
+ }
255
+ }
256
+ }, 0);
257
+ }
258
+
214
259
  lastUpdate = Date.now();
215
260
 
261
+ /**
262
+ * @internal
263
+ * Flag to indicate if batching is enabled for this bloc
264
+ */
265
+ private _batchingEnabled = false;
266
+
267
+ /**
268
+ * @internal
269
+ * Pending state updates when batching is enabled
270
+ */
271
+ private _pendingUpdates: Array<{newState: S, oldState: S, action?: unknown}> = [];
272
+
216
273
  /**
217
274
  * @internal
218
275
  * Updates the state and notifies all observers of the change.
@@ -222,8 +279,59 @@ export abstract class BlocBase<
222
279
  * @param action Optional metadata about what caused the state change
223
280
  */
224
281
  _pushState = (newState: S, oldState: S, action?: unknown): void => {
282
+ // Runtime validation for state changes
283
+ if (newState === undefined) {
284
+ console.warn('BlocBase._pushState: newState is undefined', this);
285
+ return;
286
+ }
287
+
288
+ // Validate action type if provided
289
+ if (action !== undefined && action !== null) {
290
+ const actionType = typeof action;
291
+ if (!(['string', 'object', 'number'].includes(actionType))) {
292
+ console.warn('BlocBase._pushState: Invalid action type', this, action);
293
+ }
294
+ }
295
+
296
+ // If batching is enabled, queue the update
297
+ if (this._batchingEnabled) {
298
+ this._pendingUpdates.push({ newState, oldState, action });
299
+ return;
300
+ }
301
+
302
+ this._oldState = oldState;
225
303
  this._state = newState;
226
304
  this._observer.notify(newState, oldState, action);
227
305
  this.lastUpdate = Date.now();
228
306
  };
307
+
308
+ /**
309
+ * Enables batching for multiple state updates
310
+ * @param batchFn Function to execute with batching enabled
311
+ */
312
+ batch = <T>(batchFn: () => T): T => {
313
+ const wasBatching = this._batchingEnabled;
314
+ this._batchingEnabled = true;
315
+
316
+ try {
317
+ const result = batchFn();
318
+
319
+ // Process all pending updates
320
+ if (this._pendingUpdates.length > 0) {
321
+ const lastUpdate = this._pendingUpdates[this._pendingUpdates.length - 1];
322
+ this._oldState = this._pendingUpdates[0].oldState;
323
+ this._state = lastUpdate.newState;
324
+
325
+ // Notify with the final state
326
+ this._observer.notify(lastUpdate.newState, this._oldState, lastUpdate.action);
327
+ this.lastUpdate = Date.now();
328
+
329
+ this._pendingUpdates = [];
330
+ }
331
+
332
+ return result;
333
+ } finally {
334
+ this._batchingEnabled = wasBatching;
335
+ }
336
+ };
229
337
  }