@blac/core 0.1.0 → 2.0.0-rc-2
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/LICENSE +21 -0
- package/README.md +252 -0
- package/dist/index.cjs.js +2 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +505 -0
- package/dist/index.es.js +670 -0
- package/dist/index.es.js.map +1 -0
- package/package.json +38 -18
- package/src/Blac.ts +483 -0
- package/src/BlacEvent.ts +5 -0
- package/src/BlacObservable.ts +85 -0
- package/src/BlacObserver.ts +129 -0
- package/src/BlacPlugin.ts +13 -0
- package/src/Bloc.ts +109 -0
- package/src/BlocBase.ts +248 -0
- package/src/Cubit.ts +69 -0
- package/src/addons/BlacAddon.ts +17 -0
- package/src/addons/Persist.ts +105 -0
- package/src/addons/index.ts +1 -0
- package/src/index.ts +8 -0
- package/src/types.ts +62 -0
- package/dist/blac.mjs +0 -279
- package/dist/blac.umd.js +0 -1
package/src/BlacEvent.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Blac, BlacLifecycleEvent } from './Blac';
|
|
2
|
+
import { BlocBase } from './BlocBase';
|
|
3
|
+
import { BlocHookDependencyArrayFn } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Represents an observer that can subscribe to state changes in a Bloc
|
|
7
|
+
* @template S - The type of state being observed
|
|
8
|
+
*/
|
|
9
|
+
export type BlacObserver<S> = {
|
|
10
|
+
/** Function to be called when state changes */
|
|
11
|
+
fn: (newState: S, oldState: S, action?: unknown) => void | Promise<void>;
|
|
12
|
+
/** Optional function to determine if the observer should be notified of state changes */
|
|
13
|
+
dependencyArray?: BlocHookDependencyArrayFn<S>;
|
|
14
|
+
/** Cached state values used for dependency comparison */
|
|
15
|
+
lastState?: unknown[][];
|
|
16
|
+
/** Unique identifier for the observer */
|
|
17
|
+
id: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A class that manages observers for a Bloc's state changes
|
|
22
|
+
* @template S - The type of state being observed
|
|
23
|
+
*/
|
|
24
|
+
export class BlacObservable<S = unknown> {
|
|
25
|
+
/** The Bloc instance this observable is associated with */
|
|
26
|
+
bloc: BlocBase<S>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Creates a new BlacObservable instance
|
|
30
|
+
* @param bloc - The Bloc instance to observe
|
|
31
|
+
*/
|
|
32
|
+
constructor(bloc: BlocBase<S>) {
|
|
33
|
+
this.bloc = bloc;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private _observers = new Set<BlacObserver<S>>();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Gets the number of active observers
|
|
40
|
+
* @returns The number of observers currently subscribed
|
|
41
|
+
*/
|
|
42
|
+
get size(): number {
|
|
43
|
+
return this._observers.size;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Gets the set of all observers
|
|
48
|
+
* @returns The Set of all BlacObserver instances
|
|
49
|
+
*/
|
|
50
|
+
get observers() {
|
|
51
|
+
return this._observers;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Subscribes an observer to state changes
|
|
56
|
+
* @param observer - The observer to subscribe
|
|
57
|
+
* @returns A function that can be called to unsubscribe the observer
|
|
58
|
+
*/
|
|
59
|
+
subscribe(observer: BlacObserver<S>): () => void {
|
|
60
|
+
this._observers.add(observer);
|
|
61
|
+
Blac.instance.dispatchEvent(BlacLifecycleEvent.LISTENER_ADDED, this.bloc, { listenerId: observer.id });
|
|
62
|
+
|
|
63
|
+
// Immediately notify the new observer with the current state
|
|
64
|
+
// Pass current state as both newState and oldState for initial notification context
|
|
65
|
+
void observer.fn(this.bloc.state, this.bloc.state, { initialSubscription: true });
|
|
66
|
+
|
|
67
|
+
if (!observer.lastState) {
|
|
68
|
+
observer.lastState = observer.dependencyArray
|
|
69
|
+
? observer.dependencyArray(this.bloc.state)
|
|
70
|
+
: [];
|
|
71
|
+
}
|
|
72
|
+
return () => {
|
|
73
|
+
this.unsubscribe(observer);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Unsubscribes an observer from state changes
|
|
79
|
+
* @param observer - The observer to unsubscribe
|
|
80
|
+
*/
|
|
81
|
+
unsubscribe(observer: BlacObserver<S>) {
|
|
82
|
+
this._observers.delete(observer);
|
|
83
|
+
Blac.instance.dispatchEvent(BlacLifecycleEvent.LISTENER_REMOVED, this.bloc, { listenerId: observer.id });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { Blac, BlacLifecycleEvent } from './Blac';
|
|
2
|
+
import { BlocBase } from './BlocBase';
|
|
3
|
+
import { BlocHookDependencyArrayFn } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Represents an observer that can subscribe to state changes in a Bloc
|
|
7
|
+
* @template S - The type of state being observed
|
|
8
|
+
*/
|
|
9
|
+
export type BlacObserver<S> = {
|
|
10
|
+
/** Function to be called when state changes */
|
|
11
|
+
fn: (newState: S, oldState: S, action?: unknown) => void | Promise<void>;
|
|
12
|
+
/** Optional function to determine if the observer should be notified of state changes */
|
|
13
|
+
dependencyArray?: BlocHookDependencyArrayFn<S>;
|
|
14
|
+
/** Dispose function for the observer */
|
|
15
|
+
dispose?: () => void;
|
|
16
|
+
/** Cached state values used for dependency comparison */
|
|
17
|
+
lastState?: unknown[][];
|
|
18
|
+
/** Unique identifier for the observer */
|
|
19
|
+
id: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A class that manages observers for a Bloc's state changes
|
|
24
|
+
* @template S - The type of state being observed
|
|
25
|
+
*/
|
|
26
|
+
export class BlacObservable<S = unknown> {
|
|
27
|
+
/** The Bloc instance this observable is associated with */
|
|
28
|
+
bloc: BlocBase<S>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Creates a new BlacObservable instance
|
|
32
|
+
* @param bloc - The Bloc instance to observe
|
|
33
|
+
*/
|
|
34
|
+
constructor(bloc: BlocBase<S>) {
|
|
35
|
+
this.bloc = bloc;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private _observers = new Set<BlacObserver<S>>();
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Gets the number of active observers
|
|
42
|
+
* @returns The number of observers currently subscribed
|
|
43
|
+
*/
|
|
44
|
+
get size(): number {
|
|
45
|
+
return this._observers.size;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Gets the set of all observers
|
|
50
|
+
* @returns The Set of all BlacObserver instances
|
|
51
|
+
*/
|
|
52
|
+
get observers() {
|
|
53
|
+
return this._observers;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Subscribes an observer to state changes
|
|
58
|
+
* @param observer - The observer to subscribe
|
|
59
|
+
* @returns A function that can be called to unsubscribe the observer
|
|
60
|
+
*/
|
|
61
|
+
subscribe(observer: BlacObserver<S>): () => void {
|
|
62
|
+
this._observers.add(observer);
|
|
63
|
+
Blac.instance.dispatchEvent(BlacLifecycleEvent.LISTENER_ADDED, this.bloc, { listenerId: observer.id });
|
|
64
|
+
if (!observer.lastState) {
|
|
65
|
+
observer.lastState = observer.dependencyArray
|
|
66
|
+
? observer.dependencyArray(this.bloc.state)
|
|
67
|
+
: [];
|
|
68
|
+
}
|
|
69
|
+
return () => {
|
|
70
|
+
this.unsubscribe(observer);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Unsubscribes an observer from state changes
|
|
76
|
+
* @param observer - The observer to unsubscribe
|
|
77
|
+
*/
|
|
78
|
+
unsubscribe(observer: BlacObserver<S>) {
|
|
79
|
+
observer.dispose?.();
|
|
80
|
+
this._observers.delete(observer);
|
|
81
|
+
Blac.instance.dispatchEvent(BlacLifecycleEvent.LISTENER_REMOVED, this.bloc, { listenerId: observer.id });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Notifies all observers of a state change
|
|
86
|
+
* @param newState - The new state value
|
|
87
|
+
* @param oldState - The previous state value
|
|
88
|
+
* @param action - Optional action that triggered the state change
|
|
89
|
+
*/
|
|
90
|
+
notify(newState: S, oldState: S, action?: unknown) {
|
|
91
|
+
this._observers.forEach((observer) => {
|
|
92
|
+
let shouldUpdate = false;
|
|
93
|
+
|
|
94
|
+
if (observer.dependencyArray) {
|
|
95
|
+
const lastDependencyCheck = observer.lastState || [];
|
|
96
|
+
const newDependencyCheck = observer.dependencyArray(newState);
|
|
97
|
+
|
|
98
|
+
for (let o = 0; o < newDependencyCheck.length; o++) {
|
|
99
|
+
const partNew = newDependencyCheck[o];
|
|
100
|
+
const partOld = lastDependencyCheck[o] || [];
|
|
101
|
+
for (let i = 0; i < partNew.length; i++) {
|
|
102
|
+
if (!Object.is(partNew[i], partOld[i])) {
|
|
103
|
+
shouldUpdate = true;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
observer.lastState = newDependencyCheck;
|
|
110
|
+
} else {
|
|
111
|
+
shouldUpdate = true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (shouldUpdate) {
|
|
115
|
+
void observer.fn(newState, oldState, action);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Disposes of all observers and clears the observer set
|
|
122
|
+
*/
|
|
123
|
+
dispose() {
|
|
124
|
+
this._observers.forEach((observer) => {
|
|
125
|
+
this.unsubscribe(observer);
|
|
126
|
+
});
|
|
127
|
+
this._observers.clear();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { BlacLifecycleEvent } from './Blac';
|
|
2
|
+
import { BlocBase } from './BlocBase';
|
|
3
|
+
|
|
4
|
+
export interface BlacPlugin {
|
|
5
|
+
name: string;
|
|
6
|
+
|
|
7
|
+
onEvent(
|
|
8
|
+
event: BlacLifecycleEvent,
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
+
bloc: BlocBase<any>,
|
|
11
|
+
params?: unknown,
|
|
12
|
+
): void;
|
|
13
|
+
}
|
package/src/Bloc.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Blac } from './Blac';
|
|
2
|
+
import { BlocBase } from './BlocBase';
|
|
3
|
+
|
|
4
|
+
// A should be the base type for all events this Bloc handles and must be an object type
|
|
5
|
+
// to access action.constructor. Events are typically class instances.
|
|
6
|
+
// P is for props, changed from any to unknown.
|
|
7
|
+
export abstract class Bloc<
|
|
8
|
+
S, // State type
|
|
9
|
+
A extends object, // Base Action/Event type, constrained to object
|
|
10
|
+
P = unknown // Props type
|
|
11
|
+
> extends BlocBase<S, P> {
|
|
12
|
+
// Stores handlers: Map<EventConstructor (subtype of A), HandlerFunction>
|
|
13
|
+
// The handler's event parameter will be correctly typed to the specific EventConstructor
|
|
14
|
+
// by the 'on' method's signature.
|
|
15
|
+
readonly eventHandlers: Map<
|
|
16
|
+
// Key: Constructor of a specific event E (where E extends A)
|
|
17
|
+
// Using 'any[]' for constructor arguments for broader compatibility.
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
|
+
new (...args: any[]) => A,
|
|
20
|
+
// Value: Handler function. 'event: A' is used here for the stored function type.
|
|
21
|
+
// The 'on' method ensures the specific handler (event: E) is correctly typed.
|
|
22
|
+
(event: A, emit: (newState: S) => void) => void | Promise<void>
|
|
23
|
+
> = new Map();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Registers an event handler for a specific event type.
|
|
27
|
+
* This method is typically called in the constructor of a derived Bloc class.
|
|
28
|
+
* @param eventConstructor The constructor of the event to handle (e.g., LoadDataEvent).
|
|
29
|
+
* @param handler A function that processes the event and can emit new states.
|
|
30
|
+
* The 'event' parameter in the handler will be typed to the specific eventConstructor.
|
|
31
|
+
*/
|
|
32
|
+
protected on<E extends A>(
|
|
33
|
+
// Using 'any[]' for constructor arguments for broader compatibility.
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
|
+
eventConstructor: new (...args: any[]) => E,
|
|
36
|
+
handler: (event: E, emit: (newState: S) => void) => void | Promise<void>
|
|
37
|
+
): void {
|
|
38
|
+
if (this.eventHandlers.has(eventConstructor)) {
|
|
39
|
+
// Using Blac.warn or a similar logging mechanism from BlocBase if available,
|
|
40
|
+
// otherwise console.warn. Assuming this._name and this._id are available from BlocBase.
|
|
41
|
+
Blac.warn(
|
|
42
|
+
`[Bloc ${this._name}:${String(this._id)}] Handler for event '${eventConstructor.name}' already registered. It will be overwritten.`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
// Cast the specific handler (event: E) to a more general (event: A) for storage.
|
|
46
|
+
// This is safe because E extends A. When the handler is called with an 'action' of type A,
|
|
47
|
+
// if it was originally registered for type E, 'action' must be an instance of E.
|
|
48
|
+
this.eventHandlers.set(
|
|
49
|
+
eventConstructor,
|
|
50
|
+
handler as (event: A, emit: (newState: S) => void) => void | Promise<void>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Dispatches an action/event to the Bloc.
|
|
56
|
+
* If a handler is registered for this specific event type (via 'on'), it will be invoked.
|
|
57
|
+
* Asynchronous handlers are awaited.
|
|
58
|
+
* @param action The action/event instance to be processed.
|
|
59
|
+
*/
|
|
60
|
+
public add = async (action: A): Promise<void> => {
|
|
61
|
+
// Using 'any[]' for constructor arguments for broader compatibility.
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
63
|
+
const eventConstructor = action.constructor as new (...args: any[]) => A;
|
|
64
|
+
const handler = this.eventHandlers.get(eventConstructor);
|
|
65
|
+
|
|
66
|
+
if (handler) {
|
|
67
|
+
// Define the 'emit' function that handlers will use to update state.
|
|
68
|
+
// It captures the current state ('this.state') right before each emission
|
|
69
|
+
// to provide the correct 'previousState' to _pushState.
|
|
70
|
+
const emit = (newState: S): void => {
|
|
71
|
+
const previousState = this.state; // State just before this specific emission
|
|
72
|
+
// The 'action' passed to _pushState is the original action that triggered the handler,
|
|
73
|
+
// providing context for the state change (e.g., for logging or plugins).
|
|
74
|
+
this._pushState(newState, previousState, action);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
// Await the handler in case it's an async function (e.g., performs API calls).
|
|
79
|
+
// The 'action' is passed to the handler, and due to the way 'on' is typed,
|
|
80
|
+
// the 'event' parameter within the handler function will be correctly
|
|
81
|
+
// typed to its specific class (e.g., LoadMyFeatureData).
|
|
82
|
+
await handler(action, emit);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
// It's good practice to handle errors occurring within event handlers.
|
|
85
|
+
Blac.error(
|
|
86
|
+
`[Bloc ${this._name}:${String(this._id)}] Error in event handler for '${eventConstructor.name}':`,
|
|
87
|
+
error,
|
|
88
|
+
"Action:", action
|
|
89
|
+
);
|
|
90
|
+
// Depending on the desired error handling strategy, you might:
|
|
91
|
+
// 1. Emit a specific error state: this.emit(new MyErrorState(error));
|
|
92
|
+
// 2. Re-throw the error: throw error;
|
|
93
|
+
// 3. Log and ignore (as done here by default).
|
|
94
|
+
// This should be decided based on application requirements.
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
// action.constructor.name should be safe due to 'A extends object' and common JS practice for constructors.
|
|
98
|
+
// If linting still complains, it might be overly strict for this common pattern.
|
|
99
|
+
const constructorName = (action.constructor as { name?: string }).name || 'UnnamedConstructor';
|
|
100
|
+
Blac.warn(
|
|
101
|
+
`[Bloc ${this._name}:${String(this._id)}] No handler registered for action type: '${constructorName}'. Action was:`,
|
|
102
|
+
action
|
|
103
|
+
);
|
|
104
|
+
// If no handler is found, the action is effectively ignored.
|
|
105
|
+
// Consider if this is the desired behavior or if an error should be thrown
|
|
106
|
+
// or a default handler should be invoked.
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
package/src/BlocBase.ts
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { Blac, BlacLifecycleEvent } from './Blac';
|
|
2
|
+
import { BlacObservable } from './BlacObserver';
|
|
3
|
+
import BlacAddon from './addons/BlacAddon';
|
|
4
|
+
import { BlocConstructor } from './types';
|
|
5
|
+
|
|
6
|
+
export type BlocInstanceId = string | number | undefined;
|
|
7
|
+
type DependencySelector<S> = (newState: S) => unknown[][];
|
|
8
|
+
|
|
9
|
+
// Define an interface for the static properties expected on a Bloc/Cubit constructor
|
|
10
|
+
interface BlocStaticProperties {
|
|
11
|
+
isolated: boolean;
|
|
12
|
+
keepAlive: boolean;
|
|
13
|
+
addons?: BlacAddon[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Base class for both Blocs and Cubits that provides core state management functionality.
|
|
18
|
+
* Handles state transitions, observer notifications, lifecycle management, and addon integration.
|
|
19
|
+
*
|
|
20
|
+
* @abstract This class should be extended, not instantiated directly
|
|
21
|
+
* @template S The type of state managed by this Bloc
|
|
22
|
+
* @template P The type of props that can be passed during instance creation (optional)
|
|
23
|
+
*/
|
|
24
|
+
export abstract class BlocBase<
|
|
25
|
+
S,
|
|
26
|
+
P = unknown
|
|
27
|
+
> {
|
|
28
|
+
/**
|
|
29
|
+
* When true, every consumer will receive its own unique instance of this Bloc.
|
|
30
|
+
* Use this when state should not be shared between components.
|
|
31
|
+
* @default false
|
|
32
|
+
*/
|
|
33
|
+
static isolated = false;
|
|
34
|
+
get isIsolated() {
|
|
35
|
+
return this._isolated;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* When true, the Bloc instance persists even when there are no active consumers.
|
|
40
|
+
* Useful for maintaining state between component unmount/remount cycles.
|
|
41
|
+
* @default false
|
|
42
|
+
*/
|
|
43
|
+
static keepAlive = false;
|
|
44
|
+
get isKeepAlive() {
|
|
45
|
+
return this._keepAlive;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Defines how dependencies are selected from the state for efficient updates.
|
|
50
|
+
* When provided, observers will only be notified when selected dependencies change.
|
|
51
|
+
*/
|
|
52
|
+
defaultDependencySelector: DependencySelector<S> | undefined;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @internal
|
|
56
|
+
* Optional array of addons to extend the functionality of this Bloc.
|
|
57
|
+
*/
|
|
58
|
+
public _addons?: BlacAddon[];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @internal
|
|
62
|
+
* Indicates if this specific Bloc instance is isolated from others of the same type.
|
|
63
|
+
*/
|
|
64
|
+
public _isolated = false;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @internal
|
|
68
|
+
* Observable responsible for managing state listeners and notifying consumers.
|
|
69
|
+
*/
|
|
70
|
+
public _observer: BlacObservable<S>;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @internal
|
|
74
|
+
* Reference to the global Blac manager instance.
|
|
75
|
+
*/
|
|
76
|
+
public _blac = Blac.getInstance();
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The unique identifier for this Bloc instance.
|
|
80
|
+
* Defaults to the class name, but can be customized.
|
|
81
|
+
*/
|
|
82
|
+
public _id: BlocInstanceId;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @internal
|
|
86
|
+
* Reference string used internally for tracking and debugging.
|
|
87
|
+
*/
|
|
88
|
+
public _instanceRef?: string;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @internal
|
|
92
|
+
* Indicates if this specific Bloc instance should be kept alive when no consumers are present.
|
|
93
|
+
*/
|
|
94
|
+
public _keepAlive = false;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @readonly
|
|
98
|
+
* Timestamp when this Bloc instance was created, useful for debugging and performance tracking.
|
|
99
|
+
*/
|
|
100
|
+
public readonly _createdAt = Date.now();
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @internal
|
|
104
|
+
* The current state of the Bloc.
|
|
105
|
+
*/
|
|
106
|
+
public _state: S;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @internal
|
|
110
|
+
* The previous state of the Bloc, maintained for comparison and history.
|
|
111
|
+
*/
|
|
112
|
+
public _oldState: S | undefined;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Props passed during Bloc instance creation.
|
|
116
|
+
* Can be used to configure or parameterize the Bloc's behavior.
|
|
117
|
+
*/
|
|
118
|
+
public props: P | null = null;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Creates a new BlocBase instance with the given initial state.
|
|
122
|
+
* Sets up the observer, registers with the Blac manager, and initializes addons.
|
|
123
|
+
*
|
|
124
|
+
* @param initialState The initial state value for this Bloc
|
|
125
|
+
*/
|
|
126
|
+
constructor(initialState: S) {
|
|
127
|
+
this._state = initialState;
|
|
128
|
+
this._observer = new BlacObservable(this);
|
|
129
|
+
this._blac.dispatchEvent(BlacLifecycleEvent.BLOC_CREATED, this);
|
|
130
|
+
this._id = this.constructor.name;
|
|
131
|
+
|
|
132
|
+
// Use a type assertion for the constructor to access static properties safely
|
|
133
|
+
const constructorWithStaticProps = this.constructor as BlocConstructor<this> & BlocStaticProperties;
|
|
134
|
+
|
|
135
|
+
this._keepAlive = constructorWithStaticProps.keepAlive;
|
|
136
|
+
this._isolated = constructorWithStaticProps.isolated;
|
|
137
|
+
this._addons = constructorWithStaticProps.addons;
|
|
138
|
+
|
|
139
|
+
this._connectAddons();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Returns the current state of the Bloc.
|
|
144
|
+
* Use this getter to access the state in a read-only manner.
|
|
145
|
+
*/
|
|
146
|
+
get state(): S {
|
|
147
|
+
return this._state;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* @internal
|
|
152
|
+
* Returns the name of the Bloc class for identification and debugging.
|
|
153
|
+
*/
|
|
154
|
+
get _name() {
|
|
155
|
+
return this.constructor.name;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @internal
|
|
160
|
+
* Updates the Bloc instance's ID to a new value.
|
|
161
|
+
* Only updates if the new ID is defined and different from the current one.
|
|
162
|
+
*
|
|
163
|
+
* @param id The new ID to assign to this Bloc instance
|
|
164
|
+
*/
|
|
165
|
+
_updateId = (id?: BlocInstanceId) => {
|
|
166
|
+
const originalId = this._id;
|
|
167
|
+
if (!id || id === originalId) return;
|
|
168
|
+
this._id = id;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* @internal
|
|
173
|
+
* Cleans up resources and removes this Bloc from the system.
|
|
174
|
+
* Notifies the Blac manager and clears all observers.
|
|
175
|
+
*/
|
|
176
|
+
_dispose() {
|
|
177
|
+
this._observer.dispose();
|
|
178
|
+
this._blac.dispatchEvent(BlacLifecycleEvent.BLOC_DISPOSED, this);
|
|
179
|
+
this.onDispose?.();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* @internal
|
|
184
|
+
* Optional function to be called when the Bloc is disposed.
|
|
185
|
+
*/
|
|
186
|
+
onDispose?: () => void;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @internal
|
|
190
|
+
* Set of consumer IDs currently listening to this Bloc's state changes.
|
|
191
|
+
*/
|
|
192
|
+
_consumers = new Set<string>();
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* @internal
|
|
196
|
+
* Registers a new consumer to this Bloc instance.
|
|
197
|
+
* Notifies the Blac manager that a consumer has been added.
|
|
198
|
+
*
|
|
199
|
+
* @param consumerId The unique ID of the consumer being added
|
|
200
|
+
*/
|
|
201
|
+
_addConsumer = (consumerId: string) => {
|
|
202
|
+
this._consumers.add(consumerId);
|
|
203
|
+
this._blac.dispatchEvent(BlacLifecycleEvent.BLOC_CONSUMER_ADDED, this, { consumerId });
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* @internal
|
|
208
|
+
* Unregisters a consumer from this Bloc instance.
|
|
209
|
+
* Notifies the Blac manager that a consumer has been removed.
|
|
210
|
+
*
|
|
211
|
+
* @param consumerId The unique ID of the consumer being removed
|
|
212
|
+
*/
|
|
213
|
+
_removeConsumer = (consumerId: string) => {
|
|
214
|
+
this._blac.log(`[${this._name}:${String(this._id ?? 'default_id')}] Removing consumer: ${consumerId}`);
|
|
215
|
+
this._consumers.delete(consumerId);
|
|
216
|
+
this._blac.dispatchEvent(BlacLifecycleEvent.BLOC_CONSUMER_REMOVED, this, { consumerId });
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* @internal
|
|
221
|
+
* Initializes all registered addons for this Bloc instance.
|
|
222
|
+
* Calls the onInit lifecycle method on each addon if defined.
|
|
223
|
+
*/
|
|
224
|
+
_connectAddons = () => {
|
|
225
|
+
const { _addons: addons } = this;
|
|
226
|
+
if (addons) {
|
|
227
|
+
addons.forEach(addon => {
|
|
228
|
+
addon.onInit?.(this);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
lastUpdate = Date.now();
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* @internal
|
|
237
|
+
* Updates the state and notifies all observers of the change.
|
|
238
|
+
*
|
|
239
|
+
* @param newState The new state to be set
|
|
240
|
+
* @param oldState The previous state for comparison
|
|
241
|
+
* @param action Optional metadata about what caused the state change
|
|
242
|
+
*/
|
|
243
|
+
_pushState = (newState: S, oldState: S, action?: unknown): void => {
|
|
244
|
+
this._state = newState;
|
|
245
|
+
this._observer.notify(newState, oldState, action);
|
|
246
|
+
this.lastUpdate = Date.now();
|
|
247
|
+
};
|
|
248
|
+
}
|
package/src/Cubit.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
* Updates the current state and notifies all observers of the change.
|
|
13
|
+
* If the new state is identical to the current state (using Object.is),
|
|
14
|
+
* no update will occur.
|
|
15
|
+
* @param state - The new state to set
|
|
16
|
+
*/
|
|
17
|
+
emit(state: S): void {
|
|
18
|
+
if (Object.is(state, this.state)) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const oldState = this.state;
|
|
23
|
+
const newState = state;
|
|
24
|
+
this._pushState(newState, oldState);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Partially updates the current state by merging it with the provided state patch.
|
|
29
|
+
* This method is only applicable when the state is an object type.
|
|
30
|
+
*
|
|
31
|
+
* @param statePatch - A partial state object containing only the properties to update
|
|
32
|
+
* @param ignoreChangeCheck - If true, skips checking if the state has actually changed
|
|
33
|
+
* @throws {TypeError} If the state is not an object type
|
|
34
|
+
*/
|
|
35
|
+
patch(
|
|
36
|
+
statePatch: S extends object ? Partial<S> : S,
|
|
37
|
+
ignoreChangeCheck = false,
|
|
38
|
+
): void {
|
|
39
|
+
if (typeof this.state !== 'object' || this.state === null) {
|
|
40
|
+
Blac.warn(
|
|
41
|
+
'Cubit.patch: was called on a cubit where the state is not an object. This is a no-op.',
|
|
42
|
+
);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let changes = false;
|
|
47
|
+
if (!ignoreChangeCheck) {
|
|
48
|
+
for (const key in statePatch) {
|
|
49
|
+
if (Object.prototype.hasOwnProperty.call(statePatch, key)) {
|
|
50
|
+
const s = this.state;
|
|
51
|
+
const current = s[key as keyof S];
|
|
52
|
+
if (!Object.is(statePatch[key as keyof S], current)) {
|
|
53
|
+
changes = true;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
changes = true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (changes) {
|
|
63
|
+
this.emit({
|
|
64
|
+
...this.state,
|
|
65
|
+
...(statePatch as Partial<S>),
|
|
66
|
+
} as S);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { BlocBase } from '../BlocBase';
|
|
2
|
+
|
|
3
|
+
export type BlacAddonInit = (bloc: BlocBase<any>) => void;
|
|
4
|
+
|
|
5
|
+
export type BlacAddonEmit = (params: {
|
|
6
|
+
oldState: unknown;
|
|
7
|
+
newState: unknown;
|
|
8
|
+
cubit: BlocBase<any>;
|
|
9
|
+
}) => void;
|
|
10
|
+
|
|
11
|
+
type BlacAddon = {
|
|
12
|
+
name: string;
|
|
13
|
+
onInit?: BlacAddonInit;
|
|
14
|
+
onEmit?: BlacAddonEmit;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default BlacAddon;
|