@blac/core 2.0.0-rc-10 → 2.0.0-rc-13
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/README.md +27 -0
- package/dist/index.cjs.js +2 -2
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +294 -11
- package/dist/index.es.js +869 -397
- package/dist/index.es.js.map +1 -1
- package/package.json +1 -1
- package/src/Blac.ts +94 -9
- package/src/BlacObserver.ts +3 -0
- package/src/Bloc.ts +43 -49
- package/src/BlocBase.ts +107 -4
- package/src/Cubit.ts +14 -0
- package/src/__tests__/Blac.config.test.ts +86 -0
- package/src/__tests__/plugins.test.ts +454 -0
- package/src/__tests__/props.test.ts +256 -0
- package/src/adapter/BlacAdapter.ts +73 -8
- package/src/events.ts +3 -0
- package/src/index.ts +5 -0
- package/src/plugins/BlocPluginRegistry.ts +220 -0
- package/src/plugins/SystemPluginRegistry.ts +192 -0
- package/src/plugins/index.ts +8 -0
- package/src/plugins/types.ts +100 -0
- package/src/types.ts +1 -10
- package/src/utils/shallowEqual.ts +40 -0
package/package.json
CHANGED
package/src/Blac.ts
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
// TODO: The 'any' types in this file are necessary for proper type inference in complex generic scenarios.
|
|
3
|
-
// Specifically:
|
|
4
|
-
// 1. BlocConstructor<any> in Maps - allows any bloc type to be stored while maintaining type safety in usage
|
|
5
|
-
// 2. Type assertions for _disposalState - private property access across inheritance hierarchy
|
|
6
|
-
// 3. Constructor argument types - enables flexible bloc instantiation patterns
|
|
7
|
-
// These 'any' usages are carefully controlled and don't compromise runtime type safety.
|
|
8
1
|
import { BlocBase, BlocInstanceId, BlocLifecycleState } from './BlocBase';
|
|
9
2
|
import {
|
|
10
3
|
BlocBaseAbstract,
|
|
@@ -13,6 +6,7 @@ import {
|
|
|
13
6
|
BlocState,
|
|
14
7
|
InferPropsFromGeneric,
|
|
15
8
|
} from './types';
|
|
9
|
+
import { SystemPluginRegistry } from './plugins/SystemPluginRegistry';
|
|
16
10
|
|
|
17
11
|
/**
|
|
18
12
|
* Configuration options for the Blac instance
|
|
@@ -20,6 +14,12 @@ import {
|
|
|
20
14
|
export interface BlacConfig {
|
|
21
15
|
/** Whether to expose the Blac instance globally */
|
|
22
16
|
exposeBlacInstance?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Whether to enable proxy dependency tracking for automatic re-render optimization.
|
|
19
|
+
* When false, state changes always cause re-renders unless dependencies are manually specified.
|
|
20
|
+
* Default: true
|
|
21
|
+
*/
|
|
22
|
+
proxyDependencyTracking?: boolean;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export interface GetBlocOptions<B extends BlocBase<unknown>> {
|
|
@@ -108,6 +108,46 @@ export class Blac {
|
|
|
108
108
|
static get getAllBlocs() {
|
|
109
109
|
return Blac.instance.getAllBlocs;
|
|
110
110
|
}
|
|
111
|
+
|
|
112
|
+
/** Private static configuration */
|
|
113
|
+
private static _config: BlacConfig = {
|
|
114
|
+
exposeBlacInstance: false,
|
|
115
|
+
proxyDependencyTracking: true,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/** Get current configuration */
|
|
119
|
+
static get config(): Readonly<BlacConfig> {
|
|
120
|
+
return { ...this._config };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Set or update Blac configuration
|
|
125
|
+
* @param config - Partial configuration to merge with existing config
|
|
126
|
+
* @throws Error if configuration is invalid
|
|
127
|
+
*/
|
|
128
|
+
static setConfig(config: Partial<BlacConfig>): void {
|
|
129
|
+
// Validate config
|
|
130
|
+
if (config.proxyDependencyTracking !== undefined &&
|
|
131
|
+
typeof config.proxyDependencyTracking !== 'boolean') {
|
|
132
|
+
throw new Error('BlacConfig.proxyDependencyTracking must be a boolean');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (config.exposeBlacInstance !== undefined &&
|
|
136
|
+
typeof config.exposeBlacInstance !== 'boolean') {
|
|
137
|
+
throw new Error('BlacConfig.exposeBlacInstance must be a boolean');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Merge with existing config
|
|
141
|
+
this._config = {
|
|
142
|
+
...this._config,
|
|
143
|
+
...config,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Log config update if logging is enabled
|
|
147
|
+
if (this.enableLog) {
|
|
148
|
+
this.instance.log('Blac config updated:', this._config);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
111
151
|
/** Map storing all registered bloc instances by their class name and ID */
|
|
112
152
|
blocInstanceMap: Map<string, BlocBase<unknown>> = new Map();
|
|
113
153
|
/** Map storing isolated bloc instances grouped by their constructor */
|
|
@@ -123,6 +163,8 @@ export class Blac {
|
|
|
123
163
|
keepAliveBlocs: Set<BlocBase<unknown>> = new Set();
|
|
124
164
|
/** Flag to control whether changes should be posted to document */
|
|
125
165
|
postChangesToDocument = false;
|
|
166
|
+
/** System plugin registry */
|
|
167
|
+
readonly plugins = new SystemPluginRegistry();
|
|
126
168
|
|
|
127
169
|
/**
|
|
128
170
|
* Creates a new Blac instance.
|
|
@@ -134,6 +176,9 @@ export class Blac {
|
|
|
134
176
|
return Blac.instance;
|
|
135
177
|
}
|
|
136
178
|
instanceManager.setInstance(this);
|
|
179
|
+
|
|
180
|
+
// Bootstrap plugins on creation
|
|
181
|
+
this.plugins.bootstrap();
|
|
137
182
|
}
|
|
138
183
|
|
|
139
184
|
/** Flag to enable/disable logging */
|
|
@@ -274,6 +319,9 @@ export class Blac {
|
|
|
274
319
|
// First dispose the bloc to prevent further operations
|
|
275
320
|
bloc._dispose();
|
|
276
321
|
|
|
322
|
+
// Notify plugins of bloc disposal
|
|
323
|
+
this.plugins.notifyBlocDisposed(bloc);
|
|
324
|
+
|
|
277
325
|
// Then clean up from registries
|
|
278
326
|
if (base.isolated) {
|
|
279
327
|
this.unregisterIsolatedBlocInstance(bloc);
|
|
@@ -468,10 +516,16 @@ export class Blac {
|
|
|
468
516
|
|
|
469
517
|
if (newBloc.isIsolated) {
|
|
470
518
|
this.registerIsolatedBlocInstance(newBloc);
|
|
471
|
-
|
|
519
|
+
} else {
|
|
520
|
+
this.registerBlocInstance(newBloc);
|
|
472
521
|
}
|
|
473
522
|
|
|
474
|
-
|
|
523
|
+
// Activate bloc plugins
|
|
524
|
+
newBloc._activatePlugins();
|
|
525
|
+
|
|
526
|
+
// Notify system plugins of bloc creation
|
|
527
|
+
this.plugins.notifyBlocCreated(newBloc);
|
|
528
|
+
|
|
475
529
|
return newBloc;
|
|
476
530
|
}
|
|
477
531
|
|
|
@@ -747,4 +801,35 @@ export class Blac {
|
|
|
747
801
|
static get validateConsumers() {
|
|
748
802
|
return Blac.instance.validateConsumers;
|
|
749
803
|
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Bootstrap the Blac instance and all plugins
|
|
807
|
+
*/
|
|
808
|
+
bootstrap(): void {
|
|
809
|
+
this.plugins.bootstrap();
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Shutdown the Blac instance and all plugins
|
|
814
|
+
*/
|
|
815
|
+
shutdown(): void {
|
|
816
|
+
this.plugins.shutdown();
|
|
817
|
+
|
|
818
|
+
// Dispose all non-keepAlive blocs
|
|
819
|
+
for (const bloc of this.blocInstanceMap.values()) {
|
|
820
|
+
if (!bloc._keepAlive) {
|
|
821
|
+
this.disposeBloc(bloc);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
for (const blocs of this.isolatedBlocMap.values()) {
|
|
826
|
+
for (const bloc of blocs) {
|
|
827
|
+
if (!bloc._keepAlive) {
|
|
828
|
+
this.disposeBloc(bloc);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
|
|
750
835
|
}
|
package/src/BlacObserver.ts
CHANGED
|
@@ -174,6 +174,9 @@ export class BlacObservable<S = unknown> {
|
|
|
174
174
|
void observer.fn(newState, oldState, action);
|
|
175
175
|
}
|
|
176
176
|
});
|
|
177
|
+
|
|
178
|
+
// Notify system plugins of state change
|
|
179
|
+
Blac.instance.plugins.notifyStateChanged(this.bloc, oldState, newState);
|
|
177
180
|
}
|
|
178
181
|
|
|
179
182
|
/**
|
package/src/Bloc.ts
CHANGED
|
@@ -2,27 +2,13 @@ import { Blac } from './Blac';
|
|
|
2
2
|
import { BlocBase } from './BlocBase';
|
|
3
3
|
import { BlocEventConstraint } from './types';
|
|
4
4
|
|
|
5
|
-
// A should be the base type for all events this Bloc handles and must extend BlocEventConstraint
|
|
6
|
-
// to access action.constructor and ensure proper event structure.
|
|
7
|
-
// P is for props, changed from any to unknown.
|
|
8
5
|
export abstract class Bloc<
|
|
9
6
|
S, // State type
|
|
10
7
|
A extends BlocEventConstraint = BlocEventConstraint, // Base Action/Event type with proper constraints
|
|
11
8
|
P = unknown, // Props type
|
|
12
9
|
> extends BlocBase<S, P> {
|
|
13
|
-
// Stores handlers: Map<EventConstructor (subtype of A), HandlerFunction>
|
|
14
|
-
// The handler's event parameter will be correctly typed to the specific EventConstructor
|
|
15
|
-
// by the 'on' method's signature.
|
|
16
10
|
readonly eventHandlers: Map<
|
|
17
|
-
// Key: Constructor of a specific event E (where E extends A)
|
|
18
|
-
// TODO: 'any[]' is required for constructor arguments to allow flexible event instantiation.
|
|
19
|
-
// Using specific parameter types would break type inference for events with different
|
|
20
|
-
// constructor signatures. The 'any[]' enables polymorphic event handling while
|
|
21
|
-
// maintaining type safety through the generic constraint 'E extends A'.
|
|
22
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
11
|
new (...args: any[]) => A,
|
|
24
|
-
// Value: Handler function. 'event: A' is used here for the stored function type.
|
|
25
|
-
// The 'on' method ensures the specific handler (event: E) is correctly typed.
|
|
26
12
|
(event: A, emit: (newState: S) => void) => void | Promise<void>
|
|
27
13
|
> = new Map();
|
|
28
14
|
|
|
@@ -46,22 +32,14 @@ export abstract class Bloc<
|
|
|
46
32
|
* The 'event' parameter in the handler will be typed to the specific eventConstructor.
|
|
47
33
|
*/
|
|
48
34
|
protected on<E extends A>(
|
|
49
|
-
// TODO: 'any[]' is required for constructor arguments (see explanation above).
|
|
50
|
-
// This allows events with different constructor signatures to be handled uniformly.
|
|
51
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
52
35
|
eventConstructor: new (...args: any[]) => E,
|
|
53
36
|
handler: (event: E, emit: (newState: S) => void) => void | Promise<void>,
|
|
54
37
|
): void {
|
|
55
38
|
if (this.eventHandlers.has(eventConstructor)) {
|
|
56
|
-
// Using Blac.warn or a similar logging mechanism from BlocBase if available,
|
|
57
|
-
// otherwise console.warn. Assuming this._name and this._id are available from BlocBase.
|
|
58
39
|
Blac.warn(
|
|
59
40
|
`[Bloc ${this._name}:${String(this._id)}] Handler for event '${eventConstructor.name}' already registered. It will be overwritten.`,
|
|
60
41
|
);
|
|
61
42
|
}
|
|
62
|
-
// Cast the specific handler (event: E) to a more general (event: A) for storage.
|
|
63
|
-
// This is safe because E extends A. When the handler is called with an 'action' of type A,
|
|
64
|
-
// if it was originally registered for type E, 'action' must be an instance of E.
|
|
65
43
|
this.eventHandlers.set(
|
|
66
44
|
eventConstructor,
|
|
67
45
|
handler as (
|
|
@@ -77,10 +55,32 @@ export abstract class Bloc<
|
|
|
77
55
|
* @param action The action/event instance to be processed.
|
|
78
56
|
*/
|
|
79
57
|
public add = async (action: A): Promise<void> => {
|
|
80
|
-
//
|
|
81
|
-
|
|
58
|
+
// Transform event through plugins
|
|
59
|
+
let transformedAction: A | null = action;
|
|
60
|
+
try {
|
|
61
|
+
transformedAction = (this._plugins as any).transformEvent(action);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error('Error transforming event:', error);
|
|
64
|
+
// Continue with original event if transformation fails
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// If event was cancelled by plugin, don't process it
|
|
68
|
+
if (transformedAction === null) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Notify bloc plugins of event
|
|
73
|
+
try {
|
|
74
|
+
(this._plugins as any).notifyEvent(transformedAction);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error('Error notifying plugins of event:', error);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Notify system plugins of event
|
|
80
|
+
Blac.instance.plugins.notifyEventAdded(this as any, transformedAction);
|
|
81
|
+
|
|
82
|
+
this._eventQueue.push(transformedAction);
|
|
82
83
|
|
|
83
|
-
// If not already processing, start processing the queue
|
|
84
84
|
if (!this._isProcessingEvent) {
|
|
85
85
|
await this._processEventQueue();
|
|
86
86
|
}
|
|
@@ -91,7 +91,6 @@ export abstract class Bloc<
|
|
|
91
91
|
* Processes events from the queue sequentially
|
|
92
92
|
*/
|
|
93
93
|
private async _processEventQueue(): Promise<void> {
|
|
94
|
-
// Prevent concurrent processing
|
|
95
94
|
if (this._isProcessingEvent) {
|
|
96
95
|
return;
|
|
97
96
|
}
|
|
@@ -113,34 +112,18 @@ export abstract class Bloc<
|
|
|
113
112
|
* Processes a single event
|
|
114
113
|
*/
|
|
115
114
|
private async _processEvent(action: A): Promise<void> {
|
|
116
|
-
// Using 'any[]' for constructor arguments for broader compatibility.
|
|
117
|
-
// TODO: Type assertion required to cast action.constructor to proper event constructor type.
|
|
118
|
-
// JavaScript's constructor property returns 'Function', but we need the specific event
|
|
119
|
-
// constructor type to look up handlers. This is safe because we validate the action
|
|
120
|
-
// extends the BlocEventConstraint interface.
|
|
121
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
122
115
|
const eventConstructor = action.constructor as new (...args: any[]) => A;
|
|
123
116
|
const handler = this.eventHandlers.get(eventConstructor);
|
|
124
117
|
|
|
125
118
|
if (handler) {
|
|
126
|
-
// Define the 'emit' function that handlers will use to update state.
|
|
127
|
-
// It captures the current state ('this.state') right before each emission
|
|
128
|
-
// to provide the correct 'previousState' to _pushState.
|
|
129
119
|
const emit = (newState: S): void => {
|
|
130
120
|
const previousState = this.state; // State just before this specific emission
|
|
131
|
-
// The 'action' passed to _pushState is the original action that triggered the handler,
|
|
132
|
-
// providing context for the state change (e.g., for logging or plugins).
|
|
133
121
|
this._pushState(newState, previousState, action);
|
|
134
122
|
};
|
|
135
123
|
|
|
136
124
|
try {
|
|
137
|
-
// Await the handler in case it's an async function (e.g., performs API calls).
|
|
138
|
-
// The 'action' is passed to the handler, and due to the way 'on' is typed,
|
|
139
|
-
// the 'event' parameter within the handler function will be correctly
|
|
140
|
-
// typed to its specific class (e.g., LoadMyFeatureData).
|
|
141
125
|
await handler(action, emit);
|
|
142
126
|
} catch (error) {
|
|
143
|
-
// Enhanced error handling with better context
|
|
144
127
|
const constructorName =
|
|
145
128
|
(action.constructor as { name?: string }).name ||
|
|
146
129
|
'UnnamedConstructor';
|
|
@@ -160,19 +143,30 @@ export abstract class Bloc<
|
|
|
160
143
|
errorContext,
|
|
161
144
|
);
|
|
162
145
|
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
146
|
+
// Notify plugins of the error
|
|
147
|
+
if (error instanceof Error) {
|
|
148
|
+
try {
|
|
149
|
+
this._plugins.notifyError(error, {
|
|
150
|
+
phase: 'event-processing',
|
|
151
|
+
operation: 'handler',
|
|
152
|
+
metadata: { event: action }
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
Blac.instance.plugins.notifyError(error, this as any, {
|
|
156
|
+
phase: 'event-processing',
|
|
157
|
+
operation: 'handler',
|
|
158
|
+
metadata: { event: action }
|
|
159
|
+
});
|
|
160
|
+
} catch (pluginError) {
|
|
161
|
+
console.error('Error notifying plugins:', pluginError);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
168
164
|
|
|
169
|
-
// Optional: Re-throw for critical errors (can be configured)
|
|
170
165
|
if (error instanceof Error && error.name === 'CriticalError') {
|
|
171
166
|
throw error;
|
|
172
167
|
}
|
|
173
168
|
}
|
|
174
169
|
} else {
|
|
175
|
-
// Enhanced warning with more context
|
|
176
170
|
const constructorName =
|
|
177
171
|
(action.constructor as { name?: string }).name || 'UnnamedConstructor';
|
|
178
172
|
Blac.warn(
|
package/src/BlocBase.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { BlacObservable } from './BlacObserver';
|
|
2
2
|
import { generateUUID } from './utils/uuid';
|
|
3
|
+
import { BlocPlugin, ErrorContext } from './plugins/types';
|
|
4
|
+
import { BlocPluginRegistry } from './plugins/BlocPluginRegistry';
|
|
3
5
|
|
|
4
6
|
export type BlocInstanceId = string | number | undefined;
|
|
5
7
|
type DependencySelector<S> = (
|
|
@@ -32,6 +34,7 @@ export interface StateTransitionResult {
|
|
|
32
34
|
interface BlocStaticProperties {
|
|
33
35
|
isolated: boolean;
|
|
34
36
|
keepAlive: boolean;
|
|
37
|
+
plugins?: BlocPlugin<any, any>[];
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
/**
|
|
@@ -158,6 +161,11 @@ export abstract class BlocBase<S, P = unknown> {
|
|
|
158
161
|
*/
|
|
159
162
|
private _consumerRefs = new Map<string, WeakRef<object>>();
|
|
160
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Plugin registry for this bloc instance
|
|
166
|
+
*/
|
|
167
|
+
protected _plugins: BlocPluginRegistry<S, any>;
|
|
168
|
+
|
|
161
169
|
/**
|
|
162
170
|
* @internal
|
|
163
171
|
* Validates that all consumer references are still alive
|
|
@@ -210,6 +218,16 @@ export abstract class BlocBase<S, P = unknown> {
|
|
|
210
218
|
: false;
|
|
211
219
|
this._isolated =
|
|
212
220
|
typeof Constructor.isolated === 'boolean' ? Constructor.isolated : false;
|
|
221
|
+
|
|
222
|
+
// Initialize plugin registry
|
|
223
|
+
this._plugins = new BlocPluginRegistry<S, any>();
|
|
224
|
+
|
|
225
|
+
// Register static plugins
|
|
226
|
+
if (Constructor.plugins && Array.isArray(Constructor.plugins)) {
|
|
227
|
+
for (const plugin of Constructor.plugins) {
|
|
228
|
+
this.addPlugin(plugin);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
213
231
|
}
|
|
214
232
|
|
|
215
233
|
/**
|
|
@@ -529,25 +547,55 @@ export abstract class BlocBase<S, P = unknown> {
|
|
|
529
547
|
return;
|
|
530
548
|
}
|
|
531
549
|
|
|
550
|
+
// Transform state through plugins
|
|
551
|
+
let transformedState: S = newState;
|
|
552
|
+
try {
|
|
553
|
+
const result = this._plugins.transformState(oldState, newState);
|
|
554
|
+
transformedState = result;
|
|
555
|
+
} catch (error) {
|
|
556
|
+
this._plugins.notifyError(error as Error, {
|
|
557
|
+
phase: 'state-change',
|
|
558
|
+
operation: 'transformState'
|
|
559
|
+
});
|
|
560
|
+
// Continue with original state if transformation fails
|
|
561
|
+
}
|
|
562
|
+
|
|
532
563
|
if (this._batchingEnabled) {
|
|
533
564
|
// When batching, just accumulate the updates
|
|
534
|
-
this._pendingUpdates.push({ newState, oldState, action });
|
|
565
|
+
this._pendingUpdates.push({ newState: transformedState, oldState, action });
|
|
535
566
|
|
|
536
567
|
// Update internal state for consistency
|
|
537
568
|
this._oldState = oldState;
|
|
538
|
-
this._state =
|
|
569
|
+
this._state = transformedState;
|
|
539
570
|
return;
|
|
540
571
|
}
|
|
541
572
|
|
|
542
573
|
// Normal state update flow
|
|
543
574
|
this._oldState = oldState;
|
|
544
|
-
this._state =
|
|
575
|
+
this._state = transformedState;
|
|
576
|
+
|
|
577
|
+
// Notify bloc plugins first
|
|
578
|
+
try {
|
|
579
|
+
this._plugins.notifyStateChange(oldState, transformedState);
|
|
580
|
+
} catch (error) {
|
|
581
|
+
console.error('Error notifying bloc plugins of state change:', error);
|
|
582
|
+
}
|
|
545
583
|
|
|
546
584
|
// Notify observers of the state change
|
|
547
|
-
this._observer.notify(
|
|
585
|
+
this._observer.notify(transformedState, oldState, action);
|
|
548
586
|
this.lastUpdate = Date.now();
|
|
549
587
|
};
|
|
550
588
|
|
|
589
|
+
/**
|
|
590
|
+
* Notify observers of a state change
|
|
591
|
+
* @internal Used by plugins for state hydration
|
|
592
|
+
* @param newState The new state
|
|
593
|
+
* @param oldState The old state
|
|
594
|
+
*/
|
|
595
|
+
_notifyObservers(newState: S, oldState: S): void {
|
|
596
|
+
this._observer.notify(newState, oldState);
|
|
597
|
+
}
|
|
598
|
+
|
|
551
599
|
/**
|
|
552
600
|
* Enables batching for multiple state updates
|
|
553
601
|
* @param batchFn Function to execute with batching enabled
|
|
@@ -586,4 +634,59 @@ export abstract class BlocBase<S, P = unknown> {
|
|
|
586
634
|
this._pendingUpdates = [];
|
|
587
635
|
}
|
|
588
636
|
};
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Add a plugin to this bloc instance
|
|
640
|
+
*/
|
|
641
|
+
addPlugin(plugin: BlocPlugin<S, any>): void {
|
|
642
|
+
this._plugins.add(plugin);
|
|
643
|
+
|
|
644
|
+
// Attach if already active
|
|
645
|
+
if (this._disposalState === BlocLifecycleState.ACTIVE) {
|
|
646
|
+
try {
|
|
647
|
+
if (plugin.onAttach) {
|
|
648
|
+
plugin.onAttach(this);
|
|
649
|
+
}
|
|
650
|
+
} catch (error) {
|
|
651
|
+
console.error(`Failed to attach plugin '${plugin.name}':`, error);
|
|
652
|
+
this._plugins.remove(plugin.name);
|
|
653
|
+
throw error;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Remove a plugin from this bloc instance
|
|
660
|
+
*/
|
|
661
|
+
removePlugin(pluginName: string): boolean {
|
|
662
|
+
return this._plugins.remove(pluginName);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Get a plugin by name
|
|
667
|
+
*/
|
|
668
|
+
getPlugin(pluginName: string): BlocPlugin<S, any> | undefined {
|
|
669
|
+
return this._plugins.get(pluginName);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Get all plugins
|
|
674
|
+
*/
|
|
675
|
+
getPlugins(): ReadonlyArray<BlocPlugin<S, any>> {
|
|
676
|
+
return this._plugins.getAll();
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* @internal
|
|
681
|
+
* Activate plugins when bloc becomes active
|
|
682
|
+
*/
|
|
683
|
+
_activatePlugins(): void {
|
|
684
|
+
if (this._disposalState === BlocLifecycleState.ACTIVE) {
|
|
685
|
+
try {
|
|
686
|
+
this._plugins.attach(this);
|
|
687
|
+
} catch (error) {
|
|
688
|
+
console.error(`Failed to activate plugins for ${this._name}:`, error);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
589
692
|
}
|
package/src/Cubit.ts
CHANGED
|
@@ -8,6 +8,20 @@ import { BlocBase } from './BlocBase';
|
|
|
8
8
|
* @template P - The type of parameters (optional, defaults to null)
|
|
9
9
|
*/
|
|
10
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;
|
|
11
25
|
/**
|
|
12
26
|
* Updates the current state and notifies all observers of the change.
|
|
13
27
|
* If the new state is identical to the current state (using Object.is),
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { Blac, BlacConfig } from '../index';
|
|
3
|
+
|
|
4
|
+
describe('Blac.config', () => {
|
|
5
|
+
const originalConfig = { ...Blac.config };
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
// Reset to default config before each test
|
|
9
|
+
Blac.setConfig({
|
|
10
|
+
exposeBlacInstance: false,
|
|
11
|
+
proxyDependencyTracking: true,
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
// Restore original config after tests
|
|
17
|
+
Blac.setConfig(originalConfig);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('setConfig', () => {
|
|
21
|
+
it('should have default configuration', () => {
|
|
22
|
+
expect(Blac.config).toEqual({
|
|
23
|
+
exposeBlacInstance: false,
|
|
24
|
+
proxyDependencyTracking: true,
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should update configuration with partial config', () => {
|
|
29
|
+
Blac.setConfig({ proxyDependencyTracking: false });
|
|
30
|
+
|
|
31
|
+
expect(Blac.config).toEqual({
|
|
32
|
+
exposeBlacInstance: false,
|
|
33
|
+
proxyDependencyTracking: false,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should merge configuration properly', () => {
|
|
38
|
+
Blac.setConfig({ exposeBlacInstance: true });
|
|
39
|
+
Blac.setConfig({ proxyDependencyTracking: false });
|
|
40
|
+
|
|
41
|
+
expect(Blac.config).toEqual({
|
|
42
|
+
exposeBlacInstance: true,
|
|
43
|
+
proxyDependencyTracking: false,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should throw error for invalid proxyDependencyTracking type', () => {
|
|
48
|
+
expect(() => {
|
|
49
|
+
Blac.setConfig({ proxyDependencyTracking: 'true' as any });
|
|
50
|
+
}).toThrow('BlacConfig.proxyDependencyTracking must be a boolean');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should throw error for invalid exposeBlacInstance type', () => {
|
|
54
|
+
expect(() => {
|
|
55
|
+
Blac.setConfig({ exposeBlacInstance: 1 as any });
|
|
56
|
+
}).toThrow('BlacConfig.exposeBlacInstance must be a boolean');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should return a copy of config, not the original', () => {
|
|
60
|
+
const config1 = Blac.config;
|
|
61
|
+
const config2 = Blac.config;
|
|
62
|
+
|
|
63
|
+
expect(config1).not.toBe(config2);
|
|
64
|
+
expect(config1).toEqual(config2);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should not allow direct modification of config', () => {
|
|
68
|
+
const config = Blac.config as any;
|
|
69
|
+
config.proxyDependencyTracking = false;
|
|
70
|
+
|
|
71
|
+
// Original config should remain unchanged
|
|
72
|
+
expect(Blac.config.proxyDependencyTracking).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('config type exports', () => {
|
|
77
|
+
it('should allow typed config usage', () => {
|
|
78
|
+
const testConfig: Partial<BlacConfig> = {
|
|
79
|
+
proxyDependencyTracking: false,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
Blac.setConfig(testConfig);
|
|
83
|
+
expect(Blac.config.proxyDependencyTracking).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|