@blac/core 2.0.0-rc-7 → 2.0.0-rc-8

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,13 +1,18 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- // TODO: Remove this eslint disable once any types are properly replaced
3
- import { BlocBase, BlocInstanceId } from "./BlocBase";
2
+ // TODO: The 'any' types in this file are necessary for proper type inference in complex generic scenarios.
3
+ // Specifically:
4
+ // 1. BlocConstructor<any> in Maps - allows any bloc type to be stored while maintaining type safety in usage
5
+ // 2. Type assertions for _disposalState - private property access across inheritance hierarchy
6
+ // 3. Constructor argument types - enables flexible bloc instantiation patterns
7
+ // These 'any' usages are carefully controlled and don't compromise runtime type safety.
8
+ import { BlocBase, BlocInstanceId } from './BlocBase';
4
9
  import {
5
- BlocBaseAbstract,
6
- BlocConstructor,
7
- BlocHookDependencyArrayFn,
8
- BlocState,
9
- InferPropsFromGeneric
10
- } from "./types";
10
+ BlocBaseAbstract,
11
+ BlocConstructor,
12
+ BlocHookDependencyArrayFn,
13
+ BlocState,
14
+ InferPropsFromGeneric,
15
+ } from './types';
11
16
 
12
17
  /**
13
18
  * Configuration options for the Blac instance
@@ -43,7 +48,9 @@ class SingletonBlacManager implements BlacInstanceManager {
43
48
 
44
49
  getInstance(): Blac {
45
50
  if (!SingletonBlacManager._instance) {
46
- SingletonBlacManager._instance = new Blac({ __unsafe_ignore_singleton: true });
51
+ SingletonBlacManager._instance = new Blac({
52
+ __unsafe_ignore_singleton: true,
53
+ });
47
54
  }
48
55
  return SingletonBlacManager._instance;
49
56
  }
@@ -54,8 +61,10 @@ class SingletonBlacManager implements BlacInstanceManager {
54
61
 
55
62
  resetInstance(): void {
56
63
  const oldInstance = SingletonBlacManager._instance;
57
- SingletonBlacManager._instance = new Blac({ __unsafe_ignore_singleton: true });
58
-
64
+ SingletonBlacManager._instance = new Blac({
65
+ __unsafe_ignore_singleton: true,
66
+ });
67
+
59
68
  // Transfer any keep-alive blocs to the new instance
60
69
  if (oldInstance) {
61
70
  for (const bloc of oldInstance.keepAliveBlocs) {
@@ -82,7 +91,7 @@ export function setBlacInstanceManager(manager: BlacInstanceManager): void {
82
91
  * Main Blac class that manages the state management system.
83
92
  * Can work with singleton pattern or dependency injection.
84
93
  * Handles bloc lifecycle, and instance tracking.
85
- *
94
+ *
86
95
  * Key responsibilities:
87
96
  * - Managing bloc instances (creation, disposal, lookup)
88
97
  * - Handling isolated and non-isolated blocs
@@ -101,6 +110,10 @@ export class Blac {
101
110
  /** Map storing all registered bloc instances by their class name and ID */
102
111
  blocInstanceMap: Map<string, BlocBase<unknown>> = new Map();
103
112
  /** Map storing isolated bloc instances grouped by their constructor */
113
+ // TODO: BlocConstructor<any> is required here for type inference to work correctly.
114
+ // Using BlocConstructor<BlocBase<unknown>> would break type inference when storing
115
+ // different bloc types in the same map. The 'any' allows proper polymorphic storage
116
+ // while maintaining type safety at usage sites through the BlocConstructor constraint.
104
117
  isolatedBlocMap: Map<BlocConstructor<any>, BlocBase<unknown>[]> = new Map();
105
118
  /** Map for O(1) lookup of isolated blocs by UID */
106
119
  private isolatedBlocIndex: Map<string, BlocBase<unknown>> = new Map();
@@ -125,15 +138,21 @@ export class Blac {
125
138
 
126
139
  /** Flag to enable/disable logging */
127
140
  static enableLog = false;
141
+ static logLevel: 'warn' | 'log' = 'warn';
128
142
 
129
143
  /**
130
144
  * Logs messages to console when logging is enabled
131
145
  * @param args - Arguments to log
132
146
  */
133
147
  log = (...args: unknown[]) => {
134
- if (Blac.enableLog) console.warn(`☢️ [Blac ${this.createdAt.toString()}]`, ...args);
148
+ if (Blac.enableLog && Blac.logLevel === 'warn')
149
+ console.warn(`☢️ [Blac ${this.createdAt.toString()}]`, ...args);
150
+ if (Blac.enableLog && Blac.logLevel === 'log')
151
+ console.log(`☢️ [Blac ${this.createdAt.toString()}]`, ...args);
135
152
  };
136
- static get log() { return Blac.instance.log; }
153
+ static get log() {
154
+ return Blac.instance.log;
155
+ }
137
156
 
138
157
  /**
139
158
  * Gets the current Blac instance
@@ -143,7 +162,6 @@ export class Blac {
143
162
  return instanceManager.getInstance();
144
163
  }
145
164
 
146
-
147
165
  /**
148
166
  * Logs a warning message
149
167
  * @param message - Warning message
@@ -151,10 +169,16 @@ export class Blac {
151
169
  */
152
170
  warn = (message: string, ...args: unknown[]) => {
153
171
  if (Blac.enableLog) {
154
- console.warn(`🚨 [Blac ${String(Blac.instance.createdAt)}]`, message, ...args);
172
+ console.warn(
173
+ `🚨 [Blac ${String(Blac.instance.createdAt)}]`,
174
+ message,
175
+ ...args,
176
+ );
155
177
  }
156
178
  };
157
- static get warn() { return Blac.instance.warn; }
179
+ static get warn() {
180
+ return Blac.instance.warn;
181
+ }
158
182
  /**
159
183
  * Logs an error message
160
184
  * @param message - Error message
@@ -162,17 +186,23 @@ export class Blac {
162
186
  */
163
187
  error = (message: string, ...args: unknown[]) => {
164
188
  if (Blac.enableLog) {
165
- console.error(`🚨 [Blac ${String(Blac.instance.createdAt)}]`, message, ...args);
189
+ console.error(
190
+ `🚨 [Blac ${String(Blac.instance.createdAt)}]`,
191
+ message,
192
+ ...args,
193
+ );
166
194
  }
167
195
  };
168
- static get error() { return Blac.instance.error; }
196
+ static get error() {
197
+ return Blac.instance.error;
198
+ }
169
199
 
170
200
  /**
171
201
  * Resets the Blac instance to a new one, disposing non-keepAlive blocs
172
202
  * from the old instance.
173
203
  */
174
204
  resetInstance = (): void => {
175
- this.log("Reset Blac instance");
205
+ this.log('Reset Blac instance');
176
206
 
177
207
  // Create snapshots to avoid concurrent modification issues
178
208
  const oldBlocInstanceMap = new Map(this.blocInstanceMap);
@@ -181,6 +211,7 @@ export class Blac {
181
211
  // Dispose non-keepAlive blocs from the current instance
182
212
  // Use disposeBloc method to ensure proper cleanup
183
213
  oldBlocInstanceMap.forEach((bloc) => {
214
+ // TODO: Type assertion for private property access (see explanation above)
184
215
  if (!bloc._keepAlive && (bloc as any)._disposalState === 'active') {
185
216
  this.disposeBloc(bloc);
186
217
  }
@@ -188,6 +219,7 @@ export class Blac {
188
219
 
189
220
  oldIsolatedBlocMap.forEach((blocArray) => {
190
221
  blocArray.forEach((bloc) => {
222
+ // TODO: Type assertion for private property access (see explanation above)
191
223
  if (!bloc._keepAlive && (bloc as any)._disposalState === 'active') {
192
224
  this.disposeBloc(bloc);
193
225
  }
@@ -201,7 +233,7 @@ export class Blac {
201
233
 
202
234
  // Use instance manager to reset
203
235
  instanceManager.resetInstance();
204
- }
236
+ };
205
237
  static resetInstance = (): void => {
206
238
  instanceManager.resetInstance();
207
239
  };
@@ -212,18 +244,26 @@ export class Blac {
212
244
  */
213
245
  disposeBloc = (bloc: BlocBase<unknown>): void => {
214
246
  // Check if bloc is already disposed to prevent double disposal
247
+ // TODO: Type assertion needed to access private _disposalState property from external class.
248
+ // This is safe because we know BlocBase has this property, but TypeScript can't verify
249
+ // private property access across class boundaries. Alternative would be to make
250
+ // _disposalState protected, but that would expose internal implementation details.
215
251
  if ((bloc as any)._disposalState !== 'active') {
216
- this.log(`[${bloc._name}:${String(bloc._id)}] disposeBloc called on already disposed bloc`);
252
+ this.log(
253
+ `[${bloc._name}:${String(bloc._id)}] disposeBloc called on already disposed bloc`,
254
+ );
217
255
  return;
218
256
  }
219
257
 
220
258
  const base = bloc.constructor as unknown as BlocBaseAbstract;
221
259
  const key = this.createBlocInstanceMapKey(bloc._name, bloc._id);
222
- this.log(`[${bloc._name}:${String(bloc._id)}] disposeBloc called. Isolated: ${String(base.isolated)}`);
260
+ this.log(
261
+ `[${bloc._name}:${String(bloc._id)}] disposeBloc called. Isolated: ${String(base.isolated)}`,
262
+ );
223
263
 
224
264
  // First dispose the bloc to prevent further operations
225
265
  bloc._dispose();
226
-
266
+
227
267
  // Then clean up from registries
228
268
  if (base.isolated) {
229
269
  this.unregisterIsolatedBlocInstance(bloc);
@@ -231,10 +271,12 @@ export class Blac {
231
271
  } else {
232
272
  this.unregisterBlocInstance(bloc);
233
273
  }
234
-
235
- this.log('dispatched bloc', bloc)
274
+
275
+ this.log('dispatched bloc', bloc);
236
276
  };
237
- static get disposeBloc() { return Blac.instance.disposeBloc; }
277
+ static get disposeBloc() {
278
+ return Blac.instance.disposeBloc;
279
+ }
238
280
 
239
281
  /**
240
282
  * Creates a unique key for a bloc instance in the map based on the bloc class name and instance ID
@@ -253,10 +295,10 @@ export class Blac {
253
295
  unregisterBlocInstance(bloc: BlocBase<unknown>): void {
254
296
  const key = this.createBlocInstanceMapKey(bloc._name, bloc._id);
255
297
  this.blocInstanceMap.delete(key);
256
-
298
+
257
299
  // Clean up UID tracking
258
300
  this.uidRegistry.delete(bloc.uid);
259
-
301
+
260
302
  // Remove from keep-alive set
261
303
  this.keepAliveBlocs.delete(bloc);
262
304
  }
@@ -268,10 +310,10 @@ export class Blac {
268
310
  registerBlocInstance(bloc: BlocBase<unknown>): void {
269
311
  const key = this.createBlocInstanceMapKey(bloc._name, bloc._id);
270
312
  this.blocInstanceMap.set(key, bloc);
271
-
313
+
272
314
  // Track UID for cleanup
273
315
  this.uidRegistry.set(bloc.uid, bloc);
274
-
316
+
275
317
  // Track keep-alive blocs
276
318
  if (bloc._keepAlive) {
277
319
  this.keepAliveBlocs.add(bloc);
@@ -293,7 +335,7 @@ export class Blac {
293
335
 
294
336
  const key = this.createBlocInstanceMapKey(blocClass.name, id);
295
337
  const found = this.blocInstanceMap.get(key) as InstanceType<B> | undefined;
296
- return found
338
+ return found;
297
339
  }
298
340
 
299
341
  /**
@@ -308,13 +350,13 @@ export class Blac {
308
350
  } else {
309
351
  this.isolatedBlocMap.set(blocClass, [bloc]);
310
352
  }
311
-
353
+
312
354
  // Add to isolated index for O(1) lookups
313
355
  this.isolatedBlocIndex.set(bloc.uid, bloc);
314
-
356
+
315
357
  // Track UID for cleanup
316
358
  this.uidRegistry.set(bloc.uid, bloc);
317
-
359
+
318
360
  // Track keep-alive blocs
319
361
  if (bloc._keepAlive) {
320
362
  this.keepAliveBlocs.add(bloc);
@@ -328,10 +370,10 @@ export class Blac {
328
370
  unregisterIsolatedBlocInstance(bloc: BlocBase<unknown>): void {
329
371
  const blocClass = bloc.constructor;
330
372
  const blocs = this.isolatedBlocMap.get(blocClass as BlocConstructor<any>);
331
-
373
+
332
374
  // Ensure both data structures are synchronized
333
375
  let wasRemoved = false;
334
-
376
+
335
377
  if (blocs) {
336
378
  const index = blocs.findIndex((b) => b.uid === bloc.uid);
337
379
  if (index !== -1) {
@@ -343,21 +385,21 @@ export class Blac {
343
385
  this.isolatedBlocMap.delete(blocClass as BlocConstructor<any>);
344
386
  }
345
387
  }
346
-
388
+
347
389
  // Always try to remove from isolated index, even if not found in map
348
390
  const wasInIndex = this.isolatedBlocIndex.delete(bloc.uid);
349
-
391
+
350
392
  // Clean up UID tracking
351
393
  this.uidRegistry.delete(bloc.uid);
352
-
394
+
353
395
  // Remove from keep-alive set
354
396
  this.keepAliveBlocs.delete(bloc);
355
-
397
+
356
398
  // Log inconsistency for debugging
357
399
  if (wasRemoved !== wasInIndex) {
358
400
  this.warn(
359
401
  `[Blac] Inconsistent state detected during isolated bloc cleanup for ${bloc._name}:${bloc.uid}. ` +
360
- `Map removal: ${wasRemoved}, Index removal: ${wasInIndex}`
402
+ `Map removal: ${wasRemoved}, Index removal: ${wasInIndex}`,
361
403
  );
362
404
  }
363
405
  }
@@ -377,7 +419,9 @@ export class Blac {
377
419
  return undefined;
378
420
  }
379
421
  // Find the specific bloc by ID within the isolated array
380
- const found = blocs.find((b) => b._id === id) as InstanceType<B> | undefined;
422
+ const found = blocs.find((b) => b._id === id) as
423
+ | InstanceType<B>
424
+ | undefined;
381
425
  return found;
382
426
  }
383
427
 
@@ -385,7 +429,7 @@ export class Blac {
385
429
  * Finds an isolated bloc instance by UID (O(1) lookup)
386
430
  */
387
431
  findIsolatedBlocInstanceByUid<B extends BlocBase<unknown>>(
388
- uid: string
432
+ uid: string,
389
433
  ): B | undefined {
390
434
  return this.isolatedBlocIndex.get(uid) as B | undefined;
391
435
  }
@@ -421,29 +465,42 @@ export class Blac {
421
465
  return newBloc;
422
466
  }
423
467
 
424
-
425
468
  activateBloc = (bloc: BlocBase<unknown>): void => {
469
+ // Don't activate disposed blocs
470
+ if ((bloc as any)._disposalState !== 'active') {
471
+ this.log(
472
+ `[${bloc._name}:${String(bloc._id)}] activateBloc called on disposed bloc. Ignoring.`,
473
+ );
474
+ return;
475
+ }
476
+
426
477
  const base = bloc.constructor as unknown as BlocConstructor<BlocBase<any>>;
427
478
  const isIsolated = bloc.isIsolated;
428
479
 
429
- let found = isIsolated ? this.findIsolatedBlocInstance(base, bloc._id) : this.findRegisteredBlocInstance(base, bloc._id);
480
+ let found = isIsolated
481
+ ? this.findIsolatedBlocInstance(base, bloc._id)
482
+ : this.findRegisteredBlocInstance(base, bloc._id);
430
483
  if (found) {
431
484
  return;
432
485
  }
433
486
 
434
- this.log(`[${bloc._name}:${String(bloc._id)}] activateBloc called. Isolated: ${String(bloc.isIsolated)}`);
487
+ this.log(
488
+ `[${bloc._name}:${String(bloc._id)}] activateBloc called. Isolated: ${String(bloc.isIsolated)}`,
489
+ );
435
490
  if (bloc.isIsolated) {
436
491
  this.registerIsolatedBlocInstance(bloc);
437
492
  } else {
438
493
  this.registerBlocInstance(bloc);
439
494
  }
440
495
  };
441
- static get activateBloc() { return Blac.instance.activateBloc; }
496
+ static get activateBloc() {
497
+ return Blac.instance.activateBloc;
498
+ }
442
499
 
443
500
  /**
444
501
  * Gets or creates a bloc instance based on the provided class and options.
445
502
  * If a bloc with the given ID exists, it will be returned. Otherwise, a new instance will be created.
446
- *
503
+ *
447
504
  * @param blocClass - The bloc class to get or create
448
505
  * @param options - Options including:
449
506
  * - id: The instance ID (defaults to class name if not provided)
@@ -459,11 +516,9 @@ export class Blac {
459
516
  const base = blocClass as unknown as BlocBaseAbstract;
460
517
  const blocId = id ?? blocClass.name;
461
518
 
462
-
463
519
  if (base.isolated) {
464
- const isolatedBloc = this.findIsolatedBlocInstance<B>(blocClass, blocId)
520
+ const isolatedBloc = this.findIsolatedBlocInstance<B>(blocClass, blocId);
465
521
  if (isolatedBloc) {
466
- this.log(`[${blocClass.name}:${String(blocId)}] (getBloc) Found existing isolated instance.`, options);
467
522
  return isolatedBloc;
468
523
  } else {
469
524
  if (options.throwIfNotFound) {
@@ -471,10 +526,9 @@ export class Blac {
471
526
  }
472
527
  }
473
528
  } else {
474
- const registeredBloc = this.findRegisteredBlocInstance(blocClass, blocId)
529
+ const registeredBloc = this.findRegisteredBlocInstance(blocClass, blocId);
475
530
  if (registeredBloc) {
476
- this.log(`[${blocClass.name}:${String(blocId)}] (getBloc) Found existing registered instance.`, options);
477
- return registeredBloc
531
+ return registeredBloc;
478
532
  } else {
479
533
  if (options.throwIfNotFound) {
480
534
  throw new Error(`Registered bloc ${blocClass.name} not found`);
@@ -482,20 +536,24 @@ export class Blac {
482
536
  }
483
537
  }
484
538
 
485
- const bloc = this.createNewBlocInstance(
486
- blocClass,
487
- blocId,
539
+ const bloc = this.createNewBlocInstance(blocClass, blocId, options);
540
+ this.log(
541
+ `[${blocClass.name}:${String(blocId)}] (getBloc) No existing instance found. Creating new one.`,
488
542
  options,
543
+ bloc,
489
544
  );
490
- this.log(`[${blocClass.name}:${String(blocId)}] (getBloc) No existing instance found. Creating new one.`, options, bloc);
491
-
545
+
492
546
  if (!bloc) {
493
- throw new Error(`[getBloc] Failed to create bloc instance for ${blocClass.name}. This should never happen.`);
547
+ throw new Error(
548
+ `[getBloc] Failed to create bloc instance for ${blocClass.name}. This should never happen.`,
549
+ );
494
550
  }
495
-
551
+
496
552
  return bloc;
497
553
  };
498
- static get getBloc() { return Blac.instance.getBloc; }
554
+ static get getBloc() {
555
+ return Blac.instance.getBloc;
556
+ }
499
557
 
500
558
  /**
501
559
  * Gets a bloc instance or throws an error if it doesn't exist
@@ -525,7 +583,9 @@ export class Blac {
525
583
  }
526
584
  throw new Error(`Bloc ${blocClass.name} not found`);
527
585
  };
528
- static get getBlocOrThrow() { return Blac.instance.getBlocOrThrow; }
586
+ static get getBlocOrThrow() {
587
+ return Blac.instance.getBlocOrThrow;
588
+ }
529
589
 
530
590
  /**
531
591
  * Gets all instances of a specific bloc class
@@ -545,7 +605,8 @@ export class Blac {
545
605
 
546
606
  // Search non-isolated blocs
547
607
  this.blocInstanceMap.forEach((blocInstance) => {
548
- if (blocInstance.constructor === blocClass) { // Strict constructor check
608
+ if (blocInstance.constructor === blocClass) {
609
+ // Strict constructor check
549
610
  results.push(blocInstance as InstanceType<B>);
550
611
  }
551
612
  });
@@ -554,7 +615,7 @@ export class Blac {
554
615
  if (options.searchIsolated !== false) {
555
616
  const isolatedBlocs = this.isolatedBlocMap.get(blocClass);
556
617
  if (isolatedBlocs) {
557
- results.push(...isolatedBlocs.map(bloc => bloc as InstanceType<B>));
618
+ results.push(...isolatedBlocs.map((bloc) => bloc as InstanceType<B>));
558
619
  }
559
620
  }
560
621
 
@@ -566,19 +627,21 @@ export class Blac {
566
627
  * @param blocClass - The bloc class to dispose
567
628
  */
568
629
  disposeKeepAliveBlocs = <B extends BlocConstructor<any>>(
569
- blocClass?: B
630
+ blocClass?: B,
570
631
  ): void => {
571
632
  const toDispose: BlocBase<unknown>[] = [];
572
-
633
+
573
634
  for (const bloc of this.keepAliveBlocs) {
574
635
  if (!blocClass || bloc.constructor === blocClass) {
575
636
  toDispose.push(bloc);
576
637
  }
577
638
  }
578
-
579
- toDispose.forEach(bloc => bloc._dispose());
639
+
640
+ toDispose.forEach((bloc) => bloc._dispose());
580
641
  };
581
- static get disposeKeepAliveBlocs() { return Blac.instance.disposeKeepAliveBlocs; }
642
+ static get disposeKeepAliveBlocs() {
643
+ return Blac.instance.disposeKeepAliveBlocs;
644
+ }
582
645
 
583
646
  /**
584
647
  * Disposes all blocs matching a pattern
@@ -586,14 +649,14 @@ export class Blac {
586
649
  */
587
650
  disposeBlocs = (predicate: (bloc: BlocBase<unknown>) => boolean): void => {
588
651
  const toDispose: BlocBase<unknown>[] = [];
589
-
652
+
590
653
  // Check registered blocs
591
654
  for (const bloc of this.blocInstanceMap.values()) {
592
655
  if (predicate(bloc)) {
593
656
  toDispose.push(bloc);
594
657
  }
595
658
  }
596
-
659
+
597
660
  // Check isolated blocs
598
661
  for (const blocs of this.isolatedBlocMap.values()) {
599
662
  for (const bloc of blocs) {
@@ -602,10 +665,12 @@ export class Blac {
602
665
  }
603
666
  }
604
667
  }
605
-
606
- toDispose.forEach(bloc => bloc._dispose());
668
+
669
+ toDispose.forEach((bloc) => bloc._dispose());
607
670
  };
608
- static get disposeBlocs() { return Blac.instance.disposeBlocs; }
671
+ static get disposeBlocs() {
672
+ return Blac.instance.disposeBlocs;
673
+ }
609
674
 
610
675
  /**
611
676
  * Gets memory usage statistics for debugging
@@ -614,12 +679,17 @@ export class Blac {
614
679
  return {
615
680
  totalBlocs: this.uidRegistry.size,
616
681
  registeredBlocs: this.blocInstanceMap.size,
617
- isolatedBlocs: Array.from(this.isolatedBlocMap.values()).reduce((sum, arr) => sum + arr.length, 0),
682
+ isolatedBlocs: Array.from(this.isolatedBlocMap.values()).reduce(
683
+ (sum, arr) => sum + arr.length,
684
+ 0,
685
+ ),
618
686
  keepAliveBlocs: this.keepAliveBlocs.size,
619
687
  isolatedBlocTypes: this.isolatedBlocMap.size,
620
688
  };
621
689
  };
622
- static get getMemoryStats() { return Blac.instance.getMemoryStats; }
690
+ static get getMemoryStats() {
691
+ return Blac.instance.getMemoryStats;
692
+ }
623
693
 
624
694
  /**
625
695
  * Validates consumer references and cleans up orphaned consumers
@@ -628,18 +698,30 @@ export class Blac {
628
698
  for (const bloc of this.uidRegistry.values()) {
629
699
  // Validate consumers using the bloc's own validation method
630
700
  bloc._validateConsumers();
631
-
701
+
632
702
  // Check if bloc should be disposed after validation
633
- if (bloc._consumers.size === 0 && !bloc._keepAlive && (bloc as any)._disposalState === 'active') {
703
+ // TODO: Type assertion for private property access (see explanation above)
704
+ if (
705
+ bloc._consumers.size === 0 &&
706
+ !bloc._keepAlive &&
707
+ (bloc as any)._disposalState === 'active'
708
+ ) {
634
709
  // Schedule disposal for blocs with no consumers
635
710
  setTimeout(() => {
636
711
  // Double-check conditions before disposal
637
- if (bloc._consumers.size === 0 && !bloc._keepAlive && (bloc as any)._disposalState === 'active') {
712
+ // TODO: Type assertion for private property access (see explanation above)
713
+ if (
714
+ bloc._consumers.size === 0 &&
715
+ !bloc._keepAlive &&
716
+ (bloc as any)._disposalState === 'active'
717
+ ) {
638
718
  this.disposeBloc(bloc);
639
719
  }
640
720
  }, 1000); // Give a grace period
641
721
  }
642
722
  }
643
723
  };
644
- static get validateConsumers() { return Blac.instance.validateConsumers; }
724
+ static get validateConsumers() {
725
+ return Blac.instance.validateConsumers;
726
+ }
645
727
  }
@@ -78,7 +78,11 @@ export class BlacObservable<S = unknown> {
78
78
 
79
79
  if (this.size === 0) {
80
80
  Blac.log('BlacObservable.unsubscribe: No observers left.', this.bloc);
81
- // The bloc will handle its own disposal through consumer management
81
+ // Check if bloc should be disposed when both observers and consumers are gone
82
+ if (this.bloc._consumers.size === 0 && !this.bloc._keepAlive && (this.bloc as any)._disposalState === 'active') {
83
+ Blac.log(`[${this.bloc._name}:${this.bloc._id}] No observers or consumers left. Scheduling disposal.`);
84
+ (this.bloc as any)._scheduleDisposal();
85
+ }
82
86
  }
83
87
  }
84
88
 
@@ -94,30 +98,29 @@ export class BlacObservable<S = unknown> {
94
98
 
95
99
  if (observer.dependencyArray) {
96
100
  const lastDependencyCheck = observer.lastState;
97
- const newDependencyCheck = observer.dependencyArray(newState);
101
+ const newDependencyCheck = observer.dependencyArray(newState, oldState);
98
102
 
99
- // If this is the first time (no lastState), always update
103
+ // If this is the first time (no lastState), trigger initial render
100
104
  if (!lastDependencyCheck) {
101
105
  shouldUpdate = true;
102
106
  } else {
103
- // Compare dependency arrays for changes
107
+ // Compare two-array dependency structure: [stateArray, classArray]
104
108
  if (lastDependencyCheck.length !== newDependencyCheck.length) {
105
109
  shouldUpdate = true;
106
110
  } else {
107
- // Compare each part of the dependency arrays
108
- for (let o = 0; o < newDependencyCheck.length; o++) {
109
- const partNew = newDependencyCheck[o];
110
- const partOld = lastDependencyCheck[o] || [];
111
+ // Compare each array (state and class dependencies)
112
+ for (let arrayIndex = 0; arrayIndex < newDependencyCheck.length; arrayIndex++) {
113
+ const lastArray = lastDependencyCheck[arrayIndex] || [];
114
+ const newArray = newDependencyCheck[arrayIndex] || [];
111
115
 
112
- // If the part lengths are different, definitely update
113
- if (partNew.length !== partOld.length) {
116
+ if (lastArray.length !== newArray.length) {
114
117
  shouldUpdate = true;
115
118
  break;
116
119
  }
117
120
 
118
- // Compare each value in the parts
119
- for (let i = 0; i < partNew.length; i++) {
120
- if (!Object.is(partNew[i], partOld[i])) {
121
+ // Compare each dependency value using Object.is (same as React)
122
+ for (let i = 0; i < newArray.length; i++) {
123
+ if (!Object.is(lastArray[i], newArray[i])) {
121
124
  shouldUpdate = true;
122
125
  break;
123
126
  }
package/src/Bloc.ts CHANGED
@@ -15,7 +15,10 @@ export abstract class Bloc<
15
15
  // by the 'on' method's signature.
16
16
  readonly eventHandlers: Map<
17
17
  // Key: Constructor of a specific event E (where E extends A)
18
- // Using 'any[]' for constructor arguments for broader compatibility.
18
+ // TODO: 'any[]' is required for constructor arguments to allow flexible event instantiation.
19
+ // Using specific parameter types would break type inference for events with different
20
+ // constructor signatures. The 'any[]' enables polymorphic event handling while
21
+ // maintaining type safety through the generic constraint 'E extends A'.
19
22
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
23
  new (...args: any[]) => A,
21
24
  // Value: Handler function. 'event: A' is used here for the stored function type.
@@ -43,7 +46,8 @@ export abstract class Bloc<
43
46
  * The 'event' parameter in the handler will be typed to the specific eventConstructor.
44
47
  */
45
48
  protected on<E extends A>(
46
- // Using 'any[]' for constructor arguments for broader compatibility.
49
+ // TODO: 'any[]' is required for constructor arguments (see explanation above).
50
+ // This allows events with different constructor signatures to be handled uniformly.
47
51
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
48
52
  eventConstructor: new (...args: any[]) => E,
49
53
  handler: (event: E, emit: (newState: S) => void) => void | Promise<void>
@@ -107,6 +111,10 @@ export abstract class Bloc<
107
111
  */
108
112
  private async _processEvent(action: A): Promise<void> {
109
113
  // Using 'any[]' for constructor arguments for broader compatibility.
114
+ // TODO: Type assertion required to cast action.constructor to proper event constructor type.
115
+ // JavaScript's constructor property returns 'Function', but we need the specific event
116
+ // constructor type to look up handlers. This is safe because we validate the action
117
+ // extends the BlocEventConstraint interface.
110
118
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
111
119
  const eventConstructor = action.constructor as new (...args: any[]) => A;
112
120
  const handler = this.eventHandlers.get(eventConstructor);