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

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
@@ -5,7 +5,7 @@
5
5
  // 2. Type assertions for _disposalState - private property access across inheritance hierarchy
6
6
  // 3. Constructor argument types - enables flexible bloc instantiation patterns
7
7
  // These 'any' usages are carefully controlled and don't compromise runtime type safety.
8
- import { BlocBase, BlocInstanceId } from './BlocBase';
8
+ import { BlocBase, BlocInstanceId, BlocLifecycleState } from './BlocBase';
9
9
  import {
10
10
  BlocBaseAbstract,
11
11
  BlocConstructor,
@@ -29,6 +29,8 @@ export interface GetBlocOptions<B extends BlocBase<unknown>> {
29
29
  onMount?: (bloc: B) => void;
30
30
  instanceRef?: string;
31
31
  throwIfNotFound?: boolean;
32
+ /** Force creation of a new instance for isolated blocs, bypassing instance lookup */
33
+ forceNewInstance?: boolean;
32
34
  }
33
35
 
34
36
  /**
@@ -98,7 +100,6 @@ export function setBlacInstanceManager(manager: BlacInstanceManager): void {
98
100
  * - Providing logging and debugging capabilities
99
101
  */
100
102
  export class Blac {
101
- /** @deprecated Use getInstance() instead */
102
103
  static get instance(): Blac {
103
104
  return instanceManager.getInstance();
104
105
  }
@@ -110,13 +111,12 @@ export class Blac {
110
111
  /** Map storing all registered bloc instances by their class name and ID */
111
112
  blocInstanceMap: Map<string, BlocBase<unknown>> = new Map();
112
113
  /** Map storing isolated bloc instances grouped by their constructor */
113
- // TODO: BlocConstructor<any> is required here for type inference to work correctly.
114
114
  // Using BlocConstructor<BlocBase<unknown>> would break type inference when storing
115
115
  // different bloc types in the same map. The 'any' allows proper polymorphic storage
116
116
  // while maintaining type safety at usage sites through the BlocConstructor constraint.
117
117
  isolatedBlocMap: Map<BlocConstructor<any>, BlocBase<unknown>[]> = new Map();
118
118
  /** Map for O(1) lookup of isolated blocs by UID */
119
- private isolatedBlocIndex: Map<string, BlocBase<unknown>> = new Map();
119
+ isolatedBlocIndex: Map<string, BlocBase<unknown>> = new Map();
120
120
  /** Map tracking UIDs to prevent memory leaks */
121
121
  uidRegistry: Map<string, BlocBase<unknown>> = new Map();
122
122
  /** Set of keep-alive blocs for controlled cleanup */
@@ -139,16 +139,22 @@ export class Blac {
139
139
  /** Flag to enable/disable logging */
140
140
  static enableLog = false;
141
141
  static logLevel: 'warn' | 'log' = 'warn';
142
+ static logSpy: ((...args: unknown[]) => void) | null = null;
142
143
 
143
144
  /**
144
145
  * Logs messages to console when logging is enabled
145
146
  * @param args - Arguments to log
146
147
  */
147
148
  log = (...args: unknown[]) => {
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);
149
+ if (Blac.logSpy) {
150
+ Blac.logSpy(args);
151
+ }
152
+ if (Blac.enableLog && Blac.logLevel === 'warn') {
153
+ // Logging disabled - console.warn removed
154
+ }
155
+ if (Blac.enableLog && Blac.logLevel === 'log') {
156
+ // Logging disabled - console.log removed
157
+ }
152
158
  };
153
159
  static get log() {
154
160
  return Blac.instance.log;
@@ -169,11 +175,7 @@ export class Blac {
169
175
  */
170
176
  warn = (message: string, ...args: unknown[]) => {
171
177
  if (Blac.enableLog) {
172
- console.warn(
173
- `🚨 [Blac ${String(Blac.instance.createdAt)}]`,
174
- message,
175
- ...args,
176
- );
178
+ // Logging disabled - console.warn removed
177
179
  }
178
180
  };
179
181
  static get warn() {
@@ -186,11 +188,7 @@ export class Blac {
186
188
  */
187
189
  error = (message: string, ...args: unknown[]) => {
188
190
  if (Blac.enableLog) {
189
- console.error(
190
- `🚨 [Blac ${String(Blac.instance.createdAt)}]`,
191
- message,
192
- ...args,
193
- );
191
+ // Logging disabled - console.error removed
194
192
  }
195
193
  };
196
194
  static get error() {
@@ -212,7 +210,10 @@ export class Blac {
212
210
  // Use disposeBloc method to ensure proper cleanup
213
211
  oldBlocInstanceMap.forEach((bloc) => {
214
212
  // TODO: Type assertion for private property access (see explanation above)
215
- if (!bloc._keepAlive && (bloc as any)._disposalState === 'active') {
213
+ if (
214
+ !bloc._keepAlive &&
215
+ (bloc as any)._disposalState === BlocLifecycleState.ACTIVE
216
+ ) {
216
217
  this.disposeBloc(bloc);
217
218
  }
218
219
  });
@@ -220,7 +221,10 @@ export class Blac {
220
221
  oldIsolatedBlocMap.forEach((blocArray) => {
221
222
  blocArray.forEach((bloc) => {
222
223
  // TODO: Type assertion for private property access (see explanation above)
223
- if (!bloc._keepAlive && (bloc as any)._disposalState === 'active') {
224
+ if (
225
+ !bloc._keepAlive &&
226
+ (bloc as any)._disposalState === BlocLifecycleState.ACTIVE
227
+ ) {
224
228
  this.disposeBloc(bloc);
225
229
  }
226
230
  });
@@ -248,9 +252,15 @@ export class Blac {
248
252
  // This is safe because we know BlocBase has this property, but TypeScript can't verify
249
253
  // private property access across class boundaries. Alternative would be to make
250
254
  // _disposalState protected, but that would expose internal implementation details.
251
- if ((bloc as any)._disposalState !== 'active') {
255
+ const currentState = (bloc as any)._disposalState;
256
+ const validStatesForDisposal = [
257
+ BlocLifecycleState.ACTIVE,
258
+ BlocLifecycleState.DISPOSAL_REQUESTED,
259
+ ];
260
+
261
+ if (!validStatesForDisposal.includes(currentState)) {
252
262
  this.log(
253
- `[${bloc._name}:${String(bloc._id)}] disposeBloc called on already disposed bloc`,
263
+ `[${bloc._name}:${String(bloc._id)}] disposeBloc called on bloc in invalid state: ${currentState}`,
254
264
  );
255
265
  return;
256
266
  }
@@ -467,7 +477,7 @@ export class Blac {
467
477
 
468
478
  activateBloc = (bloc: BlocBase<unknown>): void => {
469
479
  // Don't activate disposed blocs
470
- if ((bloc as any)._disposalState !== 'active') {
480
+ if ((bloc as any)._disposalState !== BlocLifecycleState.ACTIVE) {
471
481
  this.log(
472
482
  `[${bloc._name}:${String(bloc._id)}] activateBloc called on disposed bloc. Ignoring.`,
473
483
  );
@@ -512,26 +522,39 @@ export class Blac {
512
522
  blocClass: B,
513
523
  options: GetBlocOptions<InstanceType<B>> = {},
514
524
  ): InstanceType<B> => {
515
- const { id } = options;
525
+ const { id, forceNewInstance } = options;
516
526
  const base = blocClass as unknown as BlocBaseAbstract;
517
527
  const blocId = id ?? blocClass.name;
528
+ this.log(`[${blocClass.name}:${String(blocId)}] (getBloc) Called:`, {
529
+ options,
530
+ base,
531
+ });
518
532
 
519
- if (base.isolated) {
520
- const isolatedBloc = this.findIsolatedBlocInstance<B>(blocClass, blocId);
521
- if (isolatedBloc) {
522
- return isolatedBloc;
523
- } else {
524
- if (options.throwIfNotFound) {
525
- throw new Error(`Isolated bloc ${blocClass.name} not found`);
533
+ // Skip instance lookup if forceNewInstance is true for isolated blocs
534
+ if (!forceNewInstance) {
535
+ if (base.isolated) {
536
+ const isolatedBloc = this.findIsolatedBlocInstance<B>(
537
+ blocClass,
538
+ options.instanceRef ?? blocId,
539
+ );
540
+ if (isolatedBloc) {
541
+ return isolatedBloc;
542
+ } else {
543
+ if (options.throwIfNotFound) {
544
+ throw new Error(`Isolated bloc ${blocClass.name} not found`);
545
+ }
526
546
  }
527
- }
528
- } else {
529
- const registeredBloc = this.findRegisteredBlocInstance(blocClass, blocId);
530
- if (registeredBloc) {
531
- return registeredBloc;
532
547
  } else {
533
- if (options.throwIfNotFound) {
534
- throw new Error(`Registered bloc ${blocClass.name} not found`);
548
+ const registeredBloc = this.findRegisteredBlocInstance(
549
+ blocClass,
550
+ blocId,
551
+ );
552
+ if (registeredBloc) {
553
+ return registeredBloc;
554
+ } else {
555
+ if (options.throwIfNotFound) {
556
+ throw new Error(`Registered bloc ${blocClass.name} not found`);
557
+ }
535
558
  }
536
559
  }
537
560
  }
@@ -704,7 +727,7 @@ export class Blac {
704
727
  if (
705
728
  bloc._consumers.size === 0 &&
706
729
  !bloc._keepAlive &&
707
- (bloc as any)._disposalState === 'active'
730
+ (bloc as any)._disposalState === BlocLifecycleState.ACTIVE
708
731
  ) {
709
732
  // Schedule disposal for blocs with no consumers
710
733
  setTimeout(() => {
@@ -713,7 +736,7 @@ export class Blac {
713
736
  if (
714
737
  bloc._consumers.size === 0 &&
715
738
  !bloc._keepAlive &&
716
- (bloc as any)._disposalState === 'active'
739
+ (bloc as any)._disposalState === BlocLifecycleState.ACTIVE
717
740
  ) {
718
741
  this.disposeBloc(bloc);
719
742
  }
@@ -1,4 +1,5 @@
1
1
  import { Blac } from './Blac';
2
+ import { BlocLifecycleState } from './BlocBase';
2
3
  import { BlocBase } from './BlocBase';
3
4
  import { BlocHookDependencyArrayFn } from './types';
4
5
 
@@ -14,7 +15,7 @@ export type BlacObserver<S> = {
14
15
  /** Dispose function for the observer */
15
16
  dispose?: () => void;
16
17
  /** Cached state values used for dependency comparison */
17
- lastState?: unknown[][];
18
+ lastState?: unknown[];
18
19
  /** Unique identifier for the observer */
19
20
  id: string;
20
21
  };
@@ -59,13 +60,49 @@ export class BlacObservable<S = unknown> {
59
60
  * @returns A function that can be called to unsubscribe the observer
60
61
  */
61
62
  subscribe(observer: BlacObserver<S>): () => void {
62
- Blac.log('BlacObservable.subscribe: Subscribing observer.', this.bloc, observer);
63
+ // Check if bloc is disposed or in disposal process
64
+ const disposalState = (this.bloc as any)._disposalState;
65
+ if (
66
+ disposalState === BlocLifecycleState.DISPOSED ||
67
+ disposalState === BlocLifecycleState.DISPOSING
68
+ ) {
69
+ Blac.log(
70
+ 'BlacObservable.subscribe: Cannot subscribe to disposed/disposing bloc.',
71
+ this.bloc,
72
+ observer,
73
+ );
74
+ return () => {}; // Return no-op unsubscribe
75
+ }
76
+
77
+ Blac.log(
78
+ 'BlacObservable.subscribe: Subscribing observer.',
79
+ this.bloc,
80
+ observer,
81
+ );
63
82
  this._observers.add(observer);
83
+
84
+ // If we're in DISPOSAL_REQUESTED state, cancel the disposal since we have a new observer
85
+ if (disposalState === BlocLifecycleState.DISPOSAL_REQUESTED) {
86
+ Blac.log(
87
+ 'BlacObservable.subscribe: Cancelling disposal due to new subscription.',
88
+ this.bloc,
89
+ );
90
+ // Transition back to active state
91
+ (this.bloc as any)._atomicStateTransition(
92
+ BlocLifecycleState.DISPOSAL_REQUESTED,
93
+ BlocLifecycleState.ACTIVE,
94
+ );
95
+ }
96
+
64
97
  // Don't initialize lastState here - let it remain undefined for first-time detection
65
98
  return () => {
66
- Blac.log('BlacObservable.subscribe: Unsubscribing observer.', this.bloc, observer);
99
+ Blac.log(
100
+ 'BlacObservable.subscribe: Unsubscribing observer.',
101
+ this.bloc,
102
+ observer,
103
+ );
67
104
  this.unsubscribe(observer);
68
- }
105
+ };
69
106
  }
70
107
 
71
108
  /**
@@ -79,8 +116,14 @@ export class BlacObservable<S = unknown> {
79
116
  if (this.size === 0) {
80
117
  Blac.log('BlacObservable.unsubscribe: No observers left.', this.bloc);
81
118
  // 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.`);
119
+ if (
120
+ this.bloc._consumers.size === 0 &&
121
+ !this.bloc._keepAlive &&
122
+ (this.bloc as any)._disposalState === BlocLifecycleState.ACTIVE
123
+ ) {
124
+ Blac.log(
125
+ `[${this.bloc._name}:${this.bloc._id}] No observers or consumers left. Scheduling disposal.`,
126
+ );
84
127
  (this.bloc as any)._scheduleDisposal();
85
128
  }
86
129
  }
@@ -98,35 +141,26 @@ export class BlacObservable<S = unknown> {
98
141
 
99
142
  if (observer.dependencyArray) {
100
143
  const lastDependencyCheck = observer.lastState;
101
- const newDependencyCheck = observer.dependencyArray(newState, oldState);
144
+ const newDependencyCheck = observer.dependencyArray(
145
+ newState,
146
+ oldState,
147
+ this.bloc,
148
+ );
102
149
 
103
150
  // If this is the first time (no lastState), trigger initial render
104
151
  if (!lastDependencyCheck) {
105
152
  shouldUpdate = true;
106
153
  } else {
107
- // Compare two-array dependency structure: [stateArray, classArray]
154
+ // Compare dependency arrays
108
155
  if (lastDependencyCheck.length !== newDependencyCheck.length) {
109
156
  shouldUpdate = true;
110
157
  } else {
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] || [];
115
-
116
- if (lastArray.length !== newArray.length) {
158
+ // Compare each dependency value using Object.is (same as React)
159
+ for (let i = 0; i < newDependencyCheck.length; i++) {
160
+ if (!Object.is(lastDependencyCheck[i], newDependencyCheck[i])) {
117
161
  shouldUpdate = true;
118
162
  break;
119
163
  }
120
-
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])) {
124
- shouldUpdate = true;
125
- break;
126
- }
127
- }
128
-
129
- if (shouldUpdate) break;
130
164
  }
131
165
  }
132
166
  }