@push.rocks/smartstate 2.2.1 → 2.3.1
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/dist_bundle/bundle.js +15 -14901
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/smartstate.classes.computed.js +3 -3
- package/dist_ts/smartstate.classes.smartstate.d.ts +4 -0
- package/dist_ts/smartstate.classes.smartstate.js +22 -3
- package/dist_ts/smartstate.classes.stateaction.d.ts +3 -3
- package/dist_ts/smartstate.classes.stateaction.js +1 -1
- package/dist_ts/smartstate.classes.statepart.d.ts +15 -2
- package/dist_ts/smartstate.classes.statepart.js +69 -12
- package/dist_ts/smartstate.classes.stateprocess.d.ts +48 -0
- package/dist_ts/smartstate.classes.stateprocess.js +138 -0
- package/package.json +13 -13
- package/readme.md +209 -17
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +1 -0
- package/ts/smartstate.classes.computed.ts +2 -1
- package/ts/smartstate.classes.smartstate.ts +22 -3
- package/ts/smartstate.classes.stateaction.ts +3 -3
- package/ts/smartstate.classes.statepart.ts +85 -20
- package/ts/smartstate.classes.stateprocess.ts +177 -0
- /package/{npmextra.json → .smartconfig.json} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@push.rocks/smartstate",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A TypeScript-first reactive state management library with middleware, computed state, batching, persistence, and Web Component Context Protocol support.",
|
|
6
6
|
"main": "dist_ts/index.js",
|
|
@@ -10,23 +10,22 @@
|
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"scripts": {
|
|
12
12
|
"test": "(tstest test/ --verbose)",
|
|
13
|
-
"build": "(tsbuild tsfolders
|
|
13
|
+
"build": "(tsbuild tsfolders && tsbundle npm)",
|
|
14
14
|
"buildDocs": "tsdoc"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"@git.zone/tsbuild": "^4.
|
|
18
|
-
"@git.zone/tsbundle": "^2.
|
|
19
|
-
"@git.zone/tsrun": "^2.0.
|
|
20
|
-
"@git.zone/tstest": "^3.
|
|
21
|
-
"@
|
|
22
|
-
"@types/node": "^25.3.2"
|
|
17
|
+
"@git.zone/tsbuild": "^4.4.0",
|
|
18
|
+
"@git.zone/tsbundle": "^2.10.0",
|
|
19
|
+
"@git.zone/tsrun": "^2.0.2",
|
|
20
|
+
"@git.zone/tstest": "^3.6.3",
|
|
21
|
+
"@types/node": "^25.6.0"
|
|
23
22
|
},
|
|
24
23
|
"dependencies": {
|
|
25
|
-
"@push.rocks/smarthash": "^3.2.
|
|
26
|
-
"@push.rocks/smartjson": "^6.0.
|
|
24
|
+
"@push.rocks/smarthash": "^3.2.7",
|
|
25
|
+
"@push.rocks/smartjson": "^6.0.1",
|
|
27
26
|
"@push.rocks/smartpromise": "^4.2.3",
|
|
28
27
|
"@push.rocks/smartrx": "^3.0.10",
|
|
29
|
-
"@push.rocks/webstore": "^2.0.
|
|
28
|
+
"@push.rocks/webstore": "^2.0.21"
|
|
30
29
|
},
|
|
31
30
|
"files": [
|
|
32
31
|
"ts/**/*",
|
|
@@ -37,7 +36,8 @@
|
|
|
37
36
|
"dist_ts_web/**/*",
|
|
38
37
|
"assets/**/*",
|
|
39
38
|
"cli.js",
|
|
40
|
-
"
|
|
39
|
+
".smartconfig.json",
|
|
40
|
+
"license",
|
|
41
41
|
"readme.md"
|
|
42
42
|
],
|
|
43
43
|
"browserslist": [
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
"type": "git",
|
|
67
67
|
"url": "https://code.foss.global/push.rocks/smartstate.git"
|
|
68
68
|
},
|
|
69
|
-
"packageManager": "pnpm@10.
|
|
69
|
+
"packageManager": "pnpm@10.28.2"
|
|
70
70
|
}
|
package/readme.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @push.rocks/smartstate
|
|
2
2
|
|
|
3
|
-
A TypeScript-first reactive state management library with middleware, computed state, batching, persistence, and Web Component Context Protocol support 🚀
|
|
3
|
+
A TypeScript-first reactive state management library with processes, middleware, computed state, batching, persistence, and Web Component Context Protocol support 🚀
|
|
4
4
|
|
|
5
5
|
## Issue Reporting and Security
|
|
6
6
|
|
|
@@ -48,7 +48,7 @@ await userState.setState({ name: 'Alice', loggedIn: true });
|
|
|
48
48
|
|
|
49
49
|
### 🧩 State Parts & Init Modes
|
|
50
50
|
|
|
51
|
-
State parts are isolated, typed units of state
|
|
51
|
+
State parts are isolated, typed units of state — the building blocks of your application's state tree. Create them via `getStatePart()`:
|
|
52
52
|
|
|
53
53
|
```typescript
|
|
54
54
|
const part = await state.getStatePart<IMyState>(name, initialState, initMode);
|
|
@@ -58,10 +58,10 @@ const part = await state.getStatePart<IMyState>(name, initialState, initMode);
|
|
|
58
58
|
|-----------|----------|
|
|
59
59
|
| `'soft'` (default) | Returns existing if found, creates new otherwise |
|
|
60
60
|
| `'mandatory'` | Throws if state part already exists — useful for ensuring single-initialization |
|
|
61
|
-
| `'force'` | Always creates a new state part, overwriting any existing one |
|
|
61
|
+
| `'force'` | Always creates a new state part, disposing and overwriting any existing one |
|
|
62
62
|
| `'persistent'` | Like `'soft'` but automatically persists state to IndexedDB via WebStore |
|
|
63
63
|
|
|
64
|
-
You can use either
|
|
64
|
+
You can use either string literal union types or enums for state part names:
|
|
65
65
|
|
|
66
66
|
```typescript
|
|
67
67
|
// String literal types (simpler)
|
|
@@ -82,12 +82,12 @@ const settings = await state.getStatePart('settings', { theme: 'dark', fontSize:
|
|
|
82
82
|
|
|
83
83
|
// ✅ Automatically saved to IndexedDB on every setState()
|
|
84
84
|
// ✅ On next app load, persisted values override defaults
|
|
85
|
-
// ✅ Persistence writes complete before in-memory updates
|
|
85
|
+
// ✅ Persistence writes complete before in-memory updates
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
### 🔭 Selecting State
|
|
89
89
|
|
|
90
|
-
`select()` returns an RxJS Observable that emits the current value immediately and on every subsequent change:
|
|
90
|
+
`select()` returns an RxJS Observable that emits the current value immediately (via `BehaviorSubject`) and on every subsequent change:
|
|
91
91
|
|
|
92
92
|
```typescript
|
|
93
93
|
// Full state
|
|
@@ -99,6 +99,8 @@ userState.select((s) => s.name).subscribe((name) => console.log(name));
|
|
|
99
99
|
|
|
100
100
|
Selectors are **memoized** — calling `select(fn)` with the same function reference returns the same cached Observable, shared across all subscribers via `shareReplay`. This means you can call `select(mySelector)` in multiple places without creating duplicate subscriptions.
|
|
101
101
|
|
|
102
|
+
**Change detection** is built in: `select()` uses `distinctUntilChanged` with deep JSON comparison, so subscribers only fire when the selected value actually changes. Selecting `s => s.name` won't re-emit when only `s.count` changes.
|
|
103
|
+
|
|
102
104
|
#### ✂️ AbortSignal Support
|
|
103
105
|
|
|
104
106
|
Clean up subscriptions without manual `.unsubscribe()` — the modern way:
|
|
@@ -125,7 +127,6 @@ interface ILoginPayload {
|
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
const loginAction = userState.createAction<ILoginPayload>(async (statePart, payload) => {
|
|
128
|
-
// You have access to the current state via statePart.getState()
|
|
129
130
|
const current = statePart.getState();
|
|
130
131
|
return { ...current, name: payload.username, loggedIn: true };
|
|
131
132
|
});
|
|
@@ -136,7 +137,157 @@ await loginAction.trigger({ username: 'Alice', email: 'alice@example.com' });
|
|
|
136
137
|
await userState.dispatchAction(loginAction, { username: 'Alice', email: 'alice@example.com' });
|
|
137
138
|
```
|
|
138
139
|
|
|
139
|
-
Both `trigger()` and `dispatchAction()` return a Promise with the new state.
|
|
140
|
+
Both `trigger()` and `dispatchAction()` return a Promise with the new state. All dispatches are serialized through a mutation queue, so concurrent dispatches never cause lost updates.
|
|
141
|
+
|
|
142
|
+
#### 🔗 Nested Actions (Action Context)
|
|
143
|
+
|
|
144
|
+
When you need to dispatch sub-actions from within an action, use the `context` parameter. This is critical because calling `dispatchAction()` directly from inside an action would deadlock (it tries to acquire the mutation queue that's already held). The context's `dispatch()` bypasses the queue and executes inline:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
const incrementAction = userState.createAction<number>(async (statePart, amount) => {
|
|
148
|
+
const current = statePart.getState();
|
|
149
|
+
return { ...current, count: current.count + amount };
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const doubleIncrementAction = userState.createAction<number>(async (statePart, amount, context) => {
|
|
153
|
+
// ✅ Safe: uses context.dispatch() which bypasses the mutation queue
|
|
154
|
+
await context.dispatch(incrementAction, amount);
|
|
155
|
+
const current = statePart.getState();
|
|
156
|
+
return { ...current, count: current.count + amount };
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// ❌ DON'T do this inside an action — it will deadlock:
|
|
160
|
+
// await statePart.dispatchAction(someAction, payload);
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
A built-in depth limit (10 levels) prevents infinite circular dispatch chains, throwing a clear error if exceeded.
|
|
164
|
+
|
|
165
|
+
### 🔄 Processes (Polling, Streams & Scheduled Tasks)
|
|
166
|
+
|
|
167
|
+
Processes are managed, pausable observable-to-state bridges — the "side effects" layer. They tie an ongoing data source (polling, WebSockets, event streams) to state updates with full lifecycle control and optional auto-pause.
|
|
168
|
+
|
|
169
|
+
#### Basic Process: Polling an API
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
import { interval, switchMap, from } from 'rxjs';
|
|
173
|
+
|
|
174
|
+
const metricsPoller = dashboard.createProcess<{ cpu: number; memory: number }>({
|
|
175
|
+
// Producer: an Observable factory — called on start and each resume
|
|
176
|
+
producer: () => interval(5000).pipe(
|
|
177
|
+
switchMap(() => from(fetch('/api/metrics').then(r => r.json()))),
|
|
178
|
+
),
|
|
179
|
+
// Reducer: folds each produced value into state (runs through middleware & validation)
|
|
180
|
+
reducer: (currentState, metrics) => ({
|
|
181
|
+
...currentState,
|
|
182
|
+
metrics,
|
|
183
|
+
lastUpdated: Date.now(),
|
|
184
|
+
}),
|
|
185
|
+
autoPause: 'visibility', // ⏸️ Stop polling when the tab is hidden
|
|
186
|
+
autoStart: true, // ▶️ Start immediately
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Full lifecycle control
|
|
190
|
+
metricsPoller.pause(); // Unsubscribes from producer
|
|
191
|
+
metricsPoller.resume(); // Re-subscribes (fresh subscription)
|
|
192
|
+
metricsPoller.dispose(); // Permanent cleanup
|
|
193
|
+
|
|
194
|
+
// Observe status reactively
|
|
195
|
+
metricsPoller.status; // 'idle' | 'running' | 'paused' | 'disposed'
|
|
196
|
+
metricsPoller.status$.subscribe(s => console.log('Process:', s));
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
#### Scheduled Actions
|
|
200
|
+
|
|
201
|
+
Dispatch an existing action on a recurring interval — syntactic sugar over `createProcess`:
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
const refreshAction = dashboard.createAction<void>(async (sp) => {
|
|
205
|
+
const data = await fetch('/api/dashboard').then(r => r.json());
|
|
206
|
+
return { ...sp.getState()!, ...data, lastUpdated: Date.now() };
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Dispatches refreshAction every 30 seconds, auto-pauses when tab is hidden
|
|
210
|
+
const scheduled = dashboard.createScheduledAction({
|
|
211
|
+
action: refreshAction,
|
|
212
|
+
payload: undefined,
|
|
213
|
+
intervalMs: 30000,
|
|
214
|
+
autoPause: 'visibility',
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// It's a full StateProcess — pause, resume, dispose all work
|
|
218
|
+
scheduled.dispose();
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
#### Custom Auto-Pause Signals
|
|
222
|
+
|
|
223
|
+
Pass any `Observable<boolean>` as the auto-pause signal — `true` means active, `false` means pause:
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
import { fromEvent, map, startWith } from 'rxjs';
|
|
227
|
+
|
|
228
|
+
// Pause when offline, resume when online
|
|
229
|
+
const onlineSignal = fromEvent(window, 'online').pipe(
|
|
230
|
+
startWith(null),
|
|
231
|
+
map(() => navigator.onLine),
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const syncProcess = userPart.createProcess<SyncPayload>({
|
|
235
|
+
producer: () => interval(10000).pipe(
|
|
236
|
+
switchMap(() => from(syncWithServer())),
|
|
237
|
+
),
|
|
238
|
+
reducer: (state, result) => ({ ...state, ...result }),
|
|
239
|
+
autoPause: onlineSignal,
|
|
240
|
+
});
|
|
241
|
+
syncProcess.start();
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
#### WebSocket / Live Streams
|
|
245
|
+
|
|
246
|
+
Pause disconnects; resume creates a fresh connection:
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
const liveProcess = tickerPart.createProcess<TradeEvent>({
|
|
250
|
+
producer: () => new Observable<TradeEvent>(subscriber => {
|
|
251
|
+
const ws = new WebSocket('wss://trades.example.com');
|
|
252
|
+
ws.onmessage = (e) => subscriber.next(JSON.parse(e.data));
|
|
253
|
+
ws.onerror = (e) => subscriber.error(e);
|
|
254
|
+
ws.onclose = () => subscriber.complete();
|
|
255
|
+
return () => ws.close(); // Teardown: close WebSocket on unsubscribe
|
|
256
|
+
}),
|
|
257
|
+
reducer: (state, trade) => ({
|
|
258
|
+
...state,
|
|
259
|
+
lastPrice: trade.price,
|
|
260
|
+
trades: [...state.trades.slice(-99), trade],
|
|
261
|
+
}),
|
|
262
|
+
autoPause: 'visibility',
|
|
263
|
+
});
|
|
264
|
+
liveProcess.start();
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
#### Error Recovery
|
|
268
|
+
|
|
269
|
+
If a producer errors, the process gracefully transitions to `'paused'` instead of dying. Call `resume()` to retry with a fresh subscription:
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
process.start();
|
|
273
|
+
// Producer errors → status becomes 'paused'
|
|
274
|
+
process.resume(); // Creates a fresh subscription — retry
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
#### Process Cleanup Cascades
|
|
278
|
+
|
|
279
|
+
Disposing a `StatePart` or `Smartstate` instance automatically disposes all attached processes:
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
const p1 = part.createProcess({ ... });
|
|
283
|
+
const p2 = part.createProcess({ ... });
|
|
284
|
+
p1.start();
|
|
285
|
+
p2.start();
|
|
286
|
+
|
|
287
|
+
part.dispose();
|
|
288
|
+
console.log(p1.status); // 'disposed'
|
|
289
|
+
console.log(p2.status); // 'disposed'
|
|
290
|
+
```
|
|
140
291
|
|
|
141
292
|
### 🛡️ Middleware
|
|
142
293
|
|
|
@@ -171,7 +322,7 @@ const remove = userState.addMiddleware(myMiddleware);
|
|
|
171
322
|
remove(); // middleware no longer runs
|
|
172
323
|
```
|
|
173
324
|
|
|
174
|
-
Middleware runs **sequentially** in insertion order. If any middleware throws, the state remains unchanged — the operation is **atomic**.
|
|
325
|
+
Middleware runs **sequentially** in insertion order. If any middleware throws, the state remains unchanged — the operation is **atomic**. Process-driven state updates go through middleware too.
|
|
175
326
|
|
|
176
327
|
### 🧮 Computed / Derived State
|
|
177
328
|
|
|
@@ -199,7 +350,7 @@ const greeting2$ = state.computed(
|
|
|
199
350
|
);
|
|
200
351
|
```
|
|
201
352
|
|
|
202
|
-
Computed observables are **lazy** — they only subscribe to their sources when someone subscribes to them, and they automatically unsubscribe when all subscribers disconnect.
|
|
353
|
+
Computed observables are **lazy** — they only subscribe to their sources when someone subscribes to them, and they automatically unsubscribe when all subscribers disconnect. They also use `distinctUntilChanged` to avoid redundant emissions when the derived value hasn't actually changed.
|
|
203
354
|
|
|
204
355
|
### 📦 Batch Updates
|
|
205
356
|
|
|
@@ -322,15 +473,31 @@ await userState.stateSetup(async (statePart) => {
|
|
|
322
473
|
// Any dispatchAction() calls will automatically wait for stateSetup() to finish
|
|
323
474
|
```
|
|
324
475
|
|
|
476
|
+
### 🧹 Disposal & Cleanup
|
|
477
|
+
|
|
478
|
+
Both `Smartstate` and individual `StatePart` instances support disposal for proper cleanup:
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
// Dispose a single state part — completes the BehaviorSubject, clears middleware, caches,
|
|
482
|
+
// and disposes all attached processes
|
|
483
|
+
userState.dispose();
|
|
484
|
+
|
|
485
|
+
// Dispose the entire Smartstate instance — disposes all state parts and clears internal maps
|
|
486
|
+
state.dispose();
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
After disposal, `setState()` and `dispatchAction()` will throw if called on a disposed `StatePart`. Calling `start()`, `pause()`, or `resume()` on a disposed `StateProcess` also throws.
|
|
490
|
+
|
|
325
491
|
### 🏎️ Performance
|
|
326
492
|
|
|
327
493
|
Smartstate is built with performance in mind:
|
|
328
494
|
|
|
329
495
|
- **🔒 SHA256 Change Detection** — Uses content hashing to detect actual changes. Identical state values don't trigger notifications, even with different object references.
|
|
496
|
+
- **🎯 distinctUntilChanged on Selectors** — Sub-selectors only fire when the selected slice actually changes. `select(s => s.name)` won't emit when `s.count` changes.
|
|
330
497
|
- **♻️ Selector Memoization** — `select(fn)` caches observables by function reference and shares them via `shareReplay({ refCount: true })`. Multiple subscribers share one upstream subscription.
|
|
331
498
|
- **📦 Cumulative Notifications** — `notifyChangeCumulative()` debounces rapid changes into a single notification at the end of the call stack.
|
|
332
|
-
- **🔐 Concurrent Safety** — Simultaneous `getStatePart()` calls for the same name return the same promise, preventing duplicate creation
|
|
333
|
-
- **💾 Atomic Persistence** — WebStore writes complete before in-memory state updates, ensuring consistency
|
|
499
|
+
- **🔐 Concurrent Safety** — Simultaneous `getStatePart()` calls for the same name return the same promise, preventing duplicate creation. All `setState()` and `dispatchAction()` calls are serialized through a mutation queue. Process values are serialized through their own internal queue.
|
|
500
|
+
- **💾 Atomic Persistence** — WebStore writes complete before in-memory state updates, ensuring consistency.
|
|
334
501
|
- **⏸️ Batch Deferred Notifications** — `batch()` suppresses all subscriber notifications until every update in the batch completes.
|
|
335
502
|
|
|
336
503
|
## API Reference
|
|
@@ -342,23 +509,26 @@ Smartstate is built with performance in mind:
|
|
|
342
509
|
| `getStatePart(name, initial?, initMode?)` | Get or create a typed state part |
|
|
343
510
|
| `batch(fn)` | Batch state updates, defer all notifications until complete |
|
|
344
511
|
| `computed(sources, fn)` | Create a computed observable from multiple state parts |
|
|
512
|
+
| `dispose()` | Dispose all state parts and clear internal state |
|
|
345
513
|
| `isBatching` | `boolean` — whether a batch is currently active |
|
|
346
|
-
| `statePartMap` | Registry of all created state parts |
|
|
347
514
|
|
|
348
515
|
### `StatePart<TName, TPayload>`
|
|
349
516
|
|
|
350
517
|
| Method | Description |
|
|
351
518
|
|--------|-------------|
|
|
352
|
-
| `getState()` | Get current state (
|
|
519
|
+
| `getState()` | Get current state synchronously (`TPayload \| undefined`) |
|
|
353
520
|
| `setState(newState)` | Set state — runs middleware → validates → persists → notifies |
|
|
354
|
-
| `select(selectorFn?, options?)` |
|
|
521
|
+
| `select(selectorFn?, options?)` | Observable of state or derived values. Options: `{ signal?: AbortSignal }` |
|
|
355
522
|
| `createAction(actionDef)` | Create a reusable, typed state action |
|
|
356
523
|
| `dispatchAction(action, payload)` | Dispatch an action and return the new state |
|
|
357
524
|
| `addMiddleware(fn)` | Add a middleware interceptor. Returns a removal function |
|
|
358
525
|
| `waitUntilPresent(selectorFn?, opts?)` | Wait for a state condition. Opts: `number` (timeout) or `{ timeoutMs?, signal? }` |
|
|
526
|
+
| `createProcess(options)` | Create a managed, pausable process tied to this state part |
|
|
527
|
+
| `createScheduledAction(options)` | Create a process that dispatches an action on a recurring interval |
|
|
359
528
|
| `notifyChange()` | Manually trigger a change notification (with hash dedup) |
|
|
360
529
|
| `notifyChangeCumulative()` | Debounced notification — fires at end of call stack |
|
|
361
530
|
| `stateSetup(fn)` | Async state initialization with action serialization |
|
|
531
|
+
| `dispose()` | Complete the BehaviorSubject, dispose processes, clear middleware and caches |
|
|
362
532
|
|
|
363
533
|
### `StateAction<TState, TPayload>`
|
|
364
534
|
|
|
@@ -366,6 +536,23 @@ Smartstate is built with performance in mind:
|
|
|
366
536
|
|--------|-------------|
|
|
367
537
|
| `trigger(payload)` | Dispatch the action on its associated state part |
|
|
368
538
|
|
|
539
|
+
### `StateProcess<TName, TPayload, TProducerValue>`
|
|
540
|
+
|
|
541
|
+
| Method / Property | Description |
|
|
542
|
+
|-------------------|-------------|
|
|
543
|
+
| `start()` | Start the process (subscribes to producer, sets up auto-pause) |
|
|
544
|
+
| `pause()` | Pause the process (unsubscribes from producer) |
|
|
545
|
+
| `resume()` | Resume a paused process (fresh subscription to producer) |
|
|
546
|
+
| `dispose()` | Permanently stop the process and clean up |
|
|
547
|
+
| `status` | Current status: `'idle' \| 'running' \| 'paused' \| 'disposed'` |
|
|
548
|
+
| `status$` | Observable of status transitions |
|
|
549
|
+
|
|
550
|
+
### `IActionContext<TState>`
|
|
551
|
+
|
|
552
|
+
| Method | Description |
|
|
553
|
+
|--------|-------------|
|
|
554
|
+
| `dispatch(action, payload)` | Dispatch a sub-action inline (bypasses mutation queue). Available as the third argument to action definitions |
|
|
555
|
+
|
|
369
556
|
### Standalone Functions
|
|
370
557
|
|
|
371
558
|
| Function | Description |
|
|
@@ -379,8 +566,13 @@ Smartstate is built with performance in mind:
|
|
|
379
566
|
|------|-------------|
|
|
380
567
|
| `TInitMode` | `'soft' \| 'mandatory' \| 'force' \| 'persistent'` |
|
|
381
568
|
| `TMiddleware<TPayload>` | `(newState, oldState) => TPayload \| Promise<TPayload>` |
|
|
382
|
-
| `IActionDef<TState, TPayload>` | Action definition function signature |
|
|
569
|
+
| `IActionDef<TState, TPayload>` | Action definition function signature (receives statePart, payload, context?) |
|
|
570
|
+
| `IActionContext<TState>` | Context for safe nested dispatch within actions |
|
|
383
571
|
| `IContextProviderOptions<TPayload>` | Options for `attachContextProvider` |
|
|
572
|
+
| `IProcessOptions<TPayload, TValue>` | Options for `createProcess` (producer, reducer, autoPause, autoStart) |
|
|
573
|
+
| `IScheduledActionOptions<TPayload, TActionPayload>` | Options for `createScheduledAction` (action, payload, intervalMs, autoPause) |
|
|
574
|
+
| `TProcessStatus` | `'idle' \| 'running' \| 'paused' \| 'disposed'` |
|
|
575
|
+
| `TAutoPause` | `'visibility' \| Observable<boolean> \| false` |
|
|
384
576
|
|
|
385
577
|
## License and Legal Information
|
|
386
578
|
|
|
@@ -396,7 +588,7 @@ Use of these trademarks must comply with Task Venture Capital GmbH's Trademark G
|
|
|
396
588
|
|
|
397
589
|
### Company Information
|
|
398
590
|
|
|
399
|
-
Task Venture Capital GmbH
|
|
591
|
+
Task Venture Capital GmbH
|
|
400
592
|
Registered at District Court Bremen HRB 35230 HB, Germany
|
|
401
593
|
|
|
402
594
|
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@push.rocks/smartstate',
|
|
6
|
-
version: '2.
|
|
6
|
+
version: '2.3.1',
|
|
7
7
|
description: 'A TypeScript-first reactive state management library with middleware, computed state, batching, persistence, and Web Component Context Protocol support.'
|
|
8
8
|
}
|
package/ts/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as plugins from './smartstate.plugins.js';
|
|
2
|
-
import { combineLatest, map } from 'rxjs';
|
|
2
|
+
import { combineLatest, map, distinctUntilChanged } from 'rxjs';
|
|
3
3
|
import type { StatePart } from './smartstate.classes.statepart.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -12,5 +12,6 @@ export function computed<TResult>(
|
|
|
12
12
|
): plugins.smartrx.rxjs.Observable<TResult> {
|
|
13
13
|
return combineLatest(sources.map((sp) => sp.select())).pipe(
|
|
14
14
|
map((states) => computeFn(...states)),
|
|
15
|
+
distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)),
|
|
15
16
|
) as plugins.smartrx.rxjs.Observable<TResult>;
|
|
16
17
|
}
|
|
@@ -49,7 +49,11 @@ export class Smartstate<StatePartNameType extends string> {
|
|
|
49
49
|
const pending = [...this.pendingNotifications];
|
|
50
50
|
this.pendingNotifications.clear();
|
|
51
51
|
for (const sp of pending) {
|
|
52
|
-
|
|
52
|
+
try {
|
|
53
|
+
await sp.notifyChange();
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error(`Error flushing notification for state part:`, err);
|
|
56
|
+
}
|
|
53
57
|
}
|
|
54
58
|
}
|
|
55
59
|
} finally {
|
|
@@ -69,6 +73,21 @@ export class Smartstate<StatePartNameType extends string> {
|
|
|
69
73
|
return computed(sources, computeFn);
|
|
70
74
|
}
|
|
71
75
|
|
|
76
|
+
/**
|
|
77
|
+
* disposes all state parts and clears internal state
|
|
78
|
+
*/
|
|
79
|
+
public dispose(): void {
|
|
80
|
+
for (const key of Object.keys(this.statePartMap)) {
|
|
81
|
+
const part = this.statePartMap[key as StatePartNameType];
|
|
82
|
+
if (part) {
|
|
83
|
+
part.dispose();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
this.statePartMap = {} as any;
|
|
87
|
+
this.pendingStatePartCreation.clear();
|
|
88
|
+
this.pendingNotifications.clear();
|
|
89
|
+
}
|
|
90
|
+
|
|
72
91
|
/**
|
|
73
92
|
* Allows getting and initializing a new statepart
|
|
74
93
|
*/
|
|
@@ -107,7 +126,7 @@ export class Smartstate<StatePartNameType extends string> {
|
|
|
107
126
|
}
|
|
108
127
|
}
|
|
109
128
|
|
|
110
|
-
const creationPromise = this.createStatePart<PayloadType>(statePartNameArg, initialArg
|
|
129
|
+
const creationPromise = this.createStatePart<PayloadType>(statePartNameArg, initialArg!, initMode);
|
|
111
130
|
this.pendingStatePartCreation.set(statePartNameArg, creationPromise);
|
|
112
131
|
|
|
113
132
|
try {
|
|
@@ -133,7 +152,7 @@ export class Smartstate<StatePartNameType extends string> {
|
|
|
133
152
|
dbName: 'smartstate',
|
|
134
153
|
storeName: statePartName,
|
|
135
154
|
}
|
|
136
|
-
:
|
|
155
|
+
: undefined
|
|
137
156
|
);
|
|
138
157
|
newState.smartstateRef = this;
|
|
139
158
|
await newState.init();
|
|
@@ -10,7 +10,7 @@ export interface IActionContext<TStateType> {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export interface IActionDef<TStateType, TActionPayloadType> {
|
|
13
|
-
(stateArg: StatePart<any, TStateType>, actionPayload: TActionPayloadType, context
|
|
13
|
+
(stateArg: StatePart<any, TStateType>, actionPayload: TActionPayloadType, context: IActionContext<TStateType>): Promise<TStateType>;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -18,8 +18,8 @@ export interface IActionDef<TStateType, TActionPayloadType> {
|
|
|
18
18
|
*/
|
|
19
19
|
export class StateAction<TStateType, TActionPayloadType> {
|
|
20
20
|
constructor(
|
|
21
|
-
public statePartRef: StatePart<any, any>,
|
|
22
|
-
public actionDef: IActionDef<TStateType, TActionPayloadType>
|
|
21
|
+
public readonly statePartRef: StatePart<any, any>,
|
|
22
|
+
public readonly actionDef: IActionDef<TStateType, TActionPayloadType>
|
|
23
23
|
) {}
|
|
24
24
|
|
|
25
25
|
public trigger(payload: TActionPayloadType): Promise<TStateType> {
|