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