@blac/core 2.0.0-rc-13 → 2.0.0-rc-16
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/package.json +10 -15
- package/src/Blac.ts +38 -71
- package/src/Bloc.ts +8 -9
- package/src/BlocBase.ts +246 -540
- package/src/Cubit.ts +1 -16
- package/src/__tests__/Blac.config.test.ts +10 -17
- package/src/__tests__/Bloc.event.test.ts +27 -52
- package/src/__tests__/BlocBase.subscription.test.ts +349 -0
- package/src/__tests__/Cubit.test.ts +39 -50
- package/src/__tests__/memory-leaks.test.ts +383 -0
- package/src/__tests__/plugins.test.ts +107 -97
- package/src/adapter/BlacAdapter.ts +347 -207
- package/src/adapter/ProxyFactory.ts +196 -255
- package/src/adapter/__tests__/BlacAdapter.test.ts +71 -77
- package/src/adapter/__tests__/ProxyFactory.test.ts +1 -1
- package/src/adapter/index.ts +0 -1
- package/src/errors/BlacError.ts +69 -0
- package/src/errors/ErrorManager.ts +101 -0
- package/src/errors/handleError.ts +114 -0
- package/src/errors/index.ts +13 -0
- package/src/events.ts +7 -1
- package/src/index.ts +7 -1
- package/src/lifecycle/BlocLifecycle.ts +156 -0
- package/src/lifecycle/index.ts +1 -0
- package/src/plugins/BlocPluginRegistry.ts +71 -51
- package/src/plugins/SystemPluginRegistry.ts +63 -13
- package/src/plugins/__tests__/BlocPluginRegistry.test.ts +450 -0
- package/src/plugins/__tests__/SystemPluginRegistry.test.ts +404 -0
- package/src/plugins/types.ts +32 -3
- package/src/subscription/README.md +132 -0
- package/src/subscription/SubscriptionManager.ts +395 -0
- package/src/subscription/__tests__/SubscriptionManager.test.ts +355 -0
- package/src/subscription/index.ts +2 -0
- package/src/subscription/types.ts +90 -0
- package/src/testing.ts +31 -37
- package/src/types.ts +4 -21
- package/src/utils/BatchingManager.ts +59 -0
- package/src/utils/RerenderLogger.ts +265 -0
- package/src/utils/__tests__/generateInstanceId.test.ts +104 -0
- package/src/utils/__tests__/shallowEqual.test.ts +171 -0
- package/src/utils/__tests__/uuid.test.ts +144 -0
- package/src/utils/generateInstanceId.ts +54 -0
- package/src/utils/shallowEqual.ts +1 -1
- package/README.md +0 -329
- package/dist/index.cjs.js +0 -11
- package/dist/index.cjs.js.map +0 -1
- package/dist/index.d.ts +0 -1078
- package/dist/index.es.js +0 -1992
- package/dist/index.es.js.map +0 -1
- package/src/BlacObserver.ts +0 -189
- package/src/__tests__/BlocBase.lifecycle.test.ts +0 -441
- package/src/__tests__/props.test.ts +0 -256
- package/src/adapter/ConsumerTracker.ts +0 -241
- package/src/adapter/__tests__/ConsumerTracker.test.ts +0 -562
package/src/BlocBase.ts
CHANGED
|
@@ -1,36 +1,17 @@
|
|
|
1
|
-
import { BlacObservable } from './BlacObserver';
|
|
2
1
|
import { generateUUID } from './utils/uuid';
|
|
3
|
-
import { BlocPlugin
|
|
2
|
+
import { BlocPlugin } from './plugins/types';
|
|
4
3
|
import { BlocPluginRegistry } from './plugins/BlocPluginRegistry';
|
|
4
|
+
import { SubscriptionManager } from './subscription/SubscriptionManager';
|
|
5
|
+
import {
|
|
6
|
+
BlocLifecycleManager,
|
|
7
|
+
BlocLifecycleState,
|
|
8
|
+
StateTransitionResult,
|
|
9
|
+
} from './lifecycle/BlocLifecycle';
|
|
10
|
+
import { BatchingManager } from './utils/BatchingManager';
|
|
11
|
+
import type { Blac } from './Blac';
|
|
5
12
|
|
|
6
13
|
export type BlocInstanceId = string | number | undefined;
|
|
7
|
-
type DependencySelector<S> = (
|
|
8
|
-
currentState: S,
|
|
9
|
-
previousState: S | undefined,
|
|
10
|
-
instance: any,
|
|
11
|
-
) => unknown[];
|
|
12
14
|
|
|
13
|
-
/**
|
|
14
|
-
* Enum representing the lifecycle states of a Bloc instance
|
|
15
|
-
* Used for atomic state transitions to prevent race conditions
|
|
16
|
-
*/
|
|
17
|
-
export enum BlocLifecycleState {
|
|
18
|
-
ACTIVE = 'active',
|
|
19
|
-
DISPOSAL_REQUESTED = 'disposal_requested',
|
|
20
|
-
DISPOSING = 'disposing',
|
|
21
|
-
DISPOSED = 'disposed',
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Result of an atomic state transition operation
|
|
26
|
-
*/
|
|
27
|
-
export interface StateTransitionResult {
|
|
28
|
-
success: boolean;
|
|
29
|
-
currentState: BlocLifecycleState;
|
|
30
|
-
previousState: BlocLifecycleState;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Define an interface for the static properties expected on a Bloc/Cubit constructor
|
|
34
15
|
interface BlocStaticProperties {
|
|
35
16
|
isolated: boolean;
|
|
36
17
|
keepAlive: boolean;
|
|
@@ -38,180 +19,57 @@ interface BlocStaticProperties {
|
|
|
38
19
|
}
|
|
39
20
|
|
|
40
21
|
/**
|
|
41
|
-
* Base class for both Blocs and Cubits
|
|
42
|
-
* Handles state transitions, observer notifications, lifecycle management, and addon integration.
|
|
43
|
-
*
|
|
44
|
-
* @abstract This class should be extended, not instantiated directly
|
|
45
|
-
* @template S The type of state managed by this Bloc
|
|
46
|
-
* @template P The type of props that can be passed during instance creation (optional)
|
|
22
|
+
* Base class for both Blocs and Cubits using unified subscription model.
|
|
47
23
|
*/
|
|
48
|
-
export abstract class BlocBase<S
|
|
24
|
+
export abstract class BlocBase<S> {
|
|
49
25
|
public uid = generateUUID();
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
* Use this when state should not be shared between components.
|
|
53
|
-
* @default false
|
|
54
|
-
*/
|
|
26
|
+
blacInstance?: Blac;
|
|
27
|
+
|
|
55
28
|
static isolated = false;
|
|
56
29
|
get isIsolated() {
|
|
57
30
|
return this._isolated;
|
|
58
31
|
}
|
|
59
32
|
|
|
60
|
-
/**
|
|
61
|
-
* When true, the Bloc instance persists even when there are no active consumers.
|
|
62
|
-
* Useful for maintaining state between component unmount/remount cycles.
|
|
63
|
-
* @default false
|
|
64
|
-
*/
|
|
65
33
|
static keepAlive = false;
|
|
66
34
|
get isKeepAlive() {
|
|
67
35
|
return this._keepAlive;
|
|
68
36
|
}
|
|
69
37
|
|
|
70
|
-
/**
|
|
71
|
-
* Defines how dependencies are selected from the state for efficient updates.
|
|
72
|
-
* When provided, observers will only be notified when selected dependencies change.
|
|
73
|
-
*/
|
|
74
|
-
defaultDependencySelector: DependencySelector<S> | undefined;
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* @internal
|
|
78
|
-
* Indicates if this specific Bloc instance is isolated from others of the same type.
|
|
79
|
-
*/
|
|
80
38
|
public _isolated = false;
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* @internal
|
|
84
|
-
* Observable responsible for managing state listeners and notifying consumers.
|
|
85
|
-
*/
|
|
86
|
-
public _observer: BlacObservable<S>;
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* The unique identifier for this Bloc instance.
|
|
90
|
-
* Defaults to the class name, but can be customized.
|
|
91
|
-
*/
|
|
92
39
|
public _id: BlocInstanceId;
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* @internal
|
|
96
|
-
* Reference string used internally for tracking and debugging.
|
|
97
|
-
*/
|
|
98
40
|
public _instanceRef?: string;
|
|
41
|
+
public _name: string;
|
|
99
42
|
|
|
100
43
|
/**
|
|
101
|
-
*
|
|
102
|
-
* Indicates if this specific Bloc instance should be kept alive when no consumers are present.
|
|
103
|
-
*/
|
|
104
|
-
public _keepAlive = false;
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* @readonly
|
|
108
|
-
* Timestamp when this Bloc instance was created, useful for debugging and performance tracking.
|
|
109
|
-
*/
|
|
110
|
-
public readonly _createdAt = Date.now();
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* @internal
|
|
114
|
-
* Atomic disposal state to prevent race conditions
|
|
115
|
-
*/
|
|
116
|
-
private _disposalState: BlocLifecycleState = BlocLifecycleState.ACTIVE;
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* @internal
|
|
120
|
-
* Timestamp when disposal was requested (for React Strict Mode grace period)
|
|
121
|
-
*/
|
|
122
|
-
private _disposalRequestTime: number = 0;
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* @internal
|
|
126
|
-
* The current state of the Bloc.
|
|
44
|
+
* Unified subscription manager for all state notifications
|
|
127
45
|
*/
|
|
128
|
-
public
|
|
46
|
+
public _subscriptionManager: SubscriptionManager<S>;
|
|
129
47
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
public
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Props passed during Bloc instance creation.
|
|
138
|
-
* Can be used to configure or parameterize the Bloc's behavior.
|
|
139
|
-
*/
|
|
140
|
-
public props: P | null = null;
|
|
48
|
+
_state: S;
|
|
49
|
+
private _lifecycleManager = new BlocLifecycleManager();
|
|
50
|
+
private _batchingManager = new BatchingManager<S>();
|
|
51
|
+
_keepAlive = false;
|
|
52
|
+
public lastUpdate?: number;
|
|
141
53
|
|
|
142
|
-
|
|
143
|
-
* @internal
|
|
144
|
-
* Flag to prevent batching race conditions
|
|
145
|
-
*/
|
|
146
|
-
private _batchingLock = false;
|
|
54
|
+
_plugins = new BlocPluginRegistry<S, any>();
|
|
147
55
|
|
|
148
|
-
|
|
149
|
-
* @internal
|
|
150
|
-
* Pending batched updates
|
|
151
|
-
*/
|
|
152
|
-
private _pendingUpdates: Array<{
|
|
153
|
-
newState: S;
|
|
154
|
-
oldState: S;
|
|
155
|
-
action?: unknown;
|
|
156
|
-
}> = [];
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* @internal
|
|
160
|
-
* Map of consumer IDs to their WeakRef objects for proper cleanup
|
|
161
|
-
*/
|
|
162
|
-
private _consumerRefs = new Map<string, WeakRef<object>>();
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Plugin registry for this bloc instance
|
|
166
|
-
*/
|
|
167
|
-
protected _plugins: BlocPluginRegistry<S, any>;
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* @internal
|
|
171
|
-
* Validates that all consumer references are still alive
|
|
172
|
-
* Removes dead consumers automatically
|
|
173
|
-
*/
|
|
174
|
-
_validateConsumers = (): void => {
|
|
175
|
-
const deadConsumers: string[] = [];
|
|
176
|
-
|
|
177
|
-
for (const [consumerId, weakRef] of this._consumerRefs) {
|
|
178
|
-
if (weakRef.deref() === undefined) {
|
|
179
|
-
deadConsumers.push(consumerId);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Clean up dead consumers
|
|
184
|
-
for (const consumerId of deadConsumers) {
|
|
185
|
-
this._consumers.delete(consumerId);
|
|
186
|
-
this._consumerRefs.delete(consumerId);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Schedule disposal if no live consumers remain
|
|
190
|
-
if (
|
|
191
|
-
this._consumers.size === 0 &&
|
|
192
|
-
!this._keepAlive &&
|
|
193
|
-
this._disposalState === BlocLifecycleState.ACTIVE
|
|
194
|
-
) {
|
|
195
|
-
this._scheduleDisposal();
|
|
196
|
-
}
|
|
197
|
-
};
|
|
56
|
+
onDispose?: () => void;
|
|
198
57
|
|
|
199
58
|
/**
|
|
200
|
-
* Creates a new BlocBase instance with
|
|
201
|
-
* Sets up the observer, registers with the Blac manager, and initializes addons.
|
|
202
|
-
*
|
|
203
|
-
* @param initialState The initial state value for this Bloc
|
|
59
|
+
* Creates a new BlocBase instance with unified subscription management
|
|
204
60
|
*/
|
|
205
61
|
constructor(initialState: S) {
|
|
206
62
|
this._state = initialState;
|
|
207
|
-
this._observer = new BlacObservable(this);
|
|
208
63
|
this._id = this.constructor.name;
|
|
64
|
+
this._name = this.constructor.name;
|
|
65
|
+
|
|
66
|
+
// Initialize unified subscription system
|
|
67
|
+
this._subscriptionManager = new SubscriptionManager(this as any);
|
|
209
68
|
|
|
210
|
-
// Access static properties
|
|
69
|
+
// Access static properties
|
|
211
70
|
const Constructor = this.constructor as typeof BlocBase &
|
|
212
71
|
BlocStaticProperties;
|
|
213
72
|
|
|
214
|
-
// Validate that the static properties exist and are boolean
|
|
215
73
|
this._keepAlive =
|
|
216
74
|
typeof Constructor.keepAlive === 'boolean'
|
|
217
75
|
? Constructor.keepAlive
|
|
@@ -225,468 +83,316 @@ export abstract class BlocBase<S, P = unknown> {
|
|
|
225
83
|
// Register static plugins
|
|
226
84
|
if (Constructor.plugins && Array.isArray(Constructor.plugins)) {
|
|
227
85
|
for (const plugin of Constructor.plugins) {
|
|
228
|
-
this.
|
|
86
|
+
this._plugins.add(plugin);
|
|
229
87
|
}
|
|
230
88
|
}
|
|
89
|
+
|
|
90
|
+
// Attach all plugins
|
|
91
|
+
this._plugins.attach(this);
|
|
231
92
|
}
|
|
232
93
|
|
|
233
94
|
/**
|
|
234
|
-
* Returns the current state
|
|
235
|
-
* Use this getter to access the state in a read-only manner.
|
|
236
|
-
* Returns the state even during transitional lifecycle states for React compatibility.
|
|
95
|
+
* Returns the current state
|
|
237
96
|
*/
|
|
238
97
|
get state(): S {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
// Return the last known state for disposed blocs to prevent crashes
|
|
242
|
-
return this._state;
|
|
98
|
+
if (this._lifecycleManager.isDisposed) {
|
|
99
|
+
return this._state; // Return last known state for disposed blocs
|
|
243
100
|
}
|
|
244
101
|
return this._state;
|
|
245
102
|
}
|
|
246
103
|
|
|
247
104
|
/**
|
|
248
|
-
*
|
|
249
|
-
* @returns true if the bloc is in DISPOSED state
|
|
105
|
+
* Subscribe to all state changes
|
|
250
106
|
*/
|
|
251
|
-
|
|
252
|
-
return this.
|
|
107
|
+
subscribe(callback: (state: S) => void): () => void {
|
|
108
|
+
return this._subscriptionManager.subscribe({
|
|
109
|
+
type: 'observer',
|
|
110
|
+
notify: (state) => callback(state as S),
|
|
111
|
+
});
|
|
253
112
|
}
|
|
254
113
|
|
|
255
114
|
/**
|
|
256
|
-
*
|
|
257
|
-
* Returns the name of the Bloc class for identification and debugging.
|
|
115
|
+
* Subscribe with a selector for optimized updates
|
|
258
116
|
*/
|
|
259
|
-
|
|
260
|
-
|
|
117
|
+
subscribeWithSelector<T>(
|
|
118
|
+
selector: (state: S) => T,
|
|
119
|
+
callback: (value: T) => void,
|
|
120
|
+
equalityFn?: (a: T, b: T) => boolean,
|
|
121
|
+
): () => void {
|
|
122
|
+
return this._subscriptionManager.subscribe({
|
|
123
|
+
type: 'consumer',
|
|
124
|
+
selector: selector as any,
|
|
125
|
+
equalityFn: equalityFn as any,
|
|
126
|
+
notify: (value) => callback(value as T),
|
|
127
|
+
});
|
|
261
128
|
}
|
|
262
129
|
|
|
263
130
|
/**
|
|
264
|
-
*
|
|
265
|
-
* Updates the Bloc instance's ID to a new value.
|
|
266
|
-
* Only updates if the new ID is defined and different from the current one.
|
|
267
|
-
*
|
|
268
|
-
* @param id The new ID to assign to this Bloc instance
|
|
131
|
+
* Subscribe with React component reference for automatic cleanup
|
|
269
132
|
*/
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
* @param expectedState The expected current state
|
|
280
|
-
* @param newState The desired new state
|
|
281
|
-
* @returns Result indicating success/failure and state information
|
|
282
|
-
*/
|
|
283
|
-
_atomicStateTransition(
|
|
284
|
-
expectedState: BlocLifecycleState,
|
|
285
|
-
newState: BlocLifecycleState,
|
|
286
|
-
): StateTransitionResult {
|
|
287
|
-
if (this._disposalState === expectedState) {
|
|
288
|
-
const previousState = this._disposalState;
|
|
289
|
-
this._disposalState = newState;
|
|
290
|
-
|
|
291
|
-
// Log state transition for debugging
|
|
292
|
-
if ((globalThis as any).Blac?.enableLog) {
|
|
293
|
-
(globalThis as any).Blac?.log(
|
|
294
|
-
`[${this._name}:${this._id}] State transition: ${previousState} -> ${newState} (SUCCESS)`,
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return {
|
|
299
|
-
success: true,
|
|
300
|
-
currentState: newState,
|
|
301
|
-
previousState,
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Log failed transition attempt
|
|
306
|
-
if ((globalThis as any).Blac?.enableLog) {
|
|
307
|
-
(globalThis as any).Blac?.log(
|
|
308
|
-
`[${this._name}:${this._id}] State transition failed: expected ${expectedState}, current ${this._disposalState}`,
|
|
309
|
-
);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return {
|
|
313
|
-
success: false,
|
|
314
|
-
currentState: this._disposalState,
|
|
315
|
-
previousState: expectedState,
|
|
316
|
-
};
|
|
133
|
+
subscribeComponent(
|
|
134
|
+
componentRef: WeakRef<object>,
|
|
135
|
+
callback: () => void,
|
|
136
|
+
): () => void {
|
|
137
|
+
return this._subscriptionManager.subscribe({
|
|
138
|
+
type: 'consumer',
|
|
139
|
+
weakRef: componentRef,
|
|
140
|
+
notify: callback,
|
|
141
|
+
});
|
|
317
142
|
}
|
|
318
143
|
|
|
319
144
|
/**
|
|
320
|
-
*
|
|
321
|
-
* Cleans up resources and removes this Bloc from the system.
|
|
322
|
-
* Notifies the Blac manager and clears all observers.
|
|
145
|
+
* Get current subscription count
|
|
323
146
|
*/
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
let transitionResult = this._atomicStateTransition(
|
|
327
|
-
BlocLifecycleState.ACTIVE,
|
|
328
|
-
BlocLifecycleState.DISPOSING,
|
|
329
|
-
);
|
|
330
|
-
|
|
331
|
-
// If that failed, try from DISPOSAL_REQUESTED state
|
|
332
|
-
if (!transitionResult.success) {
|
|
333
|
-
transitionResult = this._atomicStateTransition(
|
|
334
|
-
BlocLifecycleState.DISPOSAL_REQUESTED,
|
|
335
|
-
BlocLifecycleState.DISPOSING,
|
|
336
|
-
);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (!transitionResult.success) {
|
|
340
|
-
// Already disposing or disposed - idempotent operation
|
|
341
|
-
return false;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
try {
|
|
345
|
-
// Step 2: Perform cleanup operations
|
|
346
|
-
this._consumers.clear();
|
|
347
|
-
this._consumerRefs.clear();
|
|
348
|
-
this._observer.clear();
|
|
349
|
-
|
|
350
|
-
// Call user-defined disposal hook
|
|
351
|
-
this.onDispose?.();
|
|
352
|
-
|
|
353
|
-
// Step 3: Final state transition to DISPOSED
|
|
354
|
-
const finalResult = this._atomicStateTransition(
|
|
355
|
-
BlocLifecycleState.DISPOSING,
|
|
356
|
-
BlocLifecycleState.DISPOSED,
|
|
357
|
-
);
|
|
358
|
-
|
|
359
|
-
return finalResult.success;
|
|
360
|
-
} catch (error) {
|
|
361
|
-
// Recovery: Reset state on cleanup failure
|
|
362
|
-
this._disposalState = BlocLifecycleState.ACTIVE;
|
|
363
|
-
throw error;
|
|
364
|
-
}
|
|
147
|
+
get subscriptionCount(): number {
|
|
148
|
+
return this._subscriptionManager.size;
|
|
365
149
|
}
|
|
366
150
|
|
|
367
151
|
/**
|
|
368
|
-
*
|
|
369
|
-
* Optional function to be called when the Bloc is disposed.
|
|
152
|
+
* Track property access for a subscription
|
|
370
153
|
*/
|
|
371
|
-
|
|
154
|
+
trackAccess(subscriptionId: string, path: string, value?: unknown): void {
|
|
155
|
+
this._subscriptionManager.trackAccess(subscriptionId, path, value);
|
|
156
|
+
}
|
|
372
157
|
|
|
373
158
|
/**
|
|
374
|
-
*
|
|
375
|
-
* Set of consumer IDs currently listening to this Bloc's state changes.
|
|
159
|
+
* Emit a new state
|
|
376
160
|
*/
|
|
377
|
-
|
|
161
|
+
protected emit(newState: S, action?: unknown): void {
|
|
162
|
+
this._pushState(newState, this._state, action);
|
|
163
|
+
}
|
|
378
164
|
|
|
379
165
|
/**
|
|
380
|
-
*
|
|
381
|
-
* Registers a new consumer to this Bloc instance.
|
|
382
|
-
* Notifies the Blac manager that a consumer has been added.
|
|
383
|
-
*
|
|
384
|
-
* @param consumerId The unique ID of the consumer being added
|
|
385
|
-
* @param consumerRef Optional reference to the consumer object for cleanup validation
|
|
166
|
+
* Internal state push method used by Bloc
|
|
386
167
|
*/
|
|
387
|
-
|
|
388
|
-
//
|
|
389
|
-
if (this.
|
|
390
|
-
|
|
168
|
+
_pushState(newState: S, oldState: S, action?: unknown): void {
|
|
169
|
+
// Validate state emission conditions
|
|
170
|
+
if (this._lifecycleManager.currentState !== BlocLifecycleState.ACTIVE) {
|
|
171
|
+
this.blacInstance?.error(
|
|
172
|
+
`[${this._name}:${this._id}] Attempted state update on ${this._lifecycleManager.currentState} bloc. Update ignored.`,
|
|
173
|
+
);
|
|
174
|
+
return;
|
|
391
175
|
}
|
|
392
176
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
// Safe consumer addition
|
|
397
|
-
this._consumers.add(consumerId);
|
|
398
|
-
|
|
399
|
-
// Store WeakRef for proper memory management
|
|
400
|
-
if (consumerRef) {
|
|
401
|
-
this._consumerRefs.set(consumerId, new WeakRef(consumerRef));
|
|
177
|
+
if (newState === undefined) {
|
|
178
|
+
return; // Silent failure for undefined states
|
|
402
179
|
}
|
|
403
180
|
|
|
404
|
-
|
|
405
|
-
(globalThis as any).Blac?.log(
|
|
406
|
-
`[${this._name}:${this._id}] Consumer added. Total consumers: ${this._consumers.size}`,
|
|
407
|
-
);
|
|
408
|
-
|
|
409
|
-
// this._blac.dispatchEvent(BlacLifecycleEvent.BLOC_CONSUMER_ADDED, this, { consumerId });
|
|
410
|
-
return true;
|
|
411
|
-
};
|
|
181
|
+
this._state = newState;
|
|
412
182
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
*/
|
|
420
|
-
_removeConsumer = (consumerId: string) => {
|
|
421
|
-
if (!this._consumers.has(consumerId)) return;
|
|
183
|
+
// Apply plugins
|
|
184
|
+
const transformedState = this._plugins.transformState(
|
|
185
|
+
oldState,
|
|
186
|
+
newState,
|
|
187
|
+
) as S;
|
|
188
|
+
this._state = transformedState;
|
|
422
189
|
|
|
423
|
-
|
|
424
|
-
this.
|
|
425
|
-
// this._blac.dispatchEvent(BlacLifecycleEvent.BLOC_CONSUMER_REMOVED, this, { consumerId });
|
|
190
|
+
// Notify plugins of state change
|
|
191
|
+
this._plugins.notifyStateChange(oldState, transformedState);
|
|
426
192
|
|
|
427
|
-
//
|
|
428
|
-
|
|
429
|
-
|
|
193
|
+
// Notify system plugins of state change
|
|
194
|
+
this.blacInstance?.plugins.notifyStateChanged(
|
|
195
|
+
this as any,
|
|
196
|
+
oldState,
|
|
197
|
+
transformedState,
|
|
430
198
|
);
|
|
431
199
|
|
|
432
|
-
//
|
|
433
|
-
if (
|
|
434
|
-
this.
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
`[${this._name}:${this._id}] No consumers left and not keep-alive. Scheduling disposal.`,
|
|
441
|
-
);
|
|
442
|
-
this._scheduleDisposal();
|
|
200
|
+
// Handle batching
|
|
201
|
+
if (this._batchingManager.isCurrentlyBatching) {
|
|
202
|
+
this._batchingManager.addUpdate({
|
|
203
|
+
newState: transformedState,
|
|
204
|
+
oldState,
|
|
205
|
+
action,
|
|
206
|
+
});
|
|
207
|
+
return;
|
|
443
208
|
}
|
|
444
|
-
};
|
|
445
209
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
private _disposalHandler?: (bloc: BlocBase<unknown>) => void;
|
|
210
|
+
// Notify all subscriptions
|
|
211
|
+
this._subscriptionManager.notify(transformedState, oldState, action);
|
|
212
|
+
this.lastUpdate = Date.now();
|
|
213
|
+
}
|
|
451
214
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
215
|
+
_batchUpdates(callback: () => void): void {
|
|
216
|
+
this._batchingManager.batchUpdates(callback, (finalUpdate) => {
|
|
217
|
+
this._subscriptionManager.notify(
|
|
218
|
+
finalUpdate.newState,
|
|
219
|
+
finalUpdate.oldState,
|
|
220
|
+
finalUpdate.action,
|
|
221
|
+
);
|
|
222
|
+
});
|
|
458
223
|
}
|
|
459
224
|
|
|
460
225
|
/**
|
|
461
|
-
*
|
|
462
|
-
* Schedules disposal of this bloc instance if it has no consumers
|
|
463
|
-
* Uses atomic state transitions to prevent race conditions
|
|
226
|
+
* Add a plugin
|
|
464
227
|
*/
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const requestResult = this._atomicStateTransition(
|
|
468
|
-
BlocLifecycleState.ACTIVE,
|
|
469
|
-
BlocLifecycleState.DISPOSAL_REQUESTED,
|
|
470
|
-
);
|
|
471
|
-
|
|
472
|
-
if (!requestResult.success) {
|
|
473
|
-
// Already requested, disposing, or disposed
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// Step 2: Verify disposal conditions under atomic protection
|
|
478
|
-
const shouldDispose = this._consumers.size === 0 && !this._keepAlive;
|
|
479
|
-
|
|
480
|
-
if (!shouldDispose) {
|
|
481
|
-
// Conditions no longer met, revert to active
|
|
482
|
-
this._atomicStateTransition(
|
|
483
|
-
BlocLifecycleState.DISPOSAL_REQUESTED,
|
|
484
|
-
BlocLifecycleState.ACTIVE,
|
|
485
|
-
);
|
|
486
|
-
return;
|
|
487
|
-
}
|
|
228
|
+
addPlugin(plugin: BlocPlugin<S, any>): void {
|
|
229
|
+
this._plugins.add(plugin);
|
|
488
230
|
|
|
489
|
-
//
|
|
490
|
-
this.
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
// Re-verify disposal conditions - React Strict Mode remount may have cancelled this
|
|
496
|
-
const stillShouldDispose =
|
|
497
|
-
this._consumers.size === 0 &&
|
|
498
|
-
!this._keepAlive &&
|
|
499
|
-
this._observer.size === 0 &&
|
|
500
|
-
(this as any)._disposalState === BlocLifecycleState.DISPOSAL_REQUESTED;
|
|
501
|
-
|
|
502
|
-
if (stillShouldDispose) {
|
|
503
|
-
// No cancellation occurred, proceed with disposal
|
|
504
|
-
if (this._disposalHandler) {
|
|
505
|
-
this._disposalHandler(this as any);
|
|
506
|
-
} else {
|
|
507
|
-
this._dispose();
|
|
508
|
-
}
|
|
509
|
-
} else {
|
|
510
|
-
// Disposal was cancelled (React Strict Mode remount), revert to active
|
|
511
|
-
this._atomicStateTransition(
|
|
512
|
-
BlocLifecycleState.DISPOSAL_REQUESTED,
|
|
513
|
-
BlocLifecycleState.ACTIVE,
|
|
514
|
-
);
|
|
231
|
+
// If plugins are already attached (bloc is active), attach the new plugin
|
|
232
|
+
if ((this._plugins as any).attached && plugin.onAttach) {
|
|
233
|
+
try {
|
|
234
|
+
plugin.onAttach(this);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.error(`Plugin '${plugin.name}' error in onAttach:`, error);
|
|
515
237
|
}
|
|
516
|
-
}
|
|
238
|
+
}
|
|
517
239
|
}
|
|
518
240
|
|
|
519
|
-
lastUpdate = Date.now();
|
|
520
|
-
|
|
521
241
|
/**
|
|
522
|
-
*
|
|
523
|
-
* Flag to indicate if batching is enabled for this bloc
|
|
242
|
+
* Remove a plugin
|
|
524
243
|
*/
|
|
525
|
-
|
|
244
|
+
removePlugin(plugin: BlocPlugin<S, any>): void {
|
|
245
|
+
this._plugins.remove(plugin.name);
|
|
246
|
+
}
|
|
526
247
|
|
|
527
248
|
/**
|
|
528
|
-
*
|
|
529
|
-
* Updates the state and notifies all observers of the change.
|
|
530
|
-
*
|
|
531
|
-
* @param newState The new state to be set
|
|
532
|
-
* @param oldState The previous state for comparison
|
|
533
|
-
* @param action Optional metadata about what caused the state change
|
|
249
|
+
* Get all plugins
|
|
534
250
|
*/
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// Validate action type if provided
|
|
542
|
-
if (
|
|
543
|
-
action !== undefined &&
|
|
544
|
-
typeof action !== 'object' &&
|
|
545
|
-
typeof action !== 'function'
|
|
546
|
-
) {
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
// Transform state through plugins
|
|
551
|
-
let transformedState: S = newState;
|
|
552
|
-
try {
|
|
553
|
-
const result = this._plugins.transformState(oldState, newState);
|
|
554
|
-
transformedState = result;
|
|
555
|
-
} catch (error) {
|
|
556
|
-
this._plugins.notifyError(error as Error, {
|
|
557
|
-
phase: 'state-change',
|
|
558
|
-
operation: 'transformState'
|
|
559
|
-
});
|
|
560
|
-
// Continue with original state if transformation fails
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
if (this._batchingEnabled) {
|
|
564
|
-
// When batching, just accumulate the updates
|
|
565
|
-
this._pendingUpdates.push({ newState: transformedState, oldState, action });
|
|
566
|
-
|
|
567
|
-
// Update internal state for consistency
|
|
568
|
-
this._oldState = oldState;
|
|
569
|
-
this._state = transformedState;
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// Normal state update flow
|
|
574
|
-
this._oldState = oldState;
|
|
575
|
-
this._state = transformedState;
|
|
576
|
-
|
|
577
|
-
// Notify bloc plugins first
|
|
578
|
-
try {
|
|
579
|
-
this._plugins.notifyStateChange(oldState, transformedState);
|
|
580
|
-
} catch (error) {
|
|
581
|
-
console.error('Error notifying bloc plugins of state change:', error);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// Notify observers of the state change
|
|
585
|
-
this._observer.notify(transformedState, oldState, action);
|
|
586
|
-
this.lastUpdate = Date.now();
|
|
587
|
-
};
|
|
251
|
+
get plugins(): ReadonlyArray<BlocPlugin<S, any>> {
|
|
252
|
+
return this._plugins.getAll();
|
|
253
|
+
}
|
|
588
254
|
|
|
589
255
|
/**
|
|
590
|
-
*
|
|
591
|
-
* @internal Used by plugins for state hydration
|
|
592
|
-
* @param newState The new state
|
|
593
|
-
* @param oldState The old state
|
|
256
|
+
* Disposal management
|
|
594
257
|
*/
|
|
595
|
-
|
|
596
|
-
this.
|
|
258
|
+
get isDisposed(): boolean {
|
|
259
|
+
return this._lifecycleManager.isDisposed;
|
|
597
260
|
}
|
|
598
261
|
|
|
599
262
|
/**
|
|
600
|
-
*
|
|
601
|
-
* @param batchFn Function to execute with batching enabled
|
|
263
|
+
* Atomic state transition for disposal
|
|
602
264
|
*/
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
265
|
+
_atomicStateTransition(
|
|
266
|
+
expectedState: BlocLifecycleState,
|
|
267
|
+
newState: BlocLifecycleState,
|
|
268
|
+
): StateTransitionResult {
|
|
269
|
+
return this._lifecycleManager.atomicStateTransition(
|
|
270
|
+
expectedState,
|
|
271
|
+
newState,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
609
274
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
275
|
+
/**
|
|
276
|
+
* Dispose the bloc and clean up resources
|
|
277
|
+
*/
|
|
278
|
+
async dispose(): Promise<void> {
|
|
279
|
+
const transitionResult = this._lifecycleManager.atomicStateTransition(
|
|
280
|
+
BlocLifecycleState.ACTIVE,
|
|
281
|
+
BlocLifecycleState.DISPOSING,
|
|
282
|
+
);
|
|
613
283
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
this._observer.notify(
|
|
623
|
-
finalUpdate.newState,
|
|
624
|
-
finalUpdate.oldState,
|
|
625
|
-
finalUpdate.action,
|
|
284
|
+
if (!transitionResult.success) {
|
|
285
|
+
if (
|
|
286
|
+
this._lifecycleManager.currentState ===
|
|
287
|
+
BlocLifecycleState.DISPOSAL_REQUESTED
|
|
288
|
+
) {
|
|
289
|
+
this._lifecycleManager.atomicStateTransition(
|
|
290
|
+
BlocLifecycleState.DISPOSAL_REQUESTED,
|
|
291
|
+
BlocLifecycleState.DISPOSING,
|
|
626
292
|
);
|
|
627
|
-
|
|
293
|
+
} else {
|
|
294
|
+
return;
|
|
628
295
|
}
|
|
629
|
-
|
|
630
|
-
return result;
|
|
631
|
-
} finally {
|
|
632
|
-
this._batchingEnabled = false;
|
|
633
|
-
this._batchingLock = false;
|
|
634
|
-
this._pendingUpdates = [];
|
|
635
296
|
}
|
|
636
|
-
};
|
|
637
297
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
addPlugin(plugin: BlocPlugin<S, any>): void {
|
|
642
|
-
this._plugins.add(plugin);
|
|
298
|
+
try {
|
|
299
|
+
// Clear subscriptions
|
|
300
|
+
this._subscriptionManager.clear();
|
|
643
301
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
302
|
+
// Call disposal hook
|
|
303
|
+
if (this.onDispose) {
|
|
304
|
+
this.onDispose();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Notify plugins of disposal
|
|
308
|
+
for (const plugin of this._plugins.getAll()) {
|
|
309
|
+
try {
|
|
310
|
+
if ('onDispose' in plugin && typeof plugin.onDispose === 'function') {
|
|
311
|
+
plugin.onDispose(this as any);
|
|
312
|
+
}
|
|
313
|
+
} catch (error) {
|
|
314
|
+
console.error('Plugin disposal error:', error);
|
|
649
315
|
}
|
|
650
|
-
} catch (error) {
|
|
651
|
-
console.error(`Failed to attach plugin '${plugin.name}':`, error);
|
|
652
|
-
this._plugins.remove(plugin.name);
|
|
653
|
-
throw error;
|
|
654
316
|
}
|
|
317
|
+
|
|
318
|
+
// Call disposal handler
|
|
319
|
+
const handler = this._lifecycleManager.getDisposalHandler();
|
|
320
|
+
if (handler) {
|
|
321
|
+
handler(this as any);
|
|
322
|
+
}
|
|
323
|
+
} finally {
|
|
324
|
+
this._lifecycleManager.atomicStateTransition(
|
|
325
|
+
BlocLifecycleState.DISPOSING,
|
|
326
|
+
BlocLifecycleState.DISPOSED,
|
|
327
|
+
);
|
|
655
328
|
}
|
|
656
329
|
}
|
|
657
330
|
|
|
658
331
|
/**
|
|
659
|
-
*
|
|
332
|
+
* Schedule disposal when no subscriptions remain
|
|
660
333
|
*/
|
|
661
|
-
|
|
662
|
-
|
|
334
|
+
_scheduleDisposal(): void {
|
|
335
|
+
const shouldDispose =
|
|
336
|
+
this._subscriptionManager.size === 0 && !this._keepAlive;
|
|
337
|
+
|
|
338
|
+
if (!shouldDispose) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
this._lifecycleManager.scheduleDisposal(
|
|
343
|
+
16,
|
|
344
|
+
() => this._subscriptionManager.size === 0 && !this._keepAlive,
|
|
345
|
+
() => this.dispose(),
|
|
346
|
+
);
|
|
663
347
|
}
|
|
664
348
|
|
|
665
349
|
/**
|
|
666
|
-
*
|
|
350
|
+
* Set disposal handler
|
|
667
351
|
*/
|
|
668
|
-
|
|
669
|
-
|
|
352
|
+
setDisposalHandler(handler: (bloc: BlocBase<unknown>) => void): void {
|
|
353
|
+
this._lifecycleManager.setDisposalHandler(
|
|
354
|
+
handler as (bloc: unknown) => void,
|
|
355
|
+
);
|
|
670
356
|
}
|
|
671
357
|
|
|
672
358
|
/**
|
|
673
|
-
*
|
|
359
|
+
* Check if disposal should be scheduled (called by subscription manager)
|
|
674
360
|
*/
|
|
675
|
-
|
|
676
|
-
|
|
361
|
+
checkDisposal(): void {
|
|
362
|
+
if (
|
|
363
|
+
this._subscriptionManager.size === 0 &&
|
|
364
|
+
!this._keepAlive &&
|
|
365
|
+
this._lifecycleManager.currentState === BlocLifecycleState.ACTIVE
|
|
366
|
+
) {
|
|
367
|
+
this._scheduleDisposal();
|
|
368
|
+
}
|
|
677
369
|
}
|
|
678
370
|
|
|
679
371
|
/**
|
|
680
|
-
*
|
|
681
|
-
* Activate plugins when bloc becomes active
|
|
372
|
+
* Cancel disposal if bloc is in disposal_requested state
|
|
682
373
|
*/
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
374
|
+
_cancelDisposalIfRequested(): void {
|
|
375
|
+
this.blacInstance?.log(
|
|
376
|
+
`[${this._name}:${this._id}] _cancelDisposalIfRequested called. Current state: ${this._lifecycleManager.currentState}`,
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
const success = this._lifecycleManager.cancelDisposal();
|
|
380
|
+
|
|
381
|
+
if (success) {
|
|
382
|
+
this.blacInstance?.log(
|
|
383
|
+
`[${this._name}:${this._id}] Disposal cancelled - new subscription added`,
|
|
384
|
+
);
|
|
385
|
+
} else if (
|
|
386
|
+
this._lifecycleManager.currentState ===
|
|
387
|
+
BlocLifecycleState.DISPOSAL_REQUESTED
|
|
388
|
+
) {
|
|
389
|
+
this.blacInstance?.warn(
|
|
390
|
+
`[${this._name}:${this._id}] Failed to cancel disposal. Current state: ${this._lifecycleManager.currentState}`,
|
|
391
|
+
);
|
|
392
|
+
} else if (this._lifecycleManager.isDisposed) {
|
|
393
|
+
this.blacInstance?.error(
|
|
394
|
+
`[${this._name}:${this._id}] Cannot cancel disposal - bloc is already disposed`,
|
|
395
|
+
);
|
|
690
396
|
}
|
|
691
397
|
}
|
|
692
398
|
}
|