feat(smartstate): Add middleware, computed, batching, selector memoization, AbortSignal support, and Web Component Context Protocol provider

This commit is contained in:
2026-02-27 11:40:07 +00:00
parent 39aa63bdb3
commit 575477df09
13 changed files with 1091 additions and 344 deletions

View File

@@ -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