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

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,11 +1,4 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
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';
1
+ import { BlocBase, BlocInstanceId, BlocLifecycleState } from './BlocBase';
9
2
  import {
10
3
  BlocBaseAbstract,
11
4
  BlocConstructor,
@@ -20,6 +13,12 @@ import {
20
13
  export interface BlacConfig {
21
14
  /** Whether to expose the Blac instance globally */
22
15
  exposeBlacInstance?: boolean;
16
+ /**
17
+ * Whether to enable proxy dependency tracking for automatic re-render optimization.
18
+ * When false, state changes always cause re-renders unless dependencies are manually specified.
19
+ * Default: true
20
+ */
21
+ proxyDependencyTracking?: boolean;
23
22
  }
24
23
 
25
24
  export interface GetBlocOptions<B extends BlocBase<unknown>> {
@@ -29,6 +28,8 @@ export interface GetBlocOptions<B extends BlocBase<unknown>> {
29
28
  onMount?: (bloc: B) => void;
30
29
  instanceRef?: string;
31
30
  throwIfNotFound?: boolean;
31
+ /** Force creation of a new instance for isolated blocs, bypassing instance lookup */
32
+ forceNewInstance?: boolean;
32
33
  }
33
34
 
34
35
  /**
@@ -98,7 +99,6 @@ export function setBlacInstanceManager(manager: BlacInstanceManager): void {
98
99
  * - Providing logging and debugging capabilities
99
100
  */
100
101
  export class Blac {
101
- /** @deprecated Use getInstance() instead */
102
102
  static get instance(): Blac {
103
103
  return instanceManager.getInstance();
104
104
  }
@@ -107,16 +107,55 @@ export class Blac {
107
107
  static get getAllBlocs() {
108
108
  return Blac.instance.getAllBlocs;
109
109
  }
110
+
111
+ /** Private static configuration */
112
+ private static _config: BlacConfig = {
113
+ exposeBlacInstance: false,
114
+ proxyDependencyTracking: true,
115
+ };
116
+
117
+ /** Get current configuration */
118
+ static get config(): Readonly<BlacConfig> {
119
+ return { ...this._config };
120
+ }
121
+
122
+ /**
123
+ * Set or update Blac configuration
124
+ * @param config - Partial configuration to merge with existing config
125
+ * @throws Error if configuration is invalid
126
+ */
127
+ static setConfig(config: Partial<BlacConfig>): void {
128
+ // Validate config
129
+ if (config.proxyDependencyTracking !== undefined &&
130
+ typeof config.proxyDependencyTracking !== 'boolean') {
131
+ throw new Error('BlacConfig.proxyDependencyTracking must be a boolean');
132
+ }
133
+
134
+ if (config.exposeBlacInstance !== undefined &&
135
+ typeof config.exposeBlacInstance !== 'boolean') {
136
+ throw new Error('BlacConfig.exposeBlacInstance must be a boolean');
137
+ }
138
+
139
+ // Merge with existing config
140
+ this._config = {
141
+ ...this._config,
142
+ ...config,
143
+ };
144
+
145
+ // Log config update if logging is enabled
146
+ if (this.enableLog) {
147
+ this.instance.log('Blac config updated:', this._config);
148
+ }
149
+ }
110
150
  /** Map storing all registered bloc instances by their class name and ID */
111
151
  blocInstanceMap: Map<string, BlocBase<unknown>> = new Map();
112
152
  /** Map storing isolated bloc instances grouped by their constructor */
113
- // TODO: BlocConstructor<any> is required here for type inference to work correctly.
114
153
  // Using BlocConstructor<BlocBase<unknown>> would break type inference when storing
115
154
  // different bloc types in the same map. The 'any' allows proper polymorphic storage
116
155
  // while maintaining type safety at usage sites through the BlocConstructor constraint.
117
156
  isolatedBlocMap: Map<BlocConstructor<any>, BlocBase<unknown>[]> = new Map();
118
157
  /** Map for O(1) lookup of isolated blocs by UID */
119
- private isolatedBlocIndex: Map<string, BlocBase<unknown>> = new Map();
158
+ isolatedBlocIndex: Map<string, BlocBase<unknown>> = new Map();
120
159
  /** Map tracking UIDs to prevent memory leaks */
121
160
  uidRegistry: Map<string, BlocBase<unknown>> = new Map();
122
161
  /** Set of keep-alive blocs for controlled cleanup */
@@ -139,16 +178,22 @@ export class Blac {
139
178
  /** Flag to enable/disable logging */
140
179
  static enableLog = false;
141
180
  static logLevel: 'warn' | 'log' = 'warn';
181
+ static logSpy: ((...args: unknown[]) => void) | null = null;
142
182
 
143
183
  /**
144
184
  * Logs messages to console when logging is enabled
145
185
  * @param args - Arguments to log
146
186
  */
147
187
  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);
188
+ if (Blac.logSpy) {
189
+ Blac.logSpy(args);
190
+ }
191
+ if (Blac.enableLog && Blac.logLevel === 'warn') {
192
+ // Logging disabled - console.warn removed
193
+ }
194
+ if (Blac.enableLog && Blac.logLevel === 'log') {
195
+ // Logging disabled - console.log removed
196
+ }
152
197
  };
153
198
  static get log() {
154
199
  return Blac.instance.log;
@@ -169,11 +214,7 @@ export class Blac {
169
214
  */
170
215
  warn = (message: string, ...args: unknown[]) => {
171
216
  if (Blac.enableLog) {
172
- console.warn(
173
- `🚨 [Blac ${String(Blac.instance.createdAt)}]`,
174
- message,
175
- ...args,
176
- );
217
+ // Logging disabled - console.warn removed
177
218
  }
178
219
  };
179
220
  static get warn() {
@@ -186,11 +227,7 @@ export class Blac {
186
227
  */
187
228
  error = (message: string, ...args: unknown[]) => {
188
229
  if (Blac.enableLog) {
189
- console.error(
190
- `🚨 [Blac ${String(Blac.instance.createdAt)}]`,
191
- message,
192
- ...args,
193
- );
230
+ // Logging disabled - console.error removed
194
231
  }
195
232
  };
196
233
  static get error() {
@@ -212,7 +249,10 @@ export class Blac {
212
249
  // Use disposeBloc method to ensure proper cleanup
213
250
  oldBlocInstanceMap.forEach((bloc) => {
214
251
  // TODO: Type assertion for private property access (see explanation above)
215
- if (!bloc._keepAlive && (bloc as any)._disposalState === 'active') {
252
+ if (
253
+ !bloc._keepAlive &&
254
+ (bloc as any)._disposalState === BlocLifecycleState.ACTIVE
255
+ ) {
216
256
  this.disposeBloc(bloc);
217
257
  }
218
258
  });
@@ -220,7 +260,10 @@ export class Blac {
220
260
  oldIsolatedBlocMap.forEach((blocArray) => {
221
261
  blocArray.forEach((bloc) => {
222
262
  // TODO: Type assertion for private property access (see explanation above)
223
- if (!bloc._keepAlive && (bloc as any)._disposalState === 'active') {
263
+ if (
264
+ !bloc._keepAlive &&
265
+ (bloc as any)._disposalState === BlocLifecycleState.ACTIVE
266
+ ) {
224
267
  this.disposeBloc(bloc);
225
268
  }
226
269
  });
@@ -248,9 +291,15 @@ export class Blac {
248
291
  // This is safe because we know BlocBase has this property, but TypeScript can't verify
249
292
  // private property access across class boundaries. Alternative would be to make
250
293
  // _disposalState protected, but that would expose internal implementation details.
251
- if ((bloc as any)._disposalState !== 'active') {
294
+ const currentState = (bloc as any)._disposalState;
295
+ const validStatesForDisposal = [
296
+ BlocLifecycleState.ACTIVE,
297
+ BlocLifecycleState.DISPOSAL_REQUESTED,
298
+ ];
299
+
300
+ if (!validStatesForDisposal.includes(currentState)) {
252
301
  this.log(
253
- `[${bloc._name}:${String(bloc._id)}] disposeBloc called on already disposed bloc`,
302
+ `[${bloc._name}:${String(bloc._id)}] disposeBloc called on bloc in invalid state: ${currentState}`,
254
303
  );
255
304
  return;
256
305
  }
@@ -467,7 +516,7 @@ export class Blac {
467
516
 
468
517
  activateBloc = (bloc: BlocBase<unknown>): void => {
469
518
  // Don't activate disposed blocs
470
- if ((bloc as any)._disposalState !== 'active') {
519
+ if ((bloc as any)._disposalState !== BlocLifecycleState.ACTIVE) {
471
520
  this.log(
472
521
  `[${bloc._name}:${String(bloc._id)}] activateBloc called on disposed bloc. Ignoring.`,
473
522
  );
@@ -512,26 +561,39 @@ export class Blac {
512
561
  blocClass: B,
513
562
  options: GetBlocOptions<InstanceType<B>> = {},
514
563
  ): InstanceType<B> => {
515
- const { id } = options;
564
+ const { id, forceNewInstance } = options;
516
565
  const base = blocClass as unknown as BlocBaseAbstract;
517
566
  const blocId = id ?? blocClass.name;
567
+ this.log(`[${blocClass.name}:${String(blocId)}] (getBloc) Called:`, {
568
+ options,
569
+ base,
570
+ });
518
571
 
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`);
572
+ // Skip instance lookup if forceNewInstance is true for isolated blocs
573
+ if (!forceNewInstance) {
574
+ if (base.isolated) {
575
+ const isolatedBloc = this.findIsolatedBlocInstance<B>(
576
+ blocClass,
577
+ options.instanceRef ?? blocId,
578
+ );
579
+ if (isolatedBloc) {
580
+ return isolatedBloc;
581
+ } else {
582
+ if (options.throwIfNotFound) {
583
+ throw new Error(`Isolated bloc ${blocClass.name} not found`);
584
+ }
526
585
  }
527
- }
528
- } else {
529
- const registeredBloc = this.findRegisteredBlocInstance(blocClass, blocId);
530
- if (registeredBloc) {
531
- return registeredBloc;
532
586
  } else {
533
- if (options.throwIfNotFound) {
534
- throw new Error(`Registered bloc ${blocClass.name} not found`);
587
+ const registeredBloc = this.findRegisteredBlocInstance(
588
+ blocClass,
589
+ blocId,
590
+ );
591
+ if (registeredBloc) {
592
+ return registeredBloc;
593
+ } else {
594
+ if (options.throwIfNotFound) {
595
+ throw new Error(`Registered bloc ${blocClass.name} not found`);
596
+ }
535
597
  }
536
598
  }
537
599
  }
@@ -704,7 +766,7 @@ export class Blac {
704
766
  if (
705
767
  bloc._consumers.size === 0 &&
706
768
  !bloc._keepAlive &&
707
- (bloc as any)._disposalState === 'active'
769
+ (bloc as any)._disposalState === BlocLifecycleState.ACTIVE
708
770
  ) {
709
771
  // Schedule disposal for blocs with no consumers
710
772
  setTimeout(() => {
@@ -713,7 +775,7 @@ export class Blac {
713
775
  if (
714
776
  bloc._consumers.size === 0 &&
715
777
  !bloc._keepAlive &&
716
- (bloc as any)._disposalState === 'active'
778
+ (bloc as any)._disposalState === BlocLifecycleState.ACTIVE
717
779
  ) {
718
780
  this.disposeBloc(bloc);
719
781
  }
@@ -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
  }