feat(smartstate): Add middleware, computed, batching, selector memoization, AbortSignal support, and Web Component Context Protocol provider
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import * as plugins from './smartstate.plugins.js';
|
||||
import { StatePart } from './smartstate.classes.statepart.js';
|
||||
import { computed } from './smartstate.classes.computed.js';
|
||||
|
||||
export type TInitMode = 'soft' | 'mandatory' | 'force' | 'persistent';
|
||||
|
||||
@@ -11,17 +12,57 @@ export class Smartstate<StatePartNameType extends string> {
|
||||
|
||||
private pendingStatePartCreation: Map<string, Promise<StatePart<StatePartNameType, any>>> = new Map();
|
||||
|
||||
// Batch support
|
||||
private batchDepth = 0;
|
||||
private pendingNotifications = new Set<StatePart<any, any>>();
|
||||
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* whether state changes are currently being batched
|
||||
*/
|
||||
public get isBatching(): boolean {
|
||||
return this.batchDepth > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* registers a state part for deferred notification during a batch
|
||||
*/
|
||||
public registerPendingNotification(statePart: StatePart<any, any>): void {
|
||||
this.pendingNotifications.add(statePart);
|
||||
}
|
||||
|
||||
/**
|
||||
* batches multiple state updates so subscribers are only notified once all updates complete
|
||||
*/
|
||||
public async batch(updateFn: () => Promise<void> | void): Promise<void> {
|
||||
this.batchDepth++;
|
||||
try {
|
||||
await updateFn();
|
||||
} finally {
|
||||
this.batchDepth--;
|
||||
if (this.batchDepth === 0) {
|
||||
const pending = [...this.pendingNotifications];
|
||||
this.pendingNotifications.clear();
|
||||
for (const sp of pending) {
|
||||
await sp.notifyChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* creates a computed observable derived from multiple state parts
|
||||
*/
|
||||
public computed<TResult>(
|
||||
sources: StatePart<StatePartNameType, any>[],
|
||||
computeFn: (...states: any[]) => TResult,
|
||||
): plugins.smartrx.rxjs.Observable<TResult> {
|
||||
return computed(sources, computeFn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows getting and initializing a new statepart
|
||||
* initMode === 'soft' (default) - returns existing statepart if exists, creates new if not
|
||||
* initMode === 'mandatory' - requires statepart to not exist, fails if it does
|
||||
* initMode === 'force' - always creates new statepart, overwriting any existing
|
||||
* initMode === 'persistent' - like 'soft' but with webstore persistence
|
||||
* @param statePartNameArg
|
||||
* @param initialArg
|
||||
* @param initMode
|
||||
*/
|
||||
public async getStatePart<PayloadType>(
|
||||
statePartNameArg: StatePartNameType,
|
||||
@@ -43,16 +84,13 @@ export class Smartstate<StatePartNameType extends string> {
|
||||
`State part '${statePartNameArg}' already exists, but initMode is 'mandatory'`
|
||||
);
|
||||
case 'force':
|
||||
// Force mode: create new state part
|
||||
break; // Fall through to creation
|
||||
break;
|
||||
case 'soft':
|
||||
case 'persistent':
|
||||
default:
|
||||
// Return existing state part
|
||||
return existingStatePart as StatePart<StatePartNameType, PayloadType>;
|
||||
}
|
||||
} else {
|
||||
// State part doesn't exist
|
||||
if (!initialArg) {
|
||||
throw new Error(
|
||||
`State part '${statePartNameArg}' does not exist and no initial state provided`
|
||||
@@ -73,9 +111,6 @@ export class Smartstate<StatePartNameType extends string> {
|
||||
|
||||
/**
|
||||
* Creates a statepart
|
||||
* @param statePartName
|
||||
* @param initialPayloadArg
|
||||
* @param initMode
|
||||
*/
|
||||
private async createStatePart<PayloadType>(
|
||||
statePartName: StatePartNameType,
|
||||
@@ -91,21 +126,20 @@ export class Smartstate<StatePartNameType extends string> {
|
||||
}
|
||||
: null
|
||||
);
|
||||
newState.smartstateRef = this;
|
||||
await newState.init();
|
||||
const currentState = newState.getState();
|
||||
|
||||
if (initMode === 'persistent' && currentState !== undefined) {
|
||||
// Persisted state exists - merge with defaults, persisted values take precedence
|
||||
await newState.setState({
|
||||
...initialPayloadArg,
|
||||
...currentState,
|
||||
});
|
||||
} else {
|
||||
// No persisted state or non-persistent mode
|
||||
await newState.setState(initialPayloadArg);
|
||||
}
|
||||
|
||||
this.statePartMap[statePartName] = newState;
|
||||
return newState;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user