@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/dist/index.cjs.js +2 -2
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.es.js +119 -82
- package/dist/index.es.js.map +1 -1
- package/package.json +1 -1
- package/src/Blac.ts +166 -79
- package/src/BlacObserver.ts +16 -13
- package/src/Bloc.ts +10 -2
- package/src/BlocBase.ts +9 -1
- package/src/types.ts +11 -4
package/src/Blac.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
// TODO:
|
|
3
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from
|
|
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({
|
|
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({
|
|
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
|
|
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() {
|
|
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(
|
|
172
|
+
console.warn(
|
|
173
|
+
`🚨 [Blac ${String(Blac.instance.createdAt)}]`,
|
|
174
|
+
message,
|
|
175
|
+
...args,
|
|
176
|
+
);
|
|
155
177
|
}
|
|
156
178
|
};
|
|
157
|
-
static get 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(
|
|
189
|
+
console.error(
|
|
190
|
+
`🚨 [Blac ${String(Blac.instance.createdAt)}]`,
|
|
191
|
+
message,
|
|
192
|
+
...args,
|
|
193
|
+
);
|
|
166
194
|
}
|
|
167
195
|
};
|
|
168
|
-
static get 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(
|
|
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(
|
|
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(
|
|
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() {
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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() {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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() {
|
|
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() {
|
|
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) {
|
|
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() {
|
|
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() {
|
|
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(
|
|
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() {
|
|
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
|
-
|
|
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
|
-
|
|
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() {
|
|
724
|
+
static get validateConsumers() {
|
|
725
|
+
return Blac.instance.validateConsumers;
|
|
726
|
+
}
|
|
640
727
|
}
|
package/src/BlacObserver.ts
CHANGED
|
@@ -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
|
-
//
|
|
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),
|
|
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
|
|
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
|
|
108
|
-
for (let
|
|
109
|
-
const
|
|
110
|
-
const
|
|
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
|
-
|
|
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
|
|
119
|
-
for (let i = 0; i <
|
|
120
|
-
if (!Object.is(
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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);
|