@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/README.md +27 -0
- package/dist/index.cjs.js +2 -2
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +224 -14
- package/dist/index.es.js +1122 -556
- package/dist/index.es.js.map +1 -1
- package/package.json +5 -4
- package/src/Blac.ts +109 -47
- package/src/BlacObserver.ts +58 -24
- package/src/Bloc.ts +120 -160
- package/src/BlocBase.ts +266 -90
- package/src/Cubit.ts +15 -1
- package/src/__tests__/Blac.config.test.ts +86 -0
- 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/__tests__/props.test.ts +256 -0
- package/src/adapter/BlacAdapter.ts +327 -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/events.ts +3 -0
- package/src/index.ts +4 -1
- package/src/testing.ts +60 -47
- package/src/types.ts +12 -13
- package/src/utils/shallowEqual.ts +40 -0
- package/src/utils/uuid.ts +4 -4
- package/src/BlacEvent.ts +0 -5
package/src/Blac.ts
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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 !==
|
|
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
|
|
520
|
-
|
|
521
|
-
if (
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
534
|
-
|
|
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 ===
|
|
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 ===
|
|
778
|
+
(bloc as any)._disposalState === BlocLifecycleState.ACTIVE
|
|
717
779
|
) {
|
|
718
780
|
this.disposeBloc(bloc);
|
|
719
781
|
}
|
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
|
}
|