@blac/core 2.0.0-rc-8 → 2.0.0-rc-10
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 +185 -4
- package/dist/index.es.js +1099 -601
- package/dist/index.es.js.map +1 -1
- package/package.json +5 -4
- package/src/Blac.ts +63 -40
- package/src/BlacObserver.ts +58 -24
- package/src/Bloc.ts +166 -157
- package/src/BlocBase.ts +266 -90
- package/src/Cubit.ts +1 -1
- package/src/__tests__/Bloc.event.test.ts +419 -0
- package/src/__tests__/BlocBase.lifecycle.test.ts +441 -0
- package/src/__tests__/Cubit.test.ts +473 -0
- package/src/adapter/BlacAdapter.ts +262 -0
- package/src/adapter/ConsumerTracker.ts +241 -0
- package/src/adapter/ProxyFactory.ts +308 -0
- package/src/adapter/__tests__/BlacAdapter.test.ts +561 -0
- package/src/adapter/__tests__/ConsumerTracker.test.ts +562 -0
- package/src/adapter/__tests__/ProxyFactory.test.ts +655 -0
- package/src/adapter/index.ts +3 -0
- package/src/index.ts +2 -1
- package/src/testing.ts +60 -47
- package/src/types.ts +11 -3
- package/src/utils/uuid.ts +4 -4
- package/src/BlacEvent.ts +0 -5
package/src/BlocBase.ts
CHANGED
|
@@ -2,7 +2,31 @@ import { BlacObservable } from './BlacObserver';
|
|
|
2
2
|
import { generateUUID } from './utils/uuid';
|
|
3
3
|
|
|
4
4
|
export type BlocInstanceId = string | number | undefined;
|
|
5
|
-
type DependencySelector<S> = (
|
|
5
|
+
type DependencySelector<S> = (
|
|
6
|
+
currentState: S,
|
|
7
|
+
previousState: S | undefined,
|
|
8
|
+
instance: any,
|
|
9
|
+
) => unknown[];
|
|
10
|
+
|
|
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
|
+
}
|
|
6
30
|
|
|
7
31
|
// Define an interface for the static properties expected on a Bloc/Cubit constructor
|
|
8
32
|
interface BlocStaticProperties {
|
|
@@ -13,15 +37,12 @@ interface BlocStaticProperties {
|
|
|
13
37
|
/**
|
|
14
38
|
* Base class for both Blocs and Cubits that provides core state management functionality.
|
|
15
39
|
* Handles state transitions, observer notifications, lifecycle management, and addon integration.
|
|
16
|
-
*
|
|
40
|
+
*
|
|
17
41
|
* @abstract This class should be extended, not instantiated directly
|
|
18
42
|
* @template S The type of state managed by this Bloc
|
|
19
43
|
* @template P The type of props that can be passed during instance creation (optional)
|
|
20
44
|
*/
|
|
21
|
-
export abstract class BlocBase<
|
|
22
|
-
S,
|
|
23
|
-
P = unknown
|
|
24
|
-
> {
|
|
45
|
+
export abstract class BlocBase<S, P = unknown> {
|
|
25
46
|
public uid = generateUUID();
|
|
26
47
|
/**
|
|
27
48
|
* When true, every consumer will receive its own unique instance of this Bloc.
|
|
@@ -32,7 +53,7 @@ export abstract class BlocBase<
|
|
|
32
53
|
get isIsolated() {
|
|
33
54
|
return this._isolated;
|
|
34
55
|
}
|
|
35
|
-
|
|
56
|
+
|
|
36
57
|
/**
|
|
37
58
|
* When true, the Bloc instance persists even when there are no active consumers.
|
|
38
59
|
* Useful for maintaining state between component unmount/remount cycles.
|
|
@@ -42,43 +63,43 @@ export abstract class BlocBase<
|
|
|
42
63
|
get isKeepAlive() {
|
|
43
64
|
return this._keepAlive;
|
|
44
65
|
}
|
|
45
|
-
|
|
66
|
+
|
|
46
67
|
/**
|
|
47
68
|
* Defines how dependencies are selected from the state for efficient updates.
|
|
48
69
|
* When provided, observers will only be notified when selected dependencies change.
|
|
49
70
|
*/
|
|
50
71
|
defaultDependencySelector: DependencySelector<S> | undefined;
|
|
51
|
-
|
|
72
|
+
|
|
52
73
|
/**
|
|
53
74
|
* @internal
|
|
54
75
|
* Indicates if this specific Bloc instance is isolated from others of the same type.
|
|
55
76
|
*/
|
|
56
77
|
public _isolated = false;
|
|
57
|
-
|
|
78
|
+
|
|
58
79
|
/**
|
|
59
80
|
* @internal
|
|
60
81
|
* Observable responsible for managing state listeners and notifying consumers.
|
|
61
82
|
*/
|
|
62
83
|
public _observer: BlacObservable<S>;
|
|
63
|
-
|
|
84
|
+
|
|
64
85
|
/**
|
|
65
86
|
* The unique identifier for this Bloc instance.
|
|
66
87
|
* Defaults to the class name, but can be customized.
|
|
67
88
|
*/
|
|
68
89
|
public _id: BlocInstanceId;
|
|
69
|
-
|
|
90
|
+
|
|
70
91
|
/**
|
|
71
92
|
* @internal
|
|
72
93
|
* Reference string used internally for tracking and debugging.
|
|
73
94
|
*/
|
|
74
95
|
public _instanceRef?: string;
|
|
75
|
-
|
|
96
|
+
|
|
76
97
|
/**
|
|
77
98
|
* @internal
|
|
78
99
|
* Indicates if this specific Bloc instance should be kept alive when no consumers are present.
|
|
79
100
|
*/
|
|
80
101
|
public _keepAlive = false;
|
|
81
|
-
|
|
102
|
+
|
|
82
103
|
/**
|
|
83
104
|
* @readonly
|
|
84
105
|
* Timestamp when this Bloc instance was created, useful for debugging and performance tracking.
|
|
@@ -87,22 +108,28 @@ export abstract class BlocBase<
|
|
|
87
108
|
|
|
88
109
|
/**
|
|
89
110
|
* @internal
|
|
90
|
-
*
|
|
111
|
+
* Atomic disposal state to prevent race conditions
|
|
112
|
+
*/
|
|
113
|
+
private _disposalState: BlocLifecycleState = BlocLifecycleState.ACTIVE;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @internal
|
|
117
|
+
* Timestamp when disposal was requested (for React Strict Mode grace period)
|
|
91
118
|
*/
|
|
92
|
-
private
|
|
119
|
+
private _disposalRequestTime: number = 0;
|
|
93
120
|
|
|
94
121
|
/**
|
|
95
122
|
* @internal
|
|
96
123
|
* The current state of the Bloc.
|
|
97
124
|
*/
|
|
98
125
|
public _state: S;
|
|
99
|
-
|
|
126
|
+
|
|
100
127
|
/**
|
|
101
128
|
* @internal
|
|
102
129
|
* The previous state of the Bloc, maintained for comparison and history.
|
|
103
130
|
*/
|
|
104
131
|
public _oldState: S | undefined;
|
|
105
|
-
|
|
132
|
+
|
|
106
133
|
/**
|
|
107
134
|
* Props passed during Bloc instance creation.
|
|
108
135
|
* Can be used to configure or parameterize the Bloc's behavior.
|
|
@@ -119,7 +146,11 @@ export abstract class BlocBase<
|
|
|
119
146
|
* @internal
|
|
120
147
|
* Pending batched updates
|
|
121
148
|
*/
|
|
122
|
-
private _pendingUpdates: Array<{
|
|
149
|
+
private _pendingUpdates: Array<{
|
|
150
|
+
newState: S;
|
|
151
|
+
oldState: S;
|
|
152
|
+
action?: unknown;
|
|
153
|
+
}> = [];
|
|
123
154
|
|
|
124
155
|
/**
|
|
125
156
|
* @internal
|
|
@@ -134,21 +165,25 @@ export abstract class BlocBase<
|
|
|
134
165
|
*/
|
|
135
166
|
_validateConsumers = (): void => {
|
|
136
167
|
const deadConsumers: string[] = [];
|
|
137
|
-
|
|
168
|
+
|
|
138
169
|
for (const [consumerId, weakRef] of this._consumerRefs) {
|
|
139
170
|
if (weakRef.deref() === undefined) {
|
|
140
171
|
deadConsumers.push(consumerId);
|
|
141
172
|
}
|
|
142
173
|
}
|
|
143
|
-
|
|
174
|
+
|
|
144
175
|
// Clean up dead consumers
|
|
145
176
|
for (const consumerId of deadConsumers) {
|
|
146
177
|
this._consumers.delete(consumerId);
|
|
147
178
|
this._consumerRefs.delete(consumerId);
|
|
148
179
|
}
|
|
149
|
-
|
|
180
|
+
|
|
150
181
|
// Schedule disposal if no live consumers remain
|
|
151
|
-
if (
|
|
182
|
+
if (
|
|
183
|
+
this._consumers.size === 0 &&
|
|
184
|
+
!this._keepAlive &&
|
|
185
|
+
this._disposalState === BlocLifecycleState.ACTIVE
|
|
186
|
+
) {
|
|
152
187
|
this._scheduleDisposal();
|
|
153
188
|
}
|
|
154
189
|
};
|
|
@@ -156,7 +191,7 @@ export abstract class BlocBase<
|
|
|
156
191
|
/**
|
|
157
192
|
* Creates a new BlocBase instance with the given initial state.
|
|
158
193
|
* Sets up the observer, registers with the Blac manager, and initializes addons.
|
|
159
|
-
*
|
|
194
|
+
*
|
|
160
195
|
* @param initialState The initial state value for this Bloc
|
|
161
196
|
*/
|
|
162
197
|
constructor(initialState: S) {
|
|
@@ -165,21 +200,40 @@ export abstract class BlocBase<
|
|
|
165
200
|
this._id = this.constructor.name;
|
|
166
201
|
|
|
167
202
|
// Access static properties safely with proper type checking
|
|
168
|
-
const Constructor = this.constructor as typeof BlocBase &
|
|
169
|
-
|
|
203
|
+
const Constructor = this.constructor as typeof BlocBase &
|
|
204
|
+
BlocStaticProperties;
|
|
205
|
+
|
|
170
206
|
// Validate that the static properties exist and are boolean
|
|
171
|
-
this._keepAlive =
|
|
172
|
-
|
|
207
|
+
this._keepAlive =
|
|
208
|
+
typeof Constructor.keepAlive === 'boolean'
|
|
209
|
+
? Constructor.keepAlive
|
|
210
|
+
: false;
|
|
211
|
+
this._isolated =
|
|
212
|
+
typeof Constructor.isolated === 'boolean' ? Constructor.isolated : false;
|
|
173
213
|
}
|
|
174
214
|
|
|
175
215
|
/**
|
|
176
216
|
* Returns the current state of the Bloc.
|
|
177
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.
|
|
178
219
|
*/
|
|
179
220
|
get state(): S {
|
|
221
|
+
// Allow state access during all states except DISPOSED for React compatibility
|
|
222
|
+
if (this._disposalState === BlocLifecycleState.DISPOSED) {
|
|
223
|
+
// Return the last known state for disposed blocs to prevent crashes
|
|
224
|
+
return this._state;
|
|
225
|
+
}
|
|
180
226
|
return this._state;
|
|
181
227
|
}
|
|
182
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Returns whether this Bloc instance has been disposed.
|
|
231
|
+
* @returns true if the bloc is in DISPOSED state
|
|
232
|
+
*/
|
|
233
|
+
get isDisposed(): boolean {
|
|
234
|
+
return this._disposalState === BlocLifecycleState.DISPOSED;
|
|
235
|
+
}
|
|
236
|
+
|
|
183
237
|
/**
|
|
184
238
|
* @internal
|
|
185
239
|
* Returns the name of the Bloc class for identification and debugging.
|
|
@@ -192,7 +246,7 @@ export abstract class BlocBase<
|
|
|
192
246
|
* @internal
|
|
193
247
|
* Updates the Bloc instance's ID to a new value.
|
|
194
248
|
* Only updates if the new ID is defined and different from the current one.
|
|
195
|
-
*
|
|
249
|
+
*
|
|
196
250
|
* @param id The new ID to assign to this Bloc instance
|
|
197
251
|
*/
|
|
198
252
|
_updateId = (id?: BlocInstanceId) => {
|
|
@@ -201,33 +255,95 @@ export abstract class BlocBase<
|
|
|
201
255
|
this._id = id;
|
|
202
256
|
};
|
|
203
257
|
|
|
258
|
+
/**
|
|
259
|
+
* @internal
|
|
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
|
|
264
|
+
*/
|
|
265
|
+
_atomicStateTransition(
|
|
266
|
+
expectedState: BlocLifecycleState,
|
|
267
|
+
newState: BlocLifecycleState,
|
|
268
|
+
): StateTransitionResult {
|
|
269
|
+
if (this._disposalState === expectedState) {
|
|
270
|
+
const previousState = this._disposalState;
|
|
271
|
+
this._disposalState = newState;
|
|
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
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
204
301
|
/**
|
|
205
302
|
* @internal
|
|
206
303
|
* Cleans up resources and removes this Bloc from the system.
|
|
207
304
|
* Notifies the Blac manager and clears all observers.
|
|
208
305
|
*/
|
|
209
|
-
_dispose() {
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
306
|
+
_dispose(): boolean {
|
|
307
|
+
// Step 1: Attempt atomic transition to DISPOSING state from either ACTIVE or DISPOSAL_REQUESTED
|
|
308
|
+
let transitionResult = this._atomicStateTransition(
|
|
309
|
+
BlocLifecycleState.ACTIVE,
|
|
310
|
+
BlocLifecycleState.DISPOSING,
|
|
311
|
+
);
|
|
312
|
+
|
|
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
|
+
if (!transitionResult.success) {
|
|
322
|
+
// Already disposing or disposed - idempotent operation
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
// Step 2: Perform cleanup operations
|
|
328
|
+
this._consumers.clear();
|
|
329
|
+
this._consumerRefs.clear();
|
|
330
|
+
this._observer.clear();
|
|
331
|
+
|
|
332
|
+
// Call user-defined disposal hook
|
|
333
|
+
this.onDispose?.();
|
|
334
|
+
|
|
335
|
+
// Step 3: Final state transition to DISPOSED
|
|
336
|
+
const finalResult = this._atomicStateTransition(
|
|
337
|
+
BlocLifecycleState.DISPOSING,
|
|
338
|
+
BlocLifecycleState.DISPOSED,
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
return finalResult.success;
|
|
342
|
+
} catch (error) {
|
|
343
|
+
// Recovery: Reset state on cleanup failure
|
|
344
|
+
this._disposalState = BlocLifecycleState.ACTIVE;
|
|
345
|
+
throw error;
|
|
213
346
|
}
|
|
214
|
-
this._disposalState = 'disposing';
|
|
215
|
-
|
|
216
|
-
// Clear all consumers and their references
|
|
217
|
-
this._consumers.clear();
|
|
218
|
-
this._consumerRefs.clear();
|
|
219
|
-
|
|
220
|
-
// Clear observer subscriptions
|
|
221
|
-
this._observer.clear();
|
|
222
|
-
|
|
223
|
-
// Call user-defined disposal hook
|
|
224
|
-
this.onDispose?.();
|
|
225
|
-
|
|
226
|
-
// Mark as fully disposed
|
|
227
|
-
this._disposalState = 'disposed';
|
|
228
|
-
|
|
229
|
-
// The Blac manager will handle removal from registry
|
|
230
|
-
// This method is called by Blac.disposeBloc, so we don't need to call it again
|
|
231
347
|
}
|
|
232
348
|
|
|
233
349
|
/**
|
|
@@ -246,51 +362,65 @@ export abstract class BlocBase<
|
|
|
246
362
|
* @internal
|
|
247
363
|
* Registers a new consumer to this Bloc instance.
|
|
248
364
|
* Notifies the Blac manager that a consumer has been added.
|
|
249
|
-
*
|
|
365
|
+
*
|
|
250
366
|
* @param consumerId The unique ID of the consumer being added
|
|
251
367
|
* @param consumerRef Optional reference to the consumer object for cleanup validation
|
|
252
368
|
*/
|
|
253
|
-
_addConsumer = (consumerId: string, consumerRef?: object) => {
|
|
254
|
-
//
|
|
255
|
-
if (this._disposalState !==
|
|
256
|
-
return;
|
|
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
|
|
257
373
|
}
|
|
258
|
-
|
|
259
|
-
|
|
374
|
+
|
|
375
|
+
// Prevent duplicate consumers
|
|
376
|
+
if (this._consumers.has(consumerId)) return true;
|
|
377
|
+
|
|
378
|
+
// Safe consumer addition
|
|
260
379
|
this._consumers.add(consumerId);
|
|
261
|
-
|
|
380
|
+
|
|
262
381
|
// Store WeakRef for proper memory management
|
|
263
382
|
if (consumerRef) {
|
|
264
383
|
this._consumerRefs.set(consumerId, new WeakRef(consumerRef));
|
|
265
384
|
}
|
|
266
|
-
|
|
385
|
+
|
|
267
386
|
// @ts-ignore - Blac is available globally
|
|
268
|
-
(globalThis as any).Blac?.log(
|
|
269
|
-
|
|
387
|
+
(globalThis as any).Blac?.log(
|
|
388
|
+
`[${this._name}:${this._id}] Consumer added. Total consumers: ${this._consumers.size}`,
|
|
389
|
+
);
|
|
390
|
+
|
|
270
391
|
// this._blac.dispatchEvent(BlacLifecycleEvent.BLOC_CONSUMER_ADDED, this, { consumerId });
|
|
392
|
+
return true;
|
|
271
393
|
};
|
|
272
394
|
|
|
273
395
|
/**
|
|
274
396
|
* @internal
|
|
275
397
|
* Unregisters a consumer from this Bloc instance.
|
|
276
398
|
* Notifies the Blac manager that a consumer has been removed.
|
|
277
|
-
*
|
|
399
|
+
*
|
|
278
400
|
* @param consumerId The unique ID of the consumer being removed
|
|
279
401
|
*/
|
|
280
402
|
_removeConsumer = (consumerId: string) => {
|
|
281
403
|
if (!this._consumers.has(consumerId)) return;
|
|
282
|
-
|
|
404
|
+
|
|
283
405
|
this._consumers.delete(consumerId);
|
|
284
406
|
this._consumerRefs.delete(consumerId);
|
|
285
407
|
// this._blac.dispatchEvent(BlacLifecycleEvent.BLOC_CONSUMER_REMOVED, this, { consumerId });
|
|
286
|
-
|
|
408
|
+
|
|
287
409
|
// @ts-ignore - Blac is available globally
|
|
288
|
-
(globalThis as any).Blac?.log(
|
|
289
|
-
|
|
410
|
+
(globalThis as any).Blac?.log(
|
|
411
|
+
`[${this._name}:${this._id}] Consumer removed. Remaining consumers: ${this._consumers.size}, keepAlive: ${this._keepAlive}`,
|
|
412
|
+
);
|
|
413
|
+
|
|
290
414
|
// If no consumers remain and not keep-alive, schedule disposal
|
|
291
|
-
if (
|
|
415
|
+
if (
|
|
416
|
+
this._consumers.size === 0 &&
|
|
417
|
+
!this._keepAlive &&
|
|
418
|
+
this._disposalState === BlocLifecycleState.ACTIVE
|
|
419
|
+
) {
|
|
292
420
|
// @ts-ignore - Blac is available globally
|
|
293
|
-
(globalThis as any).Blac?.log(
|
|
421
|
+
(globalThis as any).Blac?.log(
|
|
422
|
+
`[${this._name}:${this._id}] No consumers left and not keep-alive. Scheduling disposal.`,
|
|
423
|
+
);
|
|
294
424
|
this._scheduleDisposal();
|
|
295
425
|
}
|
|
296
426
|
};
|
|
@@ -312,21 +442,60 @@ export abstract class BlocBase<
|
|
|
312
442
|
/**
|
|
313
443
|
* @internal
|
|
314
444
|
* Schedules disposal of this bloc instance if it has no consumers
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
445
|
+
* Uses atomic state transitions to prevent race conditions
|
|
446
|
+
*/
|
|
447
|
+
private _scheduleDisposal(): void {
|
|
448
|
+
// Step 1: Atomic transition to DISPOSAL_REQUESTED
|
|
449
|
+
const requestResult = this._atomicStateTransition(
|
|
450
|
+
BlocLifecycleState.ACTIVE,
|
|
451
|
+
BlocLifecycleState.DISPOSAL_REQUESTED,
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
if (!requestResult.success) {
|
|
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;
|
|
461
|
+
|
|
462
|
+
if (!shouldDispose) {
|
|
463
|
+
// Conditions no longer met, revert to active
|
|
464
|
+
this._atomicStateTransition(
|
|
465
|
+
BlocLifecycleState.DISPOSAL_REQUESTED,
|
|
466
|
+
BlocLifecycleState.ACTIVE,
|
|
467
|
+
);
|
|
319
468
|
return;
|
|
320
469
|
}
|
|
321
|
-
|
|
322
|
-
//
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
+
}
|
|
326
491
|
} else {
|
|
327
|
-
|
|
492
|
+
// Disposal was cancelled (React Strict Mode remount), revert to active
|
|
493
|
+
this._atomicStateTransition(
|
|
494
|
+
BlocLifecycleState.DISPOSAL_REQUESTED,
|
|
495
|
+
BlocLifecycleState.ACTIVE,
|
|
496
|
+
);
|
|
328
497
|
}
|
|
329
|
-
}
|
|
498
|
+
});
|
|
330
499
|
}
|
|
331
500
|
|
|
332
501
|
lastUpdate = Date.now();
|
|
@@ -340,7 +509,7 @@ export abstract class BlocBase<
|
|
|
340
509
|
/**
|
|
341
510
|
* @internal
|
|
342
511
|
* Updates the state and notifies all observers of the change.
|
|
343
|
-
*
|
|
512
|
+
*
|
|
344
513
|
* @param newState The new state to be set
|
|
345
514
|
* @param oldState The previous state for comparison
|
|
346
515
|
* @param action Optional metadata about what caused the state change
|
|
@@ -348,20 +517,22 @@ export abstract class BlocBase<
|
|
|
348
517
|
_pushState = (newState: S, oldState: S, action?: unknown): void => {
|
|
349
518
|
// Validate newState
|
|
350
519
|
if (newState === undefined) {
|
|
351
|
-
console.warn('BlocBase._pushState: newState is undefined', this);
|
|
352
520
|
return;
|
|
353
521
|
}
|
|
354
522
|
|
|
355
523
|
// Validate action type if provided
|
|
356
|
-
if (
|
|
357
|
-
|
|
524
|
+
if (
|
|
525
|
+
action !== undefined &&
|
|
526
|
+
typeof action !== 'object' &&
|
|
527
|
+
typeof action !== 'function'
|
|
528
|
+
) {
|
|
358
529
|
return;
|
|
359
530
|
}
|
|
360
531
|
|
|
361
532
|
if (this._batchingEnabled) {
|
|
362
533
|
// When batching, just accumulate the updates
|
|
363
534
|
this._pendingUpdates.push({ newState, oldState, action });
|
|
364
|
-
|
|
535
|
+
|
|
365
536
|
// Update internal state for consistency
|
|
366
537
|
this._oldState = oldState;
|
|
367
538
|
this._state = newState;
|
|
@@ -371,7 +542,7 @@ export abstract class BlocBase<
|
|
|
371
542
|
// Normal state update flow
|
|
372
543
|
this._oldState = oldState;
|
|
373
544
|
this._state = newState;
|
|
374
|
-
|
|
545
|
+
|
|
375
546
|
// Notify observers of the state change
|
|
376
547
|
this._observer.notify(newState, oldState, action);
|
|
377
548
|
this.lastUpdate = Date.now();
|
|
@@ -387,22 +558,27 @@ export abstract class BlocBase<
|
|
|
387
558
|
// If already batching, just execute the function without nesting batches
|
|
388
559
|
return batchFn();
|
|
389
560
|
}
|
|
390
|
-
|
|
561
|
+
|
|
391
562
|
this._batchingLock = true;
|
|
392
563
|
this._batchingEnabled = true;
|
|
393
564
|
this._pendingUpdates = [];
|
|
394
565
|
|
|
395
566
|
try {
|
|
396
567
|
const result = batchFn();
|
|
397
|
-
|
|
568
|
+
|
|
398
569
|
// Process all batched updates
|
|
399
570
|
if (this._pendingUpdates.length > 0) {
|
|
400
571
|
// Only notify once with the final state
|
|
401
|
-
const finalUpdate =
|
|
402
|
-
|
|
572
|
+
const finalUpdate =
|
|
573
|
+
this._pendingUpdates[this._pendingUpdates.length - 1];
|
|
574
|
+
this._observer.notify(
|
|
575
|
+
finalUpdate.newState,
|
|
576
|
+
finalUpdate.oldState,
|
|
577
|
+
finalUpdate.action,
|
|
578
|
+
);
|
|
403
579
|
this.lastUpdate = Date.now();
|
|
404
580
|
}
|
|
405
|
-
|
|
581
|
+
|
|
406
582
|
return result;
|
|
407
583
|
} finally {
|
|
408
584
|
this._batchingEnabled = false;
|
package/src/Cubit.ts
CHANGED
|
@@ -27,7 +27,7 @@ export abstract class Cubit<S, P = null> extends BlocBase<S, P> {
|
|
|
27
27
|
/**
|
|
28
28
|
* Partially updates the current state by merging it with the provided state patch.
|
|
29
29
|
* This method is only applicable when the state is an object type.
|
|
30
|
-
*
|
|
30
|
+
*
|
|
31
31
|
* @param statePatch - A partial state object containing only the properties to update
|
|
32
32
|
* @param ignoreChangeCheck - If true, skips checking if the state has actually changed
|
|
33
33
|
* @throws {TypeError} If the state is not an object type
|