@blac/core 2.0.0-rc-3 → 2.0.0-rc-5
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 +1 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +94 -26
- package/dist/index.es.js +312 -139
- package/dist/index.es.js.map +1 -1
- package/package.json +3 -2
- package/src/Blac.ts +235 -42
- package/src/BlocBase.ts +127 -19
package/src/Blac.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
// TODO: Remove this eslint disable once any types are properly replaced
|
|
2
3
|
import { BlocBase, BlocInstanceId } from "./BlocBase";
|
|
3
4
|
import {
|
|
4
5
|
BlocBaseAbstract,
|
|
@@ -16,7 +17,7 @@ export interface BlacConfig {
|
|
|
16
17
|
exposeBlacInstance?: boolean;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
export interface GetBlocOptions<B extends BlocBase<
|
|
20
|
+
export interface GetBlocOptions<B extends BlocBase<unknown>> {
|
|
20
21
|
id?: string;
|
|
21
22
|
selector?: BlocHookDependencyArrayFn<BlocState<B>>;
|
|
22
23
|
props?: InferPropsFromGeneric<B>;
|
|
@@ -25,9 +26,61 @@ export interface GetBlocOptions<B extends BlocBase<any>> {
|
|
|
25
26
|
throwIfNotFound?: boolean;
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Interface for Blac instance management
|
|
31
|
+
*/
|
|
32
|
+
export interface BlacInstanceManager {
|
|
33
|
+
getInstance(): Blac;
|
|
34
|
+
setInstance(instance: Blac): void;
|
|
35
|
+
resetInstance(): void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Default singleton implementation of BlacInstanceManager
|
|
40
|
+
*/
|
|
41
|
+
class SingletonBlacManager implements BlacInstanceManager {
|
|
42
|
+
private static _instance: Blac;
|
|
43
|
+
|
|
44
|
+
getInstance(): Blac {
|
|
45
|
+
if (!SingletonBlacManager._instance) {
|
|
46
|
+
SingletonBlacManager._instance = new Blac({ __unsafe_ignore_singleton: true });
|
|
47
|
+
}
|
|
48
|
+
return SingletonBlacManager._instance;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setInstance(instance: Blac): void {
|
|
52
|
+
SingletonBlacManager._instance = instance;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
resetInstance(): void {
|
|
56
|
+
const oldInstance = SingletonBlacManager._instance;
|
|
57
|
+
SingletonBlacManager._instance = new Blac({ __unsafe_ignore_singleton: true });
|
|
58
|
+
|
|
59
|
+
// Transfer any keep-alive blocs to the new instance
|
|
60
|
+
if (oldInstance) {
|
|
61
|
+
for (const bloc of oldInstance.keepAliveBlocs) {
|
|
62
|
+
SingletonBlacManager._instance.keepAliveBlocs.add(bloc);
|
|
63
|
+
SingletonBlacManager._instance.uidRegistry.set(bloc.uid, bloc);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Global instance manager (can be replaced for testing or different patterns)
|
|
71
|
+
*/
|
|
72
|
+
let instanceManager: BlacInstanceManager = new SingletonBlacManager();
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Sets a custom instance manager (useful for testing or advanced patterns)
|
|
76
|
+
*/
|
|
77
|
+
export function setBlacInstanceManager(manager: BlacInstanceManager): void {
|
|
78
|
+
instanceManager = manager;
|
|
79
|
+
}
|
|
80
|
+
|
|
28
81
|
/**
|
|
29
82
|
* Main Blac class that manages the state management system.
|
|
30
|
-
*
|
|
83
|
+
* Can work with singleton pattern or dependency injection.
|
|
31
84
|
* Handles bloc lifecycle, and instance tracking.
|
|
32
85
|
*
|
|
33
86
|
* Key responsibilities:
|
|
@@ -36,15 +89,25 @@ export interface GetBlocOptions<B extends BlocBase<any>> {
|
|
|
36
89
|
* - Providing logging and debugging capabilities
|
|
37
90
|
*/
|
|
38
91
|
export class Blac {
|
|
39
|
-
/**
|
|
40
|
-
static instance: Blac
|
|
92
|
+
/** @deprecated Use getInstance() instead */
|
|
93
|
+
static get instance(): Blac {
|
|
94
|
+
return instanceManager.getInstance();
|
|
95
|
+
}
|
|
41
96
|
/** Timestamp when the instance was created */
|
|
42
97
|
createdAt = Date.now();
|
|
43
|
-
static getAllBlocs
|
|
98
|
+
static get getAllBlocs() {
|
|
99
|
+
return Blac.instance.getAllBlocs;
|
|
100
|
+
}
|
|
44
101
|
/** Map storing all registered bloc instances by their class name and ID */
|
|
45
|
-
blocInstanceMap: Map<string, BlocBase<
|
|
102
|
+
blocInstanceMap: Map<string, BlocBase<unknown>> = new Map();
|
|
46
103
|
/** Map storing isolated bloc instances grouped by their constructor */
|
|
47
|
-
isolatedBlocMap: Map<BlocConstructor<any>, BlocBase<
|
|
104
|
+
isolatedBlocMap: Map<BlocConstructor<any>, BlocBase<unknown>[]> = new Map();
|
|
105
|
+
/** Map for O(1) lookup of isolated blocs by UID */
|
|
106
|
+
private isolatedBlocIndex: Map<string, BlocBase<unknown>> = new Map();
|
|
107
|
+
/** Map tracking UIDs to prevent memory leaks */
|
|
108
|
+
uidRegistry: Map<string, BlocBase<unknown>> = new Map();
|
|
109
|
+
/** Set of keep-alive blocs for controlled cleanup */
|
|
110
|
+
keepAliveBlocs: Set<BlocBase<unknown>> = new Set();
|
|
48
111
|
/** Flag to control whether changes should be posted to document */
|
|
49
112
|
postChangesToDocument = false;
|
|
50
113
|
|
|
@@ -57,7 +120,7 @@ export class Blac {
|
|
|
57
120
|
if (!__unsafe_ignore_singleton) {
|
|
58
121
|
return Blac.instance;
|
|
59
122
|
}
|
|
60
|
-
|
|
123
|
+
instanceManager.setInstance(this);
|
|
61
124
|
}
|
|
62
125
|
|
|
63
126
|
/** Flag to enable/disable logging */
|
|
@@ -70,14 +133,14 @@ export class Blac {
|
|
|
70
133
|
log = (...args: unknown[]) => {
|
|
71
134
|
if (Blac.enableLog) console.warn(`☢️ [Blac ${this.createdAt.toString()}]`, ...args);
|
|
72
135
|
};
|
|
73
|
-
static log
|
|
136
|
+
static get log() { return Blac.instance.log; }
|
|
74
137
|
|
|
75
138
|
/**
|
|
76
|
-
* Gets the
|
|
139
|
+
* Gets the current Blac instance
|
|
77
140
|
* @returns The Blac instance
|
|
78
141
|
*/
|
|
79
142
|
static getInstance(): Blac {
|
|
80
|
-
return
|
|
143
|
+
return instanceManager.getInstance();
|
|
81
144
|
}
|
|
82
145
|
|
|
83
146
|
|
|
@@ -91,7 +154,7 @@ export class Blac {
|
|
|
91
154
|
console.warn(`🚨 [Blac ${String(Blac.instance.createdAt)}]`, message, ...args);
|
|
92
155
|
}
|
|
93
156
|
};
|
|
94
|
-
static warn
|
|
157
|
+
static get warn() { return Blac.instance.warn; }
|
|
95
158
|
/**
|
|
96
159
|
* Logs an error message
|
|
97
160
|
* @param message - Error message
|
|
@@ -102,7 +165,7 @@ export class Blac {
|
|
|
102
165
|
console.error(`🚨 [Blac ${String(Blac.instance.createdAt)}]`, message, ...args);
|
|
103
166
|
}
|
|
104
167
|
};
|
|
105
|
-
static error
|
|
168
|
+
static get error() { return Blac.instance.error; }
|
|
106
169
|
|
|
107
170
|
/**
|
|
108
171
|
* Resets the Blac instance to a new one, disposing non-keepAlive blocs
|
|
@@ -116,30 +179,35 @@ export class Blac {
|
|
|
116
179
|
const oldIsolatedBlocMap = new Map(this.isolatedBlocMap);
|
|
117
180
|
|
|
118
181
|
oldBlocInstanceMap.forEach((bloc) => {
|
|
119
|
-
bloc.
|
|
182
|
+
if (!bloc._keepAlive) {
|
|
183
|
+
bloc._dispose();
|
|
184
|
+
}
|
|
120
185
|
});
|
|
121
186
|
|
|
122
187
|
oldIsolatedBlocMap.forEach((blocArray) => {
|
|
123
188
|
blocArray.forEach((bloc) => {
|
|
124
|
-
bloc.
|
|
189
|
+
if (!bloc._keepAlive) {
|
|
190
|
+
bloc._dispose();
|
|
191
|
+
}
|
|
125
192
|
});
|
|
126
193
|
});
|
|
127
194
|
|
|
128
195
|
this.blocInstanceMap.clear();
|
|
129
196
|
this.isolatedBlocMap.clear();
|
|
197
|
+
this.isolatedBlocIndex.clear();
|
|
130
198
|
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
__unsafe_ignore_singleton: true,
|
|
134
|
-
});
|
|
199
|
+
// Use instance manager to reset
|
|
200
|
+
instanceManager.resetInstance();
|
|
135
201
|
}
|
|
136
|
-
static resetInstance =
|
|
202
|
+
static resetInstance = (): void => {
|
|
203
|
+
instanceManager.resetInstance();
|
|
204
|
+
};
|
|
137
205
|
|
|
138
206
|
/**
|
|
139
207
|
* Disposes of a bloc instance by removing it from the appropriate registry
|
|
140
208
|
* @param bloc - The bloc instance to dispose
|
|
141
209
|
*/
|
|
142
|
-
disposeBloc = (bloc: BlocBase<
|
|
210
|
+
disposeBloc = (bloc: BlocBase<unknown>): void => {
|
|
143
211
|
const base = bloc.constructor as unknown as BlocBaseAbstract;
|
|
144
212
|
const key = this.createBlocInstanceMapKey(bloc._name, bloc._id);
|
|
145
213
|
this.log(`[${bloc._name}:${String(bloc._id)}] disposeBloc called. Isolated: ${String(base.isolated)}`);
|
|
@@ -167,18 +235,32 @@ export class Blac {
|
|
|
167
235
|
* Unregister a bloc instance from the main registry
|
|
168
236
|
* @param bloc - The bloc instance to unregister
|
|
169
237
|
*/
|
|
170
|
-
unregisterBlocInstance(bloc: BlocBase<
|
|
238
|
+
unregisterBlocInstance(bloc: BlocBase<unknown>): void {
|
|
171
239
|
const key = this.createBlocInstanceMapKey(bloc._name, bloc._id);
|
|
172
240
|
this.blocInstanceMap.delete(key);
|
|
241
|
+
|
|
242
|
+
// Clean up UID tracking
|
|
243
|
+
this.uidRegistry.delete(bloc.uid);
|
|
244
|
+
|
|
245
|
+
// Remove from keep-alive set
|
|
246
|
+
this.keepAliveBlocs.delete(bloc);
|
|
173
247
|
}
|
|
174
248
|
|
|
175
249
|
/**
|
|
176
250
|
* Registers a bloc instance in the main registry
|
|
177
251
|
* @param bloc - The bloc instance to register
|
|
178
252
|
*/
|
|
179
|
-
registerBlocInstance(bloc: BlocBase<
|
|
253
|
+
registerBlocInstance(bloc: BlocBase<unknown>): void {
|
|
180
254
|
const key = this.createBlocInstanceMapKey(bloc._name, bloc._id);
|
|
181
255
|
this.blocInstanceMap.set(key, bloc);
|
|
256
|
+
|
|
257
|
+
// Track UID for cleanup
|
|
258
|
+
this.uidRegistry.set(bloc.uid, bloc);
|
|
259
|
+
|
|
260
|
+
// Track keep-alive blocs
|
|
261
|
+
if (bloc._keepAlive) {
|
|
262
|
+
this.keepAliveBlocs.add(bloc);
|
|
263
|
+
}
|
|
182
264
|
}
|
|
183
265
|
|
|
184
266
|
/**
|
|
@@ -187,7 +269,7 @@ export class Blac {
|
|
|
187
269
|
* @param id - The instance ID
|
|
188
270
|
* @returns The found bloc instance or undefined if not found
|
|
189
271
|
*/
|
|
190
|
-
findRegisteredBlocInstance<B extends BlocConstructor<
|
|
272
|
+
findRegisteredBlocInstance<B extends BlocConstructor<any>>(
|
|
191
273
|
blocClass: B,
|
|
192
274
|
id: BlocInstanceId,
|
|
193
275
|
): InstanceType<B> | undefined {
|
|
@@ -203,39 +285,59 @@ export class Blac {
|
|
|
203
285
|
* Registers an isolated bloc instance in the isolated registry
|
|
204
286
|
* @param bloc - The isolated bloc instance to register
|
|
205
287
|
*/
|
|
206
|
-
registerIsolatedBlocInstance(bloc: BlocBase<
|
|
207
|
-
const blocClass = bloc.constructor as BlocConstructor<
|
|
288
|
+
registerIsolatedBlocInstance(bloc: BlocBase<unknown>): void {
|
|
289
|
+
const blocClass = bloc.constructor as BlocConstructor<any>;
|
|
208
290
|
const blocs = this.isolatedBlocMap.get(blocClass);
|
|
209
291
|
if (blocs) {
|
|
210
292
|
blocs.push(bloc);
|
|
211
293
|
} else {
|
|
212
294
|
this.isolatedBlocMap.set(blocClass, [bloc]);
|
|
213
295
|
}
|
|
296
|
+
|
|
297
|
+
// Add to isolated index for O(1) lookups
|
|
298
|
+
this.isolatedBlocIndex.set(bloc.uid, bloc);
|
|
299
|
+
|
|
300
|
+
// Track UID for cleanup
|
|
301
|
+
this.uidRegistry.set(bloc.uid, bloc);
|
|
302
|
+
|
|
303
|
+
// Track keep-alive blocs
|
|
304
|
+
if (bloc._keepAlive) {
|
|
305
|
+
this.keepAliveBlocs.add(bloc);
|
|
306
|
+
}
|
|
214
307
|
}
|
|
215
308
|
|
|
216
309
|
/**
|
|
217
310
|
* Unregister an isolated bloc instance from the isolated registry
|
|
218
311
|
* @param bloc - The isolated bloc instance to unregister
|
|
219
312
|
*/
|
|
220
|
-
unregisterIsolatedBlocInstance(bloc: BlocBase<
|
|
313
|
+
unregisterIsolatedBlocInstance(bloc: BlocBase<unknown>): void {
|
|
221
314
|
const blocClass = bloc.constructor;
|
|
222
|
-
const blocs = this.isolatedBlocMap.get(blocClass as BlocConstructor<
|
|
315
|
+
const blocs = this.isolatedBlocMap.get(blocClass as BlocConstructor<any>);
|
|
223
316
|
if (blocs) {
|
|
224
|
-
const index = blocs.findIndex((b) => b.
|
|
317
|
+
const index = blocs.findIndex((b) => b.uid === bloc.uid);
|
|
225
318
|
if (index !== -1) {
|
|
226
319
|
blocs.splice(index, 1);
|
|
227
320
|
}
|
|
228
321
|
|
|
229
322
|
if (blocs.length === 0) {
|
|
230
|
-
this.isolatedBlocMap.delete(blocClass as BlocConstructor<
|
|
323
|
+
this.isolatedBlocMap.delete(blocClass as BlocConstructor<any>);
|
|
231
324
|
}
|
|
232
325
|
}
|
|
326
|
+
|
|
327
|
+
// Remove from isolated index
|
|
328
|
+
this.isolatedBlocIndex.delete(bloc.uid);
|
|
329
|
+
|
|
330
|
+
// Clean up UID tracking
|
|
331
|
+
this.uidRegistry.delete(bloc.uid);
|
|
332
|
+
|
|
333
|
+
// Remove from keep-alive set
|
|
334
|
+
this.keepAliveBlocs.delete(bloc);
|
|
233
335
|
}
|
|
234
336
|
|
|
235
337
|
/**
|
|
236
|
-
* Finds an isolated bloc instance by its class and ID
|
|
338
|
+
* Finds an isolated bloc instance by its class and ID (O(n) lookup)
|
|
237
339
|
*/
|
|
238
|
-
findIsolatedBlocInstance<B extends BlocConstructor<
|
|
340
|
+
findIsolatedBlocInstance<B extends BlocConstructor<any>>(
|
|
239
341
|
blocClass: B,
|
|
240
342
|
id: BlocInstanceId,
|
|
241
343
|
): InstanceType<B> | undefined {
|
|
@@ -246,11 +348,20 @@ export class Blac {
|
|
|
246
348
|
if (!blocs) {
|
|
247
349
|
return undefined;
|
|
248
350
|
}
|
|
249
|
-
//
|
|
351
|
+
// Find the specific bloc by ID within the isolated array
|
|
250
352
|
const found = blocs.find((b) => b._id === id) as InstanceType<B> | undefined;
|
|
251
353
|
return found;
|
|
252
354
|
}
|
|
253
355
|
|
|
356
|
+
/**
|
|
357
|
+
* Finds an isolated bloc instance by UID (O(1) lookup)
|
|
358
|
+
*/
|
|
359
|
+
findIsolatedBlocInstanceByUid<B extends BlocBase<unknown>>(
|
|
360
|
+
uid: string
|
|
361
|
+
): B | undefined {
|
|
362
|
+
return this.isolatedBlocIndex.get(uid) as B | undefined;
|
|
363
|
+
}
|
|
364
|
+
|
|
254
365
|
/**
|
|
255
366
|
* Creates a new bloc instance and registers it in the appropriate registry
|
|
256
367
|
* @param blocClass - The bloc class to instantiate
|
|
@@ -265,22 +376,25 @@ export class Blac {
|
|
|
265
376
|
options: GetBlocOptions<InstanceType<B>> = {},
|
|
266
377
|
): InstanceType<B> {
|
|
267
378
|
const { props, instanceRef } = options;
|
|
268
|
-
const newBloc = new blocClass(props as never) as InstanceType<
|
|
379
|
+
const newBloc = new blocClass(props as never) as InstanceType<B>;
|
|
269
380
|
newBloc._instanceRef = instanceRef;
|
|
270
381
|
newBloc.props = props || null;
|
|
271
382
|
newBloc._updateId(id);
|
|
272
383
|
|
|
384
|
+
// Set up disposal handler to break circular dependency
|
|
385
|
+
newBloc._setDisposalHandler((bloc) => this.disposeBloc(bloc));
|
|
386
|
+
|
|
273
387
|
if (newBloc.isIsolated) {
|
|
274
388
|
this.registerIsolatedBlocInstance(newBloc);
|
|
275
|
-
return newBloc
|
|
389
|
+
return newBloc;
|
|
276
390
|
}
|
|
277
391
|
|
|
278
392
|
this.registerBlocInstance(newBloc);
|
|
279
|
-
return newBloc
|
|
393
|
+
return newBloc;
|
|
280
394
|
}
|
|
281
395
|
|
|
282
396
|
|
|
283
|
-
activateBloc = (bloc: BlocBase<
|
|
397
|
+
activateBloc = (bloc: BlocBase<unknown>): void => {
|
|
284
398
|
const base = bloc.constructor as unknown as BlocConstructor<BlocBase<any>>;
|
|
285
399
|
const isIsolated = bloc.isIsolated;
|
|
286
400
|
|
|
@@ -296,7 +410,7 @@ export class Blac {
|
|
|
296
410
|
this.registerBlocInstance(bloc);
|
|
297
411
|
}
|
|
298
412
|
};
|
|
299
|
-
static activateBloc
|
|
413
|
+
static get activateBloc() { return Blac.instance.activateBloc; }
|
|
300
414
|
|
|
301
415
|
/**
|
|
302
416
|
* Gets or creates a bloc instance based on the provided class and options.
|
|
@@ -348,7 +462,7 @@ export class Blac {
|
|
|
348
462
|
this.log(`[${blocClass.name}:${String(blocId)}] (getBloc) No existing instance found. Creating new one.`, options, bloc);
|
|
349
463
|
return bloc;
|
|
350
464
|
};
|
|
351
|
-
static getBloc
|
|
465
|
+
static get getBloc() { return Blac.instance.getBloc; }
|
|
352
466
|
|
|
353
467
|
/**
|
|
354
468
|
* Gets a bloc instance or throws an error if it doesn't exist
|
|
@@ -358,7 +472,7 @@ export class Blac {
|
|
|
358
472
|
* - props: Properties to pass to the bloc constructor
|
|
359
473
|
* - instanceRef: Optional reference string for the instance
|
|
360
474
|
*/
|
|
361
|
-
getBlocOrThrow = <B extends BlocConstructor<
|
|
475
|
+
getBlocOrThrow = <B extends BlocConstructor<any>>(
|
|
362
476
|
blocClass: B,
|
|
363
477
|
options: {
|
|
364
478
|
id?: BlocInstanceId;
|
|
@@ -378,7 +492,7 @@ export class Blac {
|
|
|
378
492
|
}
|
|
379
493
|
throw new Error(`Bloc ${blocClass.name} not found`);
|
|
380
494
|
};
|
|
381
|
-
static getBlocOrThrow
|
|
495
|
+
static get getBlocOrThrow() { return Blac.instance.getBlocOrThrow; }
|
|
382
496
|
|
|
383
497
|
/**
|
|
384
498
|
* Gets all instances of a specific bloc class
|
|
@@ -387,7 +501,7 @@ export class Blac {
|
|
|
387
501
|
* - searchIsolated: Whether to search in isolated blocs (defaults to bloc's isolated property)
|
|
388
502
|
* @returns Array of matching bloc instances
|
|
389
503
|
*/
|
|
390
|
-
getAllBlocs = <B extends BlocConstructor<
|
|
504
|
+
getAllBlocs = <B extends BlocConstructor<any>>(
|
|
391
505
|
blocClass: B,
|
|
392
506
|
options: {
|
|
393
507
|
searchIsolated?: boolean;
|
|
@@ -413,4 +527,83 @@ export class Blac {
|
|
|
413
527
|
|
|
414
528
|
return results;
|
|
415
529
|
};
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Disposes all keep-alive blocs of a specific type
|
|
533
|
+
* @param blocClass - The bloc class to dispose
|
|
534
|
+
*/
|
|
535
|
+
disposeKeepAliveBlocs = <B extends BlocConstructor<any>>(
|
|
536
|
+
blocClass?: B
|
|
537
|
+
): void => {
|
|
538
|
+
const toDispose: BlocBase<unknown>[] = [];
|
|
539
|
+
|
|
540
|
+
for (const bloc of this.keepAliveBlocs) {
|
|
541
|
+
if (!blocClass || bloc.constructor === blocClass) {
|
|
542
|
+
toDispose.push(bloc);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
toDispose.forEach(bloc => bloc._dispose());
|
|
547
|
+
};
|
|
548
|
+
static get disposeKeepAliveBlocs() { return Blac.instance.disposeKeepAliveBlocs; }
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Disposes all blocs matching a pattern
|
|
552
|
+
* @param predicate - Function to test each bloc for disposal
|
|
553
|
+
*/
|
|
554
|
+
disposeBlocs = (predicate: (bloc: BlocBase<unknown>) => boolean): void => {
|
|
555
|
+
const toDispose: BlocBase<unknown>[] = [];
|
|
556
|
+
|
|
557
|
+
// Check registered blocs
|
|
558
|
+
for (const bloc of this.blocInstanceMap.values()) {
|
|
559
|
+
if (predicate(bloc)) {
|
|
560
|
+
toDispose.push(bloc);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Check isolated blocs
|
|
565
|
+
for (const blocs of this.isolatedBlocMap.values()) {
|
|
566
|
+
for (const bloc of blocs) {
|
|
567
|
+
if (predicate(bloc)) {
|
|
568
|
+
toDispose.push(bloc);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
toDispose.forEach(bloc => bloc._dispose());
|
|
574
|
+
};
|
|
575
|
+
static get disposeBlocs() { return Blac.instance.disposeBlocs; }
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Gets memory usage statistics for debugging
|
|
579
|
+
*/
|
|
580
|
+
getMemoryStats = () => {
|
|
581
|
+
return {
|
|
582
|
+
totalBlocs: this.uidRegistry.size,
|
|
583
|
+
registeredBlocs: this.blocInstanceMap.size,
|
|
584
|
+
isolatedBlocs: Array.from(this.isolatedBlocMap.values()).reduce((sum, arr) => sum + arr.length, 0),
|
|
585
|
+
keepAliveBlocs: this.keepAliveBlocs.size,
|
|
586
|
+
isolatedBlocTypes: this.isolatedBlocMap.size,
|
|
587
|
+
};
|
|
588
|
+
};
|
|
589
|
+
static get getMemoryStats() { return Blac.instance.getMemoryStats; }
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Validates consumer references and cleans up orphaned consumers
|
|
593
|
+
*/
|
|
594
|
+
validateConsumers = (): void => {
|
|
595
|
+
for (const bloc of this.uidRegistry.values()) {
|
|
596
|
+
// This would need to be called periodically to clean up orphaned consumers
|
|
597
|
+
// Implementation depends on how we want to handle consumer validation
|
|
598
|
+
if (bloc._consumers.size === 0 && !bloc._keepAlive) {
|
|
599
|
+
// Schedule disposal for blocs with no consumers
|
|
600
|
+
setTimeout(() => {
|
|
601
|
+
if (bloc._consumers.size === 0 && !bloc._keepAlive) {
|
|
602
|
+
bloc._dispose();
|
|
603
|
+
}
|
|
604
|
+
}, 1000); // Give a grace period
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
static get validateConsumers() { return Blac.instance.validateConsumers; }
|
|
416
609
|
}
|
package/src/BlocBase.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Blac } from './Blac';
|
|
2
1
|
import { BlacObservable } from './BlacObserver';
|
|
3
2
|
import { BlocConstructor } from './types';
|
|
4
3
|
|
|
@@ -62,12 +61,6 @@ export abstract class BlocBase<
|
|
|
62
61
|
*/
|
|
63
62
|
public _observer: BlacObservable<S>;
|
|
64
63
|
|
|
65
|
-
/**
|
|
66
|
-
* @internal
|
|
67
|
-
* Reference to the global Blac manager instance.
|
|
68
|
-
*/
|
|
69
|
-
public _blac = Blac.getInstance();
|
|
70
|
-
|
|
71
64
|
/**
|
|
72
65
|
* The unique identifier for this Bloc instance.
|
|
73
66
|
* Defaults to the class name, but can be customized.
|
|
@@ -119,14 +112,14 @@ export abstract class BlocBase<
|
|
|
119
112
|
constructor(initialState: S) {
|
|
120
113
|
this._state = initialState;
|
|
121
114
|
this._observer = new BlacObservable(this);
|
|
122
|
-
Blac.log('Bloc Created', this)
|
|
123
115
|
this._id = this.constructor.name;
|
|
124
116
|
|
|
125
|
-
//
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
this.
|
|
117
|
+
// Access static properties safely with proper type checking
|
|
118
|
+
const Constructor = this.constructor as typeof BlocBase & BlocStaticProperties;
|
|
119
|
+
|
|
120
|
+
// Validate that the static properties exist and are boolean
|
|
121
|
+
this._keepAlive = typeof Constructor.keepAlive === 'boolean' ? Constructor.keepAlive : false;
|
|
122
|
+
this._isolated = typeof Constructor.isolated === 'boolean' ? Constructor.isolated : false;
|
|
130
123
|
}
|
|
131
124
|
|
|
132
125
|
/**
|
|
@@ -164,11 +157,17 @@ export abstract class BlocBase<
|
|
|
164
157
|
* Notifies the Blac manager and clears all observers.
|
|
165
158
|
*/
|
|
166
159
|
_dispose() {
|
|
160
|
+
// Clear all consumers
|
|
161
|
+
this._consumers.clear();
|
|
162
|
+
|
|
163
|
+
// Clear observer subscriptions
|
|
167
164
|
this._observer.clear();
|
|
165
|
+
|
|
166
|
+
// Call user-defined disposal hook
|
|
168
167
|
this.onDispose?.();
|
|
169
|
-
|
|
170
|
-
Blac
|
|
171
|
-
Blac.
|
|
168
|
+
|
|
169
|
+
// The Blac manager will handle removal from registry
|
|
170
|
+
// This method is called by Blac.disposeBloc, so we don't need to call it again
|
|
172
171
|
}
|
|
173
172
|
|
|
174
173
|
/**
|
|
@@ -183,18 +182,29 @@ export abstract class BlocBase<
|
|
|
183
182
|
*/
|
|
184
183
|
_consumers = new Set<string>();
|
|
185
184
|
|
|
185
|
+
/**
|
|
186
|
+
* @internal
|
|
187
|
+
* WeakSet to track consumer references for cleanup validation
|
|
188
|
+
*/
|
|
189
|
+
private _consumerRefs = new WeakSet<object>();
|
|
190
|
+
|
|
186
191
|
/**
|
|
187
192
|
* @internal
|
|
188
193
|
* Registers a new consumer to this Bloc instance.
|
|
189
194
|
* Notifies the Blac manager that a consumer has been added.
|
|
190
195
|
*
|
|
191
196
|
* @param consumerId The unique ID of the consumer being added
|
|
197
|
+
* @param consumerRef Optional reference to the consumer object for cleanup validation
|
|
192
198
|
*/
|
|
193
|
-
_addConsumer = (consumerId: string) => {
|
|
199
|
+
_addConsumer = (consumerId: string, consumerRef?: object) => {
|
|
194
200
|
if (this._consumers.has(consumerId)) return;
|
|
195
201
|
this._consumers.add(consumerId);
|
|
202
|
+
|
|
203
|
+
if (consumerRef) {
|
|
204
|
+
this._consumerRefs.add(consumerRef);
|
|
205
|
+
}
|
|
206
|
+
|
|
196
207
|
// this._blac.dispatchEvent(BlacLifecycleEvent.BLOC_CONSUMER_ADDED, this, { consumerId });
|
|
197
|
-
Blac.log('BlocBase._addConsumer', this, consumerId);
|
|
198
208
|
};
|
|
199
209
|
|
|
200
210
|
/**
|
|
@@ -208,11 +218,58 @@ export abstract class BlocBase<
|
|
|
208
218
|
if (!this._consumers.has(consumerId)) return;
|
|
209
219
|
this._consumers.delete(consumerId);
|
|
210
220
|
// this._blac.dispatchEvent(BlacLifecycleEvent.BLOC_CONSUMER_REMOVED, this, { consumerId });
|
|
211
|
-
|
|
221
|
+
|
|
222
|
+
// If no consumers remain and not keep-alive, schedule disposal
|
|
223
|
+
if (this._consumers.size === 0 && !this._keepAlive) {
|
|
224
|
+
this._scheduleDisposal();
|
|
225
|
+
}
|
|
212
226
|
};
|
|
213
227
|
|
|
228
|
+
/**
|
|
229
|
+
* @internal
|
|
230
|
+
* Handler function for disposal (can be set by Blac manager)
|
|
231
|
+
*/
|
|
232
|
+
private _disposalHandler?: (bloc: BlocBase<unknown>) => void;
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* @internal
|
|
236
|
+
* Sets the disposal handler for this bloc
|
|
237
|
+
*/
|
|
238
|
+
_setDisposalHandler(handler: (bloc: BlocBase<unknown>) => void) {
|
|
239
|
+
this._disposalHandler = handler;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* @internal
|
|
244
|
+
* Schedules disposal of this bloc instance if it has no consumers
|
|
245
|
+
*/
|
|
246
|
+
private _scheduleDisposal() {
|
|
247
|
+
// Use setTimeout to avoid disposal during render cycles
|
|
248
|
+
setTimeout(() => {
|
|
249
|
+
if (this._consumers.size === 0 && !this._keepAlive) {
|
|
250
|
+
if (this._disposalHandler) {
|
|
251
|
+
this._disposalHandler(this as any);
|
|
252
|
+
} else {
|
|
253
|
+
this._dispose();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}, 0);
|
|
257
|
+
}
|
|
258
|
+
|
|
214
259
|
lastUpdate = Date.now();
|
|
215
260
|
|
|
261
|
+
/**
|
|
262
|
+
* @internal
|
|
263
|
+
* Flag to indicate if batching is enabled for this bloc
|
|
264
|
+
*/
|
|
265
|
+
private _batchingEnabled = false;
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* @internal
|
|
269
|
+
* Pending state updates when batching is enabled
|
|
270
|
+
*/
|
|
271
|
+
private _pendingUpdates: Array<{newState: S, oldState: S, action?: unknown}> = [];
|
|
272
|
+
|
|
216
273
|
/**
|
|
217
274
|
* @internal
|
|
218
275
|
* Updates the state and notifies all observers of the change.
|
|
@@ -222,8 +279,59 @@ export abstract class BlocBase<
|
|
|
222
279
|
* @param action Optional metadata about what caused the state change
|
|
223
280
|
*/
|
|
224
281
|
_pushState = (newState: S, oldState: S, action?: unknown): void => {
|
|
282
|
+
// Runtime validation for state changes
|
|
283
|
+
if (newState === undefined) {
|
|
284
|
+
console.warn('BlocBase._pushState: newState is undefined', this);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Validate action type if provided
|
|
289
|
+
if (action !== undefined && action !== null) {
|
|
290
|
+
const actionType = typeof action;
|
|
291
|
+
if (!(['string', 'object', 'number'].includes(actionType))) {
|
|
292
|
+
console.warn('BlocBase._pushState: Invalid action type', this, action);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// If batching is enabled, queue the update
|
|
297
|
+
if (this._batchingEnabled) {
|
|
298
|
+
this._pendingUpdates.push({ newState, oldState, action });
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
this._oldState = oldState;
|
|
225
303
|
this._state = newState;
|
|
226
304
|
this._observer.notify(newState, oldState, action);
|
|
227
305
|
this.lastUpdate = Date.now();
|
|
228
306
|
};
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Enables batching for multiple state updates
|
|
310
|
+
* @param batchFn Function to execute with batching enabled
|
|
311
|
+
*/
|
|
312
|
+
batch = <T>(batchFn: () => T): T => {
|
|
313
|
+
const wasBatching = this._batchingEnabled;
|
|
314
|
+
this._batchingEnabled = true;
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
const result = batchFn();
|
|
318
|
+
|
|
319
|
+
// Process all pending updates
|
|
320
|
+
if (this._pendingUpdates.length > 0) {
|
|
321
|
+
const lastUpdate = this._pendingUpdates[this._pendingUpdates.length - 1];
|
|
322
|
+
this._oldState = this._pendingUpdates[0].oldState;
|
|
323
|
+
this._state = lastUpdate.newState;
|
|
324
|
+
|
|
325
|
+
// Notify with the final state
|
|
326
|
+
this._observer.notify(lastUpdate.newState, this._oldState, lastUpdate.action);
|
|
327
|
+
this.lastUpdate = Date.now();
|
|
328
|
+
|
|
329
|
+
this._pendingUpdates = [];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return result;
|
|
333
|
+
} finally {
|
|
334
|
+
this._batchingEnabled = wasBatching;
|
|
335
|
+
}
|
|
336
|
+
};
|
|
229
337
|
}
|