@blac/core 2.0.0-rc-6 → 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,15 +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);
545
+
546
+ if (!bloc) {
547
+ throw new Error(
548
+ `[getBloc] Failed to create bloc instance for ${blocClass.name}. This should never happen.`,
549
+ );
550
+ }
551
+
491
552
  return bloc;
492
553
  };
493
- static get getBloc() { return Blac.instance.getBloc; }
554
+ static get getBloc() {
555
+ return Blac.instance.getBloc;
556
+ }
494
557
 
495
558
  /**
496
559
  * Gets a bloc instance or throws an error if it doesn't exist
@@ -520,7 +583,9 @@ export class Blac {
520
583
  }
521
584
  throw new Error(`Bloc ${blocClass.name} not found`);
522
585
  };
523
- static get getBlocOrThrow() { return Blac.instance.getBlocOrThrow; }
586
+ static get getBlocOrThrow() {
587
+ return Blac.instance.getBlocOrThrow;
588
+ }
524
589
 
525
590
  /**
526
591
  * Gets all instances of a specific bloc class
@@ -540,7 +605,8 @@ export class Blac {
540
605
 
541
606
  // Search non-isolated blocs
542
607
  this.blocInstanceMap.forEach((blocInstance) => {
543
- if (blocInstance.constructor === blocClass) { // Strict constructor check
608
+ if (blocInstance.constructor === blocClass) {
609
+ // Strict constructor check
544
610
  results.push(blocInstance as InstanceType<B>);
545
611
  }
546
612
  });
@@ -549,7 +615,7 @@ export class Blac {
549
615
  if (options.searchIsolated !== false) {
550
616
  const isolatedBlocs = this.isolatedBlocMap.get(blocClass);
551
617
  if (isolatedBlocs) {
552
- results.push(...isolatedBlocs.map(bloc => bloc as InstanceType<B>));
618
+ results.push(...isolatedBlocs.map((bloc) => bloc as InstanceType<B>));
553
619
  }
554
620
  }
555
621
 
@@ -561,19 +627,21 @@ export class Blac {
561
627
  * @param blocClass - The bloc class to dispose
562
628
  */
563
629
  disposeKeepAliveBlocs = <B extends BlocConstructor<any>>(
564
- blocClass?: B
630
+ blocClass?: B,
565
631
  ): void => {
566
632
  const toDispose: BlocBase<unknown>[] = [];
567
-
633
+
568
634
  for (const bloc of this.keepAliveBlocs) {
569
635
  if (!blocClass || bloc.constructor === blocClass) {
570
636
  toDispose.push(bloc);
571
637
  }
572
638
  }
573
-
574
- toDispose.forEach(bloc => bloc._dispose());
639
+
640
+ toDispose.forEach((bloc) => bloc._dispose());
575
641
  };
576
- static get disposeKeepAliveBlocs() { return Blac.instance.disposeKeepAliveBlocs; }
642
+ static get disposeKeepAliveBlocs() {
643
+ return Blac.instance.disposeKeepAliveBlocs;
644
+ }
577
645
 
578
646
  /**
579
647
  * Disposes all blocs matching a pattern
@@ -581,14 +649,14 @@ export class Blac {
581
649
  */
582
650
  disposeBlocs = (predicate: (bloc: BlocBase<unknown>) => boolean): void => {
583
651
  const toDispose: BlocBase<unknown>[] = [];
584
-
652
+
585
653
  // Check registered blocs
586
654
  for (const bloc of this.blocInstanceMap.values()) {
587
655
  if (predicate(bloc)) {
588
656
  toDispose.push(bloc);
589
657
  }
590
658
  }
591
-
659
+
592
660
  // Check isolated blocs
593
661
  for (const blocs of this.isolatedBlocMap.values()) {
594
662
  for (const bloc of blocs) {
@@ -597,10 +665,12 @@ export class Blac {
597
665
  }
598
666
  }
599
667
  }
600
-
601
- toDispose.forEach(bloc => bloc._dispose());
668
+
669
+ toDispose.forEach((bloc) => bloc._dispose());
602
670
  };
603
- static get disposeBlocs() { return Blac.instance.disposeBlocs; }
671
+ static get disposeBlocs() {
672
+ return Blac.instance.disposeBlocs;
673
+ }
604
674
 
605
675
  /**
606
676
  * Gets memory usage statistics for debugging
@@ -609,12 +679,17 @@ export class Blac {
609
679
  return {
610
680
  totalBlocs: this.uidRegistry.size,
611
681
  registeredBlocs: this.blocInstanceMap.size,
612
- 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
+ ),
613
686
  keepAliveBlocs: this.keepAliveBlocs.size,
614
687
  isolatedBlocTypes: this.isolatedBlocMap.size,
615
688
  };
616
689
  };
617
- static get getMemoryStats() { return Blac.instance.getMemoryStats; }
690
+ static get getMemoryStats() {
691
+ return Blac.instance.getMemoryStats;
692
+ }
618
693
 
619
694
  /**
620
695
  * Validates consumer references and cleans up orphaned consumers
@@ -623,18 +698,30 @@ export class Blac {
623
698
  for (const bloc of this.uidRegistry.values()) {
624
699
  // Validate consumers using the bloc's own validation method
625
700
  bloc._validateConsumers();
626
-
701
+
627
702
  // Check if bloc should be disposed after validation
628
- 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
+ ) {
629
709
  // Schedule disposal for blocs with no consumers
630
710
  setTimeout(() => {
631
711
  // Double-check conditions before disposal
632
- 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
+ ) {
633
718
  this.disposeBloc(bloc);
634
719
  }
635
720
  }, 1000); // Give a grace period
636
721
  }
637
722
  }
638
723
  };
639
- static get validateConsumers() { return Blac.instance.validateConsumers; }
724
+ static get validateConsumers() {
725
+ return Blac.instance.validateConsumers;
726
+ }
640
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);