feat(smartstate): Add middleware, computed, batching, selector memoization, AbortSignal support, and Web Component Context Protocol provider
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Smartstate Implementation Notes
|
||||
|
||||
## Current API (as of v2.0.28+)
|
||||
## Current API (as of v2.0.31)
|
||||
|
||||
### State Part Initialization
|
||||
- State parts can be created with different init modes: 'soft' (default), 'mandatory', 'force', 'persistent'
|
||||
@@ -8,53 +8,70 @@
|
||||
- 'mandatory' - requires state part to not exist, fails if it does
|
||||
- 'force' - always creates new state part, overwriting any existing
|
||||
- 'persistent' - like 'soft' but with WebStore persistence (IndexedDB)
|
||||
- Persistent mode automatically calls init() internally - no need to call it manually
|
||||
- Persistent mode automatically calls init() internally
|
||||
- State merge order fixed: initial state takes precedence over stored state
|
||||
|
||||
### Actions
|
||||
- Actions are created with `createAction()` method
|
||||
- Two ways to dispatch actions:
|
||||
1. `stateAction.trigger(payload)` - returns Promise<TStatePayload>
|
||||
2. `await statePart.dispatchAction(stateAction, payload)` - returns Promise<TStatePayload>
|
||||
- Both methods return the same Promise, providing flexibility in usage
|
||||
- Two ways to dispatch: `stateAction.trigger(payload)` or `statePart.dispatchAction(stateAction, payload)`
|
||||
- Both return Promise<TStatePayload>
|
||||
|
||||
### State Management Methods
|
||||
- `select()` - returns Observable with startWith current state, filters undefined states
|
||||
- `waitUntilPresent()` - waits for specific state condition
|
||||
- `select(fn?, { signal? })` - returns Observable, memoized by selector fn ref, supports AbortSignal
|
||||
- `waitUntilPresent(fn?, number | { timeoutMs?, signal? })` - waits for state condition, backward compat with number arg
|
||||
- `stateSetup()` - async state initialization with cumulative defer
|
||||
- `notifyChangeCumulative()` - defers notification to end of call stack
|
||||
- `getState()` - returns current state or undefined
|
||||
- `setState()` - validates state before setting, notifies only on actual changes
|
||||
- `setState()` - runs middleware, validates, persists, notifies
|
||||
- `addMiddleware(fn)` - intercepts setState, returns removal function
|
||||
|
||||
### Middleware
|
||||
- Type: `(newState, oldState) => newState | Promise<newState>`
|
||||
- Runs sequentially in insertion order before validation/persistence
|
||||
- Throw to reject state changes (atomic — state unchanged on error)
|
||||
- Does NOT run during initial createStatePart() hydration
|
||||
|
||||
### Selector Memoization
|
||||
- Uses WeakMap<Function, Observable> for fn-keyed cache
|
||||
- `defaultSelectObservable` for no-arg select()
|
||||
- Wrapped in `shareReplay({ bufferSize: 1, refCount: true })`
|
||||
- NOT cached when AbortSignal is provided
|
||||
|
||||
### Batch Updates
|
||||
- `smartstate.batch(async () => {...})` — defers notifications until batch completes
|
||||
- Supports nesting — only flushes at outermost level
|
||||
- StatePart has `smartstateRef` set by `createStatePart()` for batch awareness
|
||||
- State parts created via `new StatePart()` directly work without batching
|
||||
|
||||
### Computed State
|
||||
- `computed(sources, fn)` — standalone function using `combineLatest` + `map`
|
||||
- Also available as `smartstate.computed(sources, fn)`
|
||||
- Lazy — only subscribes when subscribed to
|
||||
|
||||
### Context Protocol Bridge
|
||||
- `attachContextProvider(element, { context, statePart, selectorFn? })` — returns cleanup fn
|
||||
- Listens for `context-request` CustomEvent on element
|
||||
- Supports one-shot and subscription modes
|
||||
- Works with Lit @consume(), FAST, or any Context Protocol consumer
|
||||
|
||||
### State Hash Detection
|
||||
- Uses SHA256 hash to detect actual state changes
|
||||
- Fixed: Hash comparison now properly awaits async hash calculation
|
||||
- Hash comparison properly awaits async hash calculation
|
||||
- Prevents duplicate notifications for identical state values
|
||||
- `notifyChange()` is now async to support proper hash comparison
|
||||
|
||||
### State Validation
|
||||
- Basic validation ensures state is not null/undefined
|
||||
- `validateState()` method can be overridden in subclasses for custom validation
|
||||
- Validation runs on both setState() and when loading from persistent storage
|
||||
- `validateState()` can be overridden in subclasses
|
||||
|
||||
### Type System
|
||||
- Can use either enums or string literal types for state part names
|
||||
- Test uses simple string types: `type TMyStateParts = 'testStatePart'`
|
||||
- State can be undefined initially, handled properly in select() and other methods
|
||||
### Key Notes
|
||||
- `smartstateRef` creates circular ref between StatePart and Smartstate
|
||||
- Use `===` not deep equality for StatePart comparison in tests
|
||||
- Direct rxjs imports used for: Observable, shareReplay, takeUntil, combineLatest, map
|
||||
|
||||
## Recent Fixes (v2.0.24+)
|
||||
1. Fixed state hash bug - now properly compares hash values instead of promises
|
||||
2. Fixed state initialization merge order - initial state now takes precedence
|
||||
3. Ensured stateStore is properly typed as potentially undefined
|
||||
4. Simplified init mode logic with clear behavior for each mode
|
||||
5. Added state validation with extensible validateState() method
|
||||
6. Made notifyChange() async to support proper hash comparison
|
||||
7. Updated select() to filter undefined states
|
||||
|
||||
## Dependency Versions (v2.0.30)
|
||||
## Dependency Versions (v2.0.31)
|
||||
- @git.zone/tsbuild: ^4.1.2
|
||||
- @git.zone/tsbundle: ^2.9.0
|
||||
- @git.zone/tsrun: ^2.0.1
|
||||
- @git.zone/tstest: ^3.1.8
|
||||
- @push.rocks/smartjson: ^6.0.0
|
||||
- @types/node: ^25.3.2
|
||||
- @types/node: ^25.3.2
|
||||
|
||||
Reference in New Issue
Block a user