@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/dist/index.cjs.js +2 -2
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +185 -4
- package/dist/index.es.js +1099 -601
- package/dist/index.es.js.map +1 -1
- package/package.json +5 -4
- package/src/Blac.ts +63 -40
- package/src/BlacObserver.ts +58 -24
- package/src/Bloc.ts +166 -157
- package/src/BlocBase.ts +266 -90
- package/src/Cubit.ts +1 -1
- package/src/__tests__/Bloc.event.test.ts +419 -0
- package/src/__tests__/BlocBase.lifecycle.test.ts +441 -0
- package/src/__tests__/Cubit.test.ts +473 -0
- package/src/adapter/BlacAdapter.ts +262 -0
- package/src/adapter/ConsumerTracker.ts +241 -0
- package/src/adapter/ProxyFactory.ts +308 -0
- package/src/adapter/__tests__/BlacAdapter.test.ts +561 -0
- package/src/adapter/__tests__/ConsumerTracker.test.ts +562 -0
- package/src/adapter/__tests__/ProxyFactory.test.ts +655 -0
- package/src/adapter/index.ts +3 -0
- package/src/index.ts +2 -1
- package/src/testing.ts +60 -47
- package/src/types.ts +11 -3
- package/src/utils/uuid.ts +4 -4
- package/src/BlacEvent.ts +0 -5
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
|
-
|
|
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.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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 !==
|
|
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
|
|
520
|
-
|
|
521
|
-
if (
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
534
|
-
|
|
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 ===
|
|
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 ===
|
|
739
|
+
(bloc as any)._disposalState === BlocLifecycleState.ACTIVE
|
|
717
740
|
) {
|
|
718
741
|
this.disposeBloc(bloc);
|
|
719
742
|
}
|
package/src/BlacObserver.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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 (
|
|
83
|
-
|
|
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(
|
|
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
|
|
154
|
+
// Compare dependency arrays
|
|
108
155
|
if (lastDependencyCheck.length !== newDependencyCheck.length) {
|
|
109
156
|
shouldUpdate = true;
|
|
110
157
|
} else {
|
|
111
|
-
// Compare each
|
|
112
|
-
for (let
|
|
113
|
-
|
|
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
|
}
|