@blac/core 2.0.0-rn-12 → 2.0.1

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/src/BlocBase.ts DELETED
@@ -1,589 +0,0 @@
1
- import { BlacObservable } from './BlacObserver';
2
- import { generateUUID } from './utils/uuid';
3
-
4
- export type BlocInstanceId = string | number | undefined;
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
- }
30
-
31
- // Define an interface for the static properties expected on a Bloc/Cubit constructor
32
- interface BlocStaticProperties {
33
- isolated: boolean;
34
- keepAlive: boolean;
35
- }
36
-
37
- /**
38
- * Base class for both Blocs and Cubits that provides core state management functionality.
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)
44
- */
45
- export abstract class BlocBase<S, P = unknown> {
46
- public uid = generateUUID();
47
- /**
48
- * When true, every consumer will receive its own unique instance of this Bloc.
49
- * Use this when state should not be shared between components.
50
- * @default false
51
- */
52
- static isolated = false;
53
- get isIsolated() {
54
- return this._isolated;
55
- }
56
-
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
- static keepAlive = false;
63
- get isKeepAlive() {
64
- return this._keepAlive;
65
- }
66
-
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
- public _isolated = false;
78
-
79
- /**
80
- * @internal
81
- * Observable responsible for managing state listeners and notifying consumers.
82
- */
83
- public _observer: BlacObservable<S>;
84
-
85
- /**
86
- * The unique identifier for this Bloc instance.
87
- * Defaults to the class name, but can be customized.
88
- */
89
- public _id: BlocInstanceId;
90
-
91
- /**
92
- * @internal
93
- * Reference string used internally for tracking and debugging.
94
- */
95
- public _instanceRef?: string;
96
-
97
- /**
98
- * @internal
99
- * Indicates if this specific Bloc instance should be kept alive when no consumers are present.
100
- */
101
- public _keepAlive = false;
102
-
103
- /**
104
- * @readonly
105
- * Timestamp when this Bloc instance was created, useful for debugging and performance tracking.
106
- */
107
- public readonly _createdAt = Date.now();
108
-
109
- /**
110
- * @internal
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)
118
- */
119
- private _disposalRequestTime: number = 0;
120
-
121
- /**
122
- * @internal
123
- * The current state of the Bloc.
124
- */
125
- public _state: S;
126
-
127
- /**
128
- * @internal
129
- * The previous state of the Bloc, maintained for comparison and history.
130
- */
131
- public _oldState: S | undefined;
132
-
133
- /**
134
- * Props passed during Bloc instance creation.
135
- * Can be used to configure or parameterize the Bloc's behavior.
136
- */
137
- public props: P | null = null;
138
-
139
- /**
140
- * @internal
141
- * Flag to prevent batching race conditions
142
- */
143
- private _batchingLock = false;
144
-
145
- /**
146
- * @internal
147
- * Pending batched updates
148
- */
149
- private _pendingUpdates: Array<{
150
- newState: S;
151
- oldState: S;
152
- action?: unknown;
153
- }> = [];
154
-
155
- /**
156
- * @internal
157
- * Map of consumer IDs to their WeakRef objects for proper cleanup
158
- */
159
- private _consumerRefs = new Map<string, WeakRef<object>>();
160
-
161
- /**
162
- * @internal
163
- * Validates that all consumer references are still alive
164
- * Removes dead consumers automatically
165
- */
166
- _validateConsumers = (): void => {
167
- const deadConsumers: string[] = [];
168
-
169
- for (const [consumerId, weakRef] of this._consumerRefs) {
170
- if (weakRef.deref() === undefined) {
171
- deadConsumers.push(consumerId);
172
- }
173
- }
174
-
175
- // Clean up dead consumers
176
- for (const consumerId of deadConsumers) {
177
- this._consumers.delete(consumerId);
178
- this._consumerRefs.delete(consumerId);
179
- }
180
-
181
- // Schedule disposal if no live consumers remain
182
- if (
183
- this._consumers.size === 0 &&
184
- !this._keepAlive &&
185
- this._disposalState === BlocLifecycleState.ACTIVE
186
- ) {
187
- this._scheduleDisposal();
188
- }
189
- };
190
-
191
- /**
192
- * Creates a new BlocBase instance with the given initial state.
193
- * Sets up the observer, registers with the Blac manager, and initializes addons.
194
- *
195
- * @param initialState The initial state value for this Bloc
196
- */
197
- constructor(initialState: S) {
198
- this._state = initialState;
199
- this._observer = new BlacObservable(this);
200
- this._id = this.constructor.name;
201
-
202
- // Access static properties safely with proper type checking
203
- const Constructor = this.constructor as typeof BlocBase &
204
- BlocStaticProperties;
205
-
206
- // Validate that the static properties exist and are boolean
207
- this._keepAlive =
208
- typeof Constructor.keepAlive === 'boolean'
209
- ? Constructor.keepAlive
210
- : false;
211
- this._isolated =
212
- typeof Constructor.isolated === 'boolean' ? Constructor.isolated : false;
213
- }
214
-
215
- /**
216
- * Returns the current state of the Bloc.
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.
219
- */
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
- }
226
- return this._state;
227
- }
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
-
237
- /**
238
- * @internal
239
- * Returns the name of the Bloc class for identification and debugging.
240
- */
241
- get _name() {
242
- return this.constructor.name;
243
- }
244
-
245
- /**
246
- * @internal
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
251
- */
252
- _updateId = (id?: BlocInstanceId) => {
253
- const originalId = this._id;
254
- if (!id || id === originalId) return;
255
- this._id = id;
256
- };
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
-
301
- /**
302
- * @internal
303
- * Cleans up resources and removes this Bloc from the system.
304
- * Notifies the Blac manager and clears all observers.
305
- */
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;
346
- }
347
- }
348
-
349
- /**
350
- * @internal
351
- * Optional function to be called when the Bloc is disposed.
352
- */
353
- onDispose?: () => void;
354
-
355
- /**
356
- * @internal
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));
384
- }
385
-
386
- // @ts-ignore - Blac is available globally
387
- (globalThis as any).Blac?.log(
388
- `[${this._name}:${this._id}] Consumer added. Total consumers: ${this._consumers.size}`,
389
- );
390
-
391
- // this._blac.dispatchEvent(BlacLifecycleEvent.BLOC_CONSUMER_ADDED, this, { consumerId });
392
- return true;
393
- };
394
-
395
- /**
396
- * @internal
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
401
- */
402
- _removeConsumer = (consumerId: string) => {
403
- if (!this._consumers.has(consumerId)) return;
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}`,
412
- );
413
-
414
- // If no consumers remain and not keep-alive, schedule disposal
415
- if (
416
- this._consumers.size === 0 &&
417
- !this._keepAlive &&
418
- this._disposalState === BlocLifecycleState.ACTIVE
419
- ) {
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
- this._scheduleDisposal();
425
- }
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
- }
441
-
442
- /**
443
- * @internal
444
- * Schedules disposal of this bloc instance if it has no consumers
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
- );
468
- return;
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'
528
- ) {
529
- return;
530
- }
531
-
532
- if (this._batchingEnabled) {
533
- // When batching, just accumulate the updates
534
- this._pendingUpdates.push({ newState, oldState, action });
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 = [];
587
- }
588
- };
589
- }
package/src/Cubit.ts DELETED
@@ -1,83 +0,0 @@
1
- import { Blac } from './Blac';
2
- import { BlocBase } from './BlocBase';
3
-
4
- /**
5
- * A Cubit is a simpler version of a Bloc that doesn't handle events.
6
- * It manages state and provides methods to update it.
7
- * @template S - The type of state this Cubit manages
8
- * @template P - The type of parameters (optional, defaults to null)
9
- */
10
- export abstract class Cubit<S, P = null> extends BlocBase<S, P> {
11
- /**
12
- * @internal
13
- * Protected method for useBloc to call
14
- */
15
- protected _updateProps(props: P): void {
16
- const oldProps = this.props;
17
- this.props = props;
18
- this.onPropsChanged?.(oldProps as P | undefined, props);
19
- }
20
-
21
- /**
22
- * Optional override for props handling
23
- */
24
- protected onPropsChanged?(oldProps: P | undefined, newProps: P): void;
25
- /**
26
- * Updates the current state and notifies all observers of the change.
27
- * If the new state is identical to the current state (using Object.is),
28
- * no update will occur.
29
- * @param state - The new state to set
30
- */
31
- emit(state: S): void {
32
- if (Object.is(state, this.state)) {
33
- return;
34
- }
35
-
36
- const oldState = this.state;
37
- const newState = state;
38
- this._pushState(newState, oldState);
39
- }
40
-
41
- /**
42
- * Partially updates the current state by merging it with the provided state patch.
43
- * This method is only applicable when the state is an object type.
44
- *
45
- * @param statePatch - A partial state object containing only the properties to update
46
- * @param ignoreChangeCheck - If true, skips checking if the state has actually changed
47
- * @throws {TypeError} If the state is not an object type
48
- */
49
- patch(
50
- statePatch: S extends object ? Partial<S> : S,
51
- ignoreChangeCheck = false,
52
- ): void {
53
- if (typeof this.state !== 'object' || this.state === null) {
54
- Blac.warn(
55
- 'Cubit.patch: was called on a cubit where the state is not an object. This is a no-op.',
56
- );
57
- return;
58
- }
59
-
60
- let changes = false;
61
- if (!ignoreChangeCheck) {
62
- for (const key in statePatch) {
63
- if (Object.prototype.hasOwnProperty.call(statePatch, key)) {
64
- const s = this.state;
65
- const current = s[key as keyof S];
66
- if (!Object.is(statePatch[key as keyof S], current)) {
67
- changes = true;
68
- break;
69
- }
70
- }
71
- }
72
- } else {
73
- changes = true;
74
- }
75
-
76
- if (changes) {
77
- this.emit({
78
- ...this.state,
79
- ...(statePatch as Partial<S>),
80
- } as S);
81
- }
82
- }
83
- }