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

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,59 @@ 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
+ for (const bloc of oldInstance.keepAliveBlocs) {
61
+ SingletonBlacManager._instance.keepAliveBlocs.add(bloc);
62
+ SingletonBlacManager._instance.uidRegistry.set(bloc.uid, bloc);
63
+ }
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Global instance manager (can be replaced for testing or different patterns)
69
+ */
70
+ let instanceManager: BlacInstanceManager = new SingletonBlacManager();
71
+
72
+ /**
73
+ * Sets a custom instance manager (useful for testing or advanced patterns)
74
+ */
75
+ export function setBlacInstanceManager(manager: BlacInstanceManager): void {
76
+ instanceManager = manager;
77
+ }
78
+
28
79
  /**
29
80
  * Main Blac class that manages the state management system.
30
- * Implements a singleton pattern to ensure only one instance exists.
81
+ * Can work with singleton pattern or dependency injection.
31
82
  * Handles bloc lifecycle, and instance tracking.
32
83
  *
33
84
  * Key responsibilities:
@@ -36,15 +87,25 @@ export interface GetBlocOptions<B extends BlocBase<any>> {
36
87
  * - Providing logging and debugging capabilities
37
88
  */
38
89
  export class Blac {
39
- /** The singleton instance of Blac */
40
- static instance: Blac = new Blac();
90
+ /** @deprecated Use getInstance() instead */
91
+ static get instance(): Blac {
92
+ return instanceManager.getInstance();
93
+ }
41
94
  /** Timestamp when the instance was created */
42
95
  createdAt = Date.now();
43
- static getAllBlocs = Blac.instance.getAllBlocs;
96
+ static get getAllBlocs() {
97
+ return Blac.instance.getAllBlocs;
98
+ }
44
99
  /** Map storing all registered bloc instances by their class name and ID */
45
- blocInstanceMap: Map<string, BlocBase<any>> = new Map();
100
+ blocInstanceMap: Map<string, BlocBase<unknown>> = new Map();
46
101
  /** Map storing isolated bloc instances grouped by their constructor */
47
- isolatedBlocMap: Map<BlocConstructor<any>, BlocBase<any>[]> = new Map();
102
+ isolatedBlocMap: Map<BlocConstructor<unknown>, BlocBase<unknown>[]> = new Map();
103
+ /** Map for O(1) lookup of isolated blocs by UID */
104
+ private isolatedBlocIndex: Map<string, BlocBase<unknown>> = new Map();
105
+ /** Map tracking UIDs to prevent memory leaks */
106
+ uidRegistry: Map<string, BlocBase<unknown>> = new Map();
107
+ /** Set of keep-alive blocs for controlled cleanup */
108
+ keepAliveBlocs: Set<BlocBase<unknown>> = new Set();
48
109
  /** Flag to control whether changes should be posted to document */
49
110
  postChangesToDocument = false;
50
111
 
@@ -57,7 +118,7 @@ export class Blac {
57
118
  if (!__unsafe_ignore_singleton) {
58
119
  return Blac.instance;
59
120
  }
60
- Blac.instance = this;
121
+ instanceManager.setInstance(this);
61
122
  }
62
123
 
63
124
  /** Flag to enable/disable logging */
@@ -70,14 +131,14 @@ export class Blac {
70
131
  log = (...args: unknown[]) => {
71
132
  if (Blac.enableLog) console.warn(`☢️ [Blac ${this.createdAt.toString()}]`, ...args);
72
133
  };
73
- static log = Blac.instance.log;
134
+ static get log() { return Blac.instance.log; }
74
135
 
75
136
  /**
76
- * Gets the singleton instance of Blac
137
+ * Gets the current Blac instance
77
138
  * @returns The Blac instance
78
139
  */
79
140
  static getInstance(): Blac {
80
- return Blac.instance;
141
+ return instanceManager.getInstance();
81
142
  }
82
143
 
83
144
 
@@ -91,7 +152,7 @@ export class Blac {
91
152
  console.warn(`🚨 [Blac ${String(Blac.instance.createdAt)}]`, message, ...args);
92
153
  }
93
154
  };
94
- static warn = Blac.instance.warn;
155
+ static get warn() { return Blac.instance.warn; }
95
156
  /**
96
157
  * Logs an error message
97
158
  * @param message - Error message
@@ -102,7 +163,7 @@ export class Blac {
102
163
  console.error(`🚨 [Blac ${String(Blac.instance.createdAt)}]`, message, ...args);
103
164
  }
104
165
  };
105
- static error = Blac.instance.error;
166
+ static get error() { return Blac.instance.error; }
106
167
 
107
168
  /**
108
169
  * Resets the Blac instance to a new one, disposing non-keepAlive blocs
@@ -116,30 +177,35 @@ export class Blac {
116
177
  const oldIsolatedBlocMap = new Map(this.isolatedBlocMap);
117
178
 
118
179
  oldBlocInstanceMap.forEach((bloc) => {
119
- bloc._dispose();
180
+ if (!bloc._keepAlive) {
181
+ bloc._dispose();
182
+ }
120
183
  });
121
184
 
122
185
  oldIsolatedBlocMap.forEach((blocArray) => {
123
186
  blocArray.forEach((bloc) => {
124
- bloc._dispose();
187
+ if (!bloc._keepAlive) {
188
+ bloc._dispose();
189
+ }
125
190
  });
126
191
  });
127
192
 
128
193
  this.blocInstanceMap.clear();
129
194
  this.isolatedBlocMap.clear();
195
+ this.isolatedBlocIndex.clear();
130
196
 
131
- // Create and assign the new instance
132
- Blac.instance = new Blac({
133
- __unsafe_ignore_singleton: true,
134
- });
197
+ // Use instance manager to reset
198
+ instanceManager.resetInstance();
135
199
  }
136
- static resetInstance = Blac.instance.resetInstance;
200
+ static resetInstance = (): void => {
201
+ instanceManager.resetInstance();
202
+ };
137
203
 
138
204
  /**
139
205
  * Disposes of a bloc instance by removing it from the appropriate registry
140
206
  * @param bloc - The bloc instance to dispose
141
207
  */
142
- disposeBloc = (bloc: BlocBase<any>): void => {
208
+ disposeBloc = (bloc: BlocBase<unknown>): void => {
143
209
  const base = bloc.constructor as unknown as BlocBaseAbstract;
144
210
  const key = this.createBlocInstanceMapKey(bloc._name, bloc._id);
145
211
  this.log(`[${bloc._name}:${String(bloc._id)}] disposeBloc called. Isolated: ${String(base.isolated)}`);
@@ -167,18 +233,32 @@ export class Blac {
167
233
  * Unregister a bloc instance from the main registry
168
234
  * @param bloc - The bloc instance to unregister
169
235
  */
170
- unregisterBlocInstance(bloc: BlocBase<any>): void {
236
+ unregisterBlocInstance(bloc: BlocBase<unknown>): void {
171
237
  const key = this.createBlocInstanceMapKey(bloc._name, bloc._id);
172
238
  this.blocInstanceMap.delete(key);
239
+
240
+ // Clean up UID tracking
241
+ this.uidRegistry.delete(bloc.uid);
242
+
243
+ // Remove from keep-alive set
244
+ this.keepAliveBlocs.delete(bloc);
173
245
  }
174
246
 
175
247
  /**
176
248
  * Registers a bloc instance in the main registry
177
249
  * @param bloc - The bloc instance to register
178
250
  */
179
- registerBlocInstance(bloc: BlocBase<any>): void {
251
+ registerBlocInstance(bloc: BlocBase<unknown>): void {
180
252
  const key = this.createBlocInstanceMapKey(bloc._name, bloc._id);
181
253
  this.blocInstanceMap.set(key, bloc);
254
+
255
+ // Track UID for cleanup
256
+ this.uidRegistry.set(bloc.uid, bloc);
257
+
258
+ // Track keep-alive blocs
259
+ if (bloc._keepAlive) {
260
+ this.keepAliveBlocs.add(bloc);
261
+ }
182
262
  }
183
263
 
184
264
  /**
@@ -203,7 +283,7 @@ export class Blac {
203
283
  * Registers an isolated bloc instance in the isolated registry
204
284
  * @param bloc - The isolated bloc instance to register
205
285
  */
206
- registerIsolatedBlocInstance(bloc: BlocBase<any>): void {
286
+ registerIsolatedBlocInstance(bloc: BlocBase<unknown>): void {
207
287
  const blocClass = bloc.constructor as BlocConstructor<unknown>;
208
288
  const blocs = this.isolatedBlocMap.get(blocClass);
209
289
  if (blocs) {
@@ -211,17 +291,28 @@ export class Blac {
211
291
  } else {
212
292
  this.isolatedBlocMap.set(blocClass, [bloc]);
213
293
  }
294
+
295
+ // Add to isolated index for O(1) lookups
296
+ this.isolatedBlocIndex.set(bloc.uid, bloc);
297
+
298
+ // Track UID for cleanup
299
+ this.uidRegistry.set(bloc.uid, bloc);
300
+
301
+ // Track keep-alive blocs
302
+ if (bloc._keepAlive) {
303
+ this.keepAliveBlocs.add(bloc);
304
+ }
214
305
  }
215
306
 
216
307
  /**
217
308
  * Unregister an isolated bloc instance from the isolated registry
218
309
  * @param bloc - The isolated bloc instance to unregister
219
310
  */
220
- unregisterIsolatedBlocInstance(bloc: BlocBase<any>): void {
311
+ unregisterIsolatedBlocInstance(bloc: BlocBase<unknown>): void {
221
312
  const blocClass = bloc.constructor;
222
313
  const blocs = this.isolatedBlocMap.get(blocClass as BlocConstructor<unknown>);
223
314
  if (blocs) {
224
- const index = blocs.findIndex((b) => b._id === bloc._id);
315
+ const index = blocs.findIndex((b) => b.uid === bloc.uid);
225
316
  if (index !== -1) {
226
317
  blocs.splice(index, 1);
227
318
  }
@@ -230,10 +321,19 @@ export class Blac {
230
321
  this.isolatedBlocMap.delete(blocClass as BlocConstructor<unknown>);
231
322
  }
232
323
  }
324
+
325
+ // Remove from isolated index
326
+ this.isolatedBlocIndex.delete(bloc.uid);
327
+
328
+ // Clean up UID tracking
329
+ this.uidRegistry.delete(bloc.uid);
330
+
331
+ // Remove from keep-alive set
332
+ this.keepAliveBlocs.delete(bloc);
233
333
  }
234
334
 
235
335
  /**
236
- * Finds an isolated bloc instance by its class and ID
336
+ * Finds an isolated bloc instance by its class and ID (O(n) lookup)
237
337
  */
238
338
  findIsolatedBlocInstance<B extends BlocConstructor<unknown>>(
239
339
  blocClass: B,
@@ -246,11 +346,20 @@ export class Blac {
246
346
  if (!blocs) {
247
347
  return undefined;
248
348
  }
249
- // Fix: Find the specific bloc by ID within the isolated array
349
+ // Find the specific bloc by ID within the isolated array
250
350
  const found = blocs.find((b) => b._id === id) as InstanceType<B> | undefined;
251
351
  return found;
252
352
  }
253
353
 
354
+ /**
355
+ * Finds an isolated bloc instance by UID (O(1) lookup)
356
+ */
357
+ findIsolatedBlocInstanceByUid<B extends BlocBase<unknown>>(
358
+ uid: string
359
+ ): B | undefined {
360
+ return this.isolatedBlocIndex.get(uid) as B | undefined;
361
+ }
362
+
254
363
  /**
255
364
  * Creates a new bloc instance and registers it in the appropriate registry
256
365
  * @param blocClass - The bloc class to instantiate
@@ -259,7 +368,7 @@ export class Blac {
259
368
  * @param instanceRef - Optional reference string for the instance
260
369
  * @returns The newly created bloc instance
261
370
  */
262
- createNewBlocInstance<B extends BlocConstructor<BlocBase<any>>>(
371
+ createNewBlocInstance<B extends BlocConstructor<BlocBase<unknown>>>(
263
372
  blocClass: B,
264
373
  id: BlocInstanceId,
265
374
  options: GetBlocOptions<InstanceType<B>> = {},
@@ -270,6 +379,9 @@ export class Blac {
270
379
  newBloc.props = props || null;
271
380
  newBloc._updateId(id);
272
381
 
382
+ // Set up disposal handler to break circular dependency
383
+ newBloc._setDisposalHandler((bloc) => this.disposeBloc(bloc));
384
+
273
385
  if (newBloc.isIsolated) {
274
386
  this.registerIsolatedBlocInstance(newBloc);
275
387
  return newBloc as InstanceType<B>;
@@ -280,8 +392,8 @@ export class Blac {
280
392
  }
281
393
 
282
394
 
283
- activateBloc = (bloc: BlocBase<any>): void => {
284
- const base = bloc.constructor as unknown as BlocConstructor<BlocBase<any>>;
395
+ activateBloc = (bloc: BlocBase<unknown>): void => {
396
+ const base = bloc.constructor as unknown as BlocConstructor<BlocBase<unknown>>;
285
397
  const isIsolated = bloc.isIsolated;
286
398
 
287
399
  let found = isIsolated ? this.findIsolatedBlocInstance(base, bloc._id) : this.findRegisteredBlocInstance(base, bloc._id);
@@ -296,7 +408,7 @@ export class Blac {
296
408
  this.registerBlocInstance(bloc);
297
409
  }
298
410
  };
299
- static activateBloc = Blac.instance.activateBloc;
411
+ static get activateBloc() { return Blac.instance.activateBloc; }
300
412
 
301
413
  /**
302
414
  * Gets or creates a bloc instance based on the provided class and options.
@@ -309,7 +421,7 @@ export class Blac {
309
421
  * - instanceRef: Optional reference string for the instance
310
422
  * @returns The bloc instance
311
423
  */
312
- getBloc = <B extends BlocConstructor<BlocBase<any>>>(
424
+ getBloc = <B extends BlocConstructor<BlocBase<unknown>>>(
313
425
  blocClass: B,
314
426
  options: GetBlocOptions<InstanceType<B>> = {},
315
427
  ): InstanceType<B> => {
@@ -348,7 +460,7 @@ export class Blac {
348
460
  this.log(`[${blocClass.name}:${String(blocId)}] (getBloc) No existing instance found. Creating new one.`, options, bloc);
349
461
  return bloc;
350
462
  };
351
- static getBloc = Blac.instance.getBloc;
463
+ static get getBloc() { return Blac.instance.getBloc; }
352
464
 
353
465
  /**
354
466
  * Gets a bloc instance or throws an error if it doesn't exist
@@ -378,7 +490,7 @@ export class Blac {
378
490
  }
379
491
  throw new Error(`Bloc ${blocClass.name} not found`);
380
492
  };
381
- static getBlocOrThrow = Blac.instance.getBlocOrThrow;
493
+ static get getBlocOrThrow() { return Blac.instance.getBlocOrThrow; }
382
494
 
383
495
  /**
384
496
  * Gets all instances of a specific bloc class
@@ -413,4 +525,83 @@ export class Blac {
413
525
 
414
526
  return results;
415
527
  };
528
+
529
+ /**
530
+ * Disposes all keep-alive blocs of a specific type
531
+ * @param blocClass - The bloc class to dispose
532
+ */
533
+ disposeKeepAliveBlocs = <B extends BlocConstructor<unknown>>(
534
+ blocClass?: B
535
+ ): void => {
536
+ const toDispose: BlocBase<unknown>[] = [];
537
+
538
+ for (const bloc of this.keepAliveBlocs) {
539
+ if (!blocClass || bloc.constructor === blocClass) {
540
+ toDispose.push(bloc);
541
+ }
542
+ }
543
+
544
+ toDispose.forEach(bloc => bloc._dispose());
545
+ };
546
+ static get disposeKeepAliveBlocs() { return Blac.instance.disposeKeepAliveBlocs; }
547
+
548
+ /**
549
+ * Disposes all blocs matching a pattern
550
+ * @param predicate - Function to test each bloc for disposal
551
+ */
552
+ disposeBlocs = (predicate: (bloc: BlocBase<unknown>) => boolean): void => {
553
+ const toDispose: BlocBase<unknown>[] = [];
554
+
555
+ // Check registered blocs
556
+ for (const bloc of this.blocInstanceMap.values()) {
557
+ if (predicate(bloc)) {
558
+ toDispose.push(bloc);
559
+ }
560
+ }
561
+
562
+ // Check isolated blocs
563
+ for (const blocs of this.isolatedBlocMap.values()) {
564
+ for (const bloc of blocs) {
565
+ if (predicate(bloc)) {
566
+ toDispose.push(bloc);
567
+ }
568
+ }
569
+ }
570
+
571
+ toDispose.forEach(bloc => bloc._dispose());
572
+ };
573
+ static get disposeBlocs() { return Blac.instance.disposeBlocs; }
574
+
575
+ /**
576
+ * Gets memory usage statistics for debugging
577
+ */
578
+ getMemoryStats = () => {
579
+ return {
580
+ totalBlocs: this.uidRegistry.size,
581
+ registeredBlocs: this.blocInstanceMap.size,
582
+ isolatedBlocs: Array.from(this.isolatedBlocMap.values()).reduce((sum, arr) => sum + arr.length, 0),
583
+ keepAliveBlocs: this.keepAliveBlocs.size,
584
+ isolatedBlocTypes: this.isolatedBlocMap.size,
585
+ };
586
+ };
587
+ static get getMemoryStats() { return Blac.instance.getMemoryStats; }
588
+
589
+ /**
590
+ * Validates consumer references and cleans up orphaned consumers
591
+ */
592
+ validateConsumers = (): void => {
593
+ for (const bloc of this.uidRegistry.values()) {
594
+ // This would need to be called periodically to clean up orphaned consumers
595
+ // Implementation depends on how we want to handle consumer validation
596
+ if (bloc._consumers.size === 0 && !bloc._keepAlive) {
597
+ // Schedule disposal for blocs with no consumers
598
+ setTimeout(() => {
599
+ if (bloc._consumers.size === 0 && !bloc._keepAlive) {
600
+ bloc._dispose();
601
+ }
602
+ }, 1000); // Give a grace period
603
+ }
604
+ }
605
+ };
606
+ static get validateConsumers() { return Blac.instance.validateConsumers; }
416
607
  }
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
  }