@push.rocks/smartstate 2.0.31 → 2.1.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_ts/00_commitinfo_data.js +3 -3
- package/dist_ts/index.d.ts +2 -0
- package/dist_ts/index.js +3 -1
- package/dist_ts/smartstate.classes.computed.d.ts +7 -0
- package/dist_ts/smartstate.classes.computed.js +10 -0
- package/dist_ts/smartstate.classes.smartstate.d.ts +20 -10
- package/dist_ts/smartstate.classes.smartstate.js +55 -18
- package/dist_ts/smartstate.classes.stateaction.js +1 -2
- package/dist_ts/smartstate.classes.statepart.d.ts +35 -10
- package/dist_ts/smartstate.classes.statepart.js +166 -40
- package/dist_ts/smartstate.contextprovider.d.ts +16 -0
- package/dist_ts/smartstate.contextprovider.js +44 -0
- package/npmextra.json +8 -2
- package/package.json +9 -4
- package/readme.hints.md +47 -30
- package/readme.md +275 -202
- package/ts/00_commitinfo_data.ts +2 -2
- package/ts/index.ts +2 -0
- package/ts/smartstate.classes.computed.ts +16 -0
- package/ts/smartstate.classes.smartstate.ts +61 -18
- package/ts/smartstate.classes.stateaction.ts +0 -1
- package/ts/smartstate.classes.statepart.ts +196 -41
- package/ts/smartstate.contextprovider.ts +61 -0
package/readme.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @push.rocks/smartstate
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A TypeScript-first reactive state management library with middleware, computed state, batching, persistence, and Web Component Context Protocol support 🚀
|
|
4
4
|
|
|
5
5
|
## Issue Reporting and Security
|
|
6
6
|
|
|
@@ -8,310 +8,383 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
11
|
-
To install `@push.rocks/smartstate`, you can use pnpm, npm, or yarn:
|
|
12
|
-
|
|
13
11
|
```bash
|
|
14
|
-
# Using pnpm (recommended)
|
|
15
12
|
pnpm install @push.rocks/smartstate --save
|
|
13
|
+
```
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
npm install @push.rocks/smartstate --save
|
|
15
|
+
Or with npm:
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
```bash
|
|
18
|
+
npm install @push.rocks/smartstate --save
|
|
22
19
|
```
|
|
23
20
|
|
|
24
21
|
## Usage
|
|
25
22
|
|
|
26
|
-
|
|
23
|
+
### Quick Start
|
|
27
24
|
|
|
28
|
-
|
|
25
|
+
```typescript
|
|
26
|
+
import { Smartstate } from '@push.rocks/smartstate';
|
|
29
27
|
|
|
30
|
-
|
|
28
|
+
// 1. Define your state part names
|
|
29
|
+
type AppParts = 'user' | 'settings';
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
```
|
|
31
|
+
// 2. Create the root instance
|
|
32
|
+
const state = new Smartstate<AppParts>();
|
|
35
33
|
|
|
36
|
-
|
|
34
|
+
// 3. Create state parts with initial values
|
|
35
|
+
const userState = await state.getStatePart<{ name: string; loggedIn: boolean }>('user', {
|
|
36
|
+
name: '',
|
|
37
|
+
loggedIn: false,
|
|
38
|
+
});
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
// 4. Subscribe to changes
|
|
41
|
+
userState.select((s) => s.name).subscribe((name) => {
|
|
42
|
+
console.log('Name changed:', name);
|
|
43
|
+
});
|
|
39
44
|
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
// 5. Update state
|
|
46
|
+
await userState.setState({ name: 'Alice', loggedIn: true });
|
|
42
47
|
```
|
|
43
48
|
|
|
44
|
-
###
|
|
49
|
+
### 🧩 State Parts & Init Modes
|
|
45
50
|
|
|
46
|
-
|
|
51
|
+
State parts are isolated, typed units of state. They are the building blocks of your application's state tree. Create them via `getStatePart()`:
|
|
47
52
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
| `'mandatory'` | Requires state part to not exist, throws error if it does |
|
|
52
|
-
| `'force'` | Always creates new state part, overwriting any existing one |
|
|
53
|
-
| `'persistent'` | Like 'soft' but with WebStore persistence using IndexedDB |
|
|
53
|
+
```typescript
|
|
54
|
+
const part = await state.getStatePart<IMyState>(name, initialState, initMode);
|
|
55
|
+
```
|
|
54
56
|
|
|
55
|
-
|
|
57
|
+
| Init Mode | Behavior |
|
|
58
|
+
|-----------|----------|
|
|
59
|
+
| `'soft'` (default) | Returns existing if found, creates new otherwise |
|
|
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 |
|
|
62
|
+
| `'persistent'` | Like `'soft'` but automatically persists state to IndexedDB via WebStore |
|
|
56
63
|
|
|
57
|
-
|
|
64
|
+
You can use either enums or string literal types for state part names:
|
|
58
65
|
|
|
59
66
|
```typescript
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
// String literal types (simpler)
|
|
68
|
+
type AppParts = 'user' | 'settings' | 'cart';
|
|
69
|
+
|
|
70
|
+
// Enums (more explicit)
|
|
71
|
+
enum AppParts {
|
|
72
|
+
User = 'user',
|
|
73
|
+
Settings = 'settings',
|
|
74
|
+
Cart = 'cart',
|
|
64
75
|
}
|
|
65
|
-
|
|
66
|
-
// Option 2: Using string literal types (simpler approach)
|
|
67
|
-
type AppStateParts = 'UserState' | 'SettingsState';
|
|
68
76
|
```
|
|
69
77
|
|
|
70
|
-
|
|
78
|
+
#### 💾 Persistent State
|
|
71
79
|
|
|
72
80
|
```typescript
|
|
73
|
-
|
|
74
|
-
isLoggedIn: boolean;
|
|
75
|
-
username?: string;
|
|
76
|
-
}
|
|
81
|
+
const settings = await state.getStatePart('settings', { theme: 'dark', fontSize: 14 }, 'persistent');
|
|
77
82
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
'soft' // Init mode (optional, defaults to 'soft')
|
|
82
|
-
);
|
|
83
|
+
// ✅ Automatically saved to IndexedDB on every setState()
|
|
84
|
+
// ✅ On next app load, persisted values override defaults
|
|
85
|
+
// ✅ Persistence writes complete before in-memory updates (atomic)
|
|
83
86
|
```
|
|
84
87
|
|
|
85
|
-
###
|
|
88
|
+
### 🔭 Selecting State
|
|
86
89
|
|
|
87
|
-
|
|
90
|
+
`select()` returns an RxJS Observable that emits the current value immediately and on every subsequent change:
|
|
88
91
|
|
|
89
92
|
```typescript
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
// Full state
|
|
94
|
+
userState.select().subscribe((state) => console.log(state));
|
|
95
|
+
|
|
96
|
+
// Derived value via selector function
|
|
97
|
+
userState.select((s) => s.name).subscribe((name) => console.log(name));
|
|
94
98
|
```
|
|
95
99
|
|
|
96
|
-
|
|
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
|
+
|
|
102
|
+
#### ✂️ AbortSignal Support
|
|
103
|
+
|
|
104
|
+
Clean up subscriptions without manual `.unsubscribe()` — the modern way:
|
|
97
105
|
|
|
98
106
|
```typescript
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
107
|
+
const controller = new AbortController();
|
|
108
|
+
|
|
109
|
+
userState.select((s) => s.name, { signal: controller.signal }).subscribe((name) => {
|
|
110
|
+
console.log(name); // automatically stops receiving when aborted
|
|
103
111
|
});
|
|
112
|
+
|
|
113
|
+
// Later: clean up all subscriptions tied to this signal
|
|
114
|
+
controller.abort();
|
|
104
115
|
```
|
|
105
116
|
|
|
106
|
-
###
|
|
117
|
+
### ⚡ Actions
|
|
107
118
|
|
|
108
|
-
|
|
119
|
+
Actions provide controlled, named state mutations with full async support:
|
|
109
120
|
|
|
110
121
|
```typescript
|
|
111
122
|
interface ILoginPayload {
|
|
112
123
|
username: string;
|
|
124
|
+
email: string;
|
|
113
125
|
}
|
|
114
126
|
|
|
115
|
-
const
|
|
116
|
-
|
|
127
|
+
const loginAction = userState.createAction<ILoginPayload>(async (statePart, payload) => {
|
|
128
|
+
// You have access to the current state via statePart.getState()
|
|
129
|
+
const current = statePart.getState();
|
|
130
|
+
return { ...current, name: payload.username, loggedIn: true };
|
|
117
131
|
});
|
|
118
132
|
|
|
119
|
-
//
|
|
120
|
-
|
|
133
|
+
// Two equivalent ways to dispatch:
|
|
134
|
+
await loginAction.trigger({ username: 'Alice', email: 'alice@example.com' });
|
|
135
|
+
// or
|
|
136
|
+
await userState.dispatchAction(loginAction, { username: 'Alice', email: 'alice@example.com' });
|
|
121
137
|
```
|
|
122
138
|
|
|
123
|
-
|
|
139
|
+
Both `trigger()` and `dispatchAction()` return a Promise with the new state.
|
|
124
140
|
|
|
125
|
-
|
|
141
|
+
### 🛡️ Middleware
|
|
142
|
+
|
|
143
|
+
Intercept every `setState()` call to transform, validate, log, or reject state changes:
|
|
126
144
|
|
|
127
145
|
```typescript
|
|
128
|
-
//
|
|
129
|
-
|
|
146
|
+
// Logging middleware
|
|
147
|
+
userState.addMiddleware((newState, oldState) => {
|
|
148
|
+
console.log('State changing:', oldState, '→', newState);
|
|
149
|
+
return newState;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Validation middleware — throw to reject the change
|
|
153
|
+
userState.addMiddleware((newState) => {
|
|
154
|
+
if (!newState.name) throw new Error('Name is required');
|
|
155
|
+
return newState;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Transform middleware
|
|
159
|
+
userState.addMiddleware((newState) => {
|
|
160
|
+
return { ...newState, name: newState.name.trim() };
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Async middleware
|
|
164
|
+
userState.addMiddleware(async (newState, oldState) => {
|
|
165
|
+
await auditLog('state-change', { from: oldState, to: newState });
|
|
166
|
+
return newState;
|
|
167
|
+
});
|
|
130
168
|
|
|
131
|
-
//
|
|
132
|
-
const
|
|
169
|
+
// Removal — addMiddleware() returns a dispose function
|
|
170
|
+
const remove = userState.addMiddleware(myMiddleware);
|
|
171
|
+
remove(); // middleware no longer runs
|
|
133
172
|
```
|
|
134
173
|
|
|
135
|
-
|
|
174
|
+
Middleware runs **sequentially** in insertion order. If any middleware throws, the state remains unchanged — the operation is **atomic**.
|
|
136
175
|
|
|
137
|
-
###
|
|
176
|
+
### 🧮 Computed / Derived State
|
|
138
177
|
|
|
139
|
-
|
|
178
|
+
Derive reactive values from one or more state parts using `combineLatest` under the hood:
|
|
140
179
|
|
|
141
180
|
```typescript
|
|
142
|
-
|
|
143
|
-
const currentState = userStatePart.getState();
|
|
144
|
-
if (currentState) {
|
|
145
|
-
console.log('Current user:', currentState.username);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Wait for state to be present
|
|
149
|
-
await userStatePart.waitUntilPresent();
|
|
181
|
+
import { computed } from '@push.rocks/smartstate';
|
|
150
182
|
|
|
151
|
-
|
|
152
|
-
await
|
|
183
|
+
const userState = await state.getStatePart('user', { firstName: 'Jane', lastName: 'Doe' });
|
|
184
|
+
const settingsState = await state.getStatePart('settings', { locale: 'en' });
|
|
153
185
|
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
}
|
|
186
|
+
// Standalone function
|
|
187
|
+
const greeting$ = computed(
|
|
188
|
+
[userState, settingsState],
|
|
189
|
+
(user, settings) => `Hello, ${user.firstName} (${settings.locale})`,
|
|
190
|
+
);
|
|
160
191
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const userData = await fetchUserData();
|
|
164
|
-
return { ...statePart.getState(), ...userData };
|
|
165
|
-
});
|
|
192
|
+
greeting$.subscribe((msg) => console.log(msg));
|
|
193
|
+
// => "Hello, Jane (en)"
|
|
166
194
|
|
|
167
|
-
//
|
|
168
|
-
|
|
195
|
+
// Also available as a convenience method on the Smartstate instance:
|
|
196
|
+
const greeting2$ = state.computed(
|
|
197
|
+
[userState, settingsState],
|
|
198
|
+
(user, settings) => `${user.firstName} - ${settings.locale}`,
|
|
199
|
+
);
|
|
169
200
|
```
|
|
170
201
|
|
|
171
|
-
|
|
202
|
+
Computed observables are **lazy** — they only subscribe to their sources when someone subscribes to them, and they automatically unsubscribe when all subscribers disconnect.
|
|
203
|
+
|
|
204
|
+
### 📦 Batch Updates
|
|
172
205
|
|
|
173
|
-
|
|
206
|
+
Update multiple state parts at once while deferring all notifications until the entire batch completes:
|
|
174
207
|
|
|
175
208
|
```typescript
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
{ theme: 'light' }, // Initial/default state
|
|
179
|
-
'persistent' // Mode
|
|
180
|
-
);
|
|
181
|
-
```
|
|
209
|
+
const partA = await state.getStatePart('a', { value: 1 });
|
|
210
|
+
const partB = await state.getStatePart('b', { value: 2 });
|
|
182
211
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
212
|
+
await state.batch(async () => {
|
|
213
|
+
await partA.setState({ value: 10 });
|
|
214
|
+
await partB.setState({ value: 20 });
|
|
215
|
+
// No notifications fire inside the batch
|
|
216
|
+
});
|
|
217
|
+
// Both subscribers now fire with their new values simultaneously
|
|
218
|
+
|
|
219
|
+
// Nested batches are supported — flush happens at the outermost level only
|
|
220
|
+
await state.batch(async () => {
|
|
221
|
+
await partA.setState({ value: 100 });
|
|
222
|
+
await state.batch(async () => {
|
|
223
|
+
await partB.setState({ value: 200 });
|
|
224
|
+
});
|
|
225
|
+
// Still deferred — inner batch doesn't trigger flush
|
|
226
|
+
});
|
|
227
|
+
// Now both fire
|
|
228
|
+
```
|
|
188
229
|
|
|
189
|
-
### State
|
|
230
|
+
### ⏳ Waiting for State
|
|
190
231
|
|
|
191
|
-
|
|
232
|
+
Wait for a specific state condition to be met before proceeding:
|
|
192
233
|
|
|
193
234
|
```typescript
|
|
194
|
-
//
|
|
195
|
-
await
|
|
235
|
+
// Wait for any truthy state
|
|
236
|
+
const currentState = await userState.waitUntilPresent();
|
|
196
237
|
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
238
|
+
// Wait for a specific condition
|
|
239
|
+
const name = await userState.waitUntilPresent((s) => s.name || undefined);
|
|
240
|
+
|
|
241
|
+
// With timeout (milliseconds)
|
|
242
|
+
const name = await userState.waitUntilPresent((s) => s.name || undefined, 5000);
|
|
243
|
+
|
|
244
|
+
// With AbortSignal and/or timeout via options object
|
|
245
|
+
const controller = new AbortController();
|
|
246
|
+
try {
|
|
247
|
+
const name = await userState.waitUntilPresent(
|
|
248
|
+
(s) => s.name || undefined,
|
|
249
|
+
{ timeoutMs: 5000, signal: controller.signal },
|
|
250
|
+
);
|
|
251
|
+
} catch (e) {
|
|
252
|
+
// e.message is 'Aborted' or 'waitUntilPresent timed out after 5000ms'
|
|
202
253
|
}
|
|
203
254
|
```
|
|
204
255
|
|
|
205
|
-
###
|
|
256
|
+
### 🌐 Context Protocol Bridge (Web Components)
|
|
257
|
+
|
|
258
|
+
Expose state parts to web components via the [W3C Context Protocol](https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md). This lets any web component framework (Lit, FAST, Stencil, or vanilla) consume your state without coupling:
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
import { attachContextProvider } from '@push.rocks/smartstate';
|
|
262
|
+
|
|
263
|
+
// Define a context key (use Symbol for uniqueness)
|
|
264
|
+
const themeContext = Symbol('theme');
|
|
206
265
|
|
|
207
|
-
|
|
266
|
+
// Attach a provider to a DOM element — any descendant can consume it
|
|
267
|
+
const cleanup = attachContextProvider(document.body, {
|
|
268
|
+
context: themeContext,
|
|
269
|
+
statePart: settingsState,
|
|
270
|
+
selectorFn: (s) => s.theme, // optional: provide a derived value instead of full state
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// A consumer dispatches a context-request event:
|
|
274
|
+
myComponent.dispatchEvent(
|
|
275
|
+
new CustomEvent('context-request', {
|
|
276
|
+
bubbles: true,
|
|
277
|
+
composed: true,
|
|
278
|
+
detail: {
|
|
279
|
+
context: themeContext,
|
|
280
|
+
callback: (theme) => console.log('Got theme:', theme),
|
|
281
|
+
subscribe: true, // receive updates whenever the state changes
|
|
282
|
+
},
|
|
283
|
+
}),
|
|
284
|
+
);
|
|
208
285
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
- **⚡ Concurrent Access Safety**: Prevents race conditions when multiple calls request the same state part simultaneously
|
|
286
|
+
// Works seamlessly with Lit's @consume() decorator, FAST's context, etc.
|
|
287
|
+
|
|
288
|
+
// Cleanup when the provider is no longer needed
|
|
289
|
+
cleanup();
|
|
290
|
+
```
|
|
215
291
|
|
|
216
|
-
###
|
|
292
|
+
### ✅ State Validation
|
|
217
293
|
|
|
218
|
-
`
|
|
294
|
+
Built-in validation prevents `null` and `undefined` from being set as state. For custom validation, extend `StatePart`:
|
|
219
295
|
|
|
220
296
|
```typescript
|
|
221
|
-
|
|
222
|
-
|
|
297
|
+
import { StatePart } from '@push.rocks/smartstate';
|
|
298
|
+
|
|
299
|
+
class ValidatedUserPart extends StatePart<string, IUserState> {
|
|
300
|
+
protected validateState(stateArg: any): stateArg is IUserState {
|
|
301
|
+
return (
|
|
302
|
+
super.validateState(stateArg) &&
|
|
303
|
+
typeof stateArg.name === 'string' &&
|
|
304
|
+
typeof stateArg.loggedIn === 'boolean'
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
If validation fails, `setState()` throws and the state remains unchanged.
|
|
311
|
+
|
|
312
|
+
### ⚙️ Async State Setup
|
|
223
313
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
314
|
+
Initialize state with async operations while ensuring actions wait for setup to complete:
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
await userState.stateSetup(async (statePart) => {
|
|
318
|
+
const userData = await fetchUserFromAPI();
|
|
319
|
+
return { ...statePart.getState(), ...userData };
|
|
227
320
|
});
|
|
228
321
|
|
|
229
|
-
//
|
|
230
|
-
userStatePart.select(state => state.username)
|
|
231
|
-
.pipe(
|
|
232
|
-
distinctUntilChanged(),
|
|
233
|
-
filter(username => username !== undefined)
|
|
234
|
-
)
|
|
235
|
-
.subscribe(username => {
|
|
236
|
-
console.log('Username changed:', username);
|
|
237
|
-
});
|
|
322
|
+
// Any dispatchAction() calls will automatically wait for stateSetup() to finish
|
|
238
323
|
```
|
|
239
324
|
|
|
240
|
-
###
|
|
325
|
+
### 🏎️ Performance
|
|
241
326
|
|
|
242
|
-
|
|
327
|
+
Smartstate is built with performance in mind:
|
|
243
328
|
|
|
244
|
-
|
|
245
|
-
|
|
329
|
+
- **🔒 SHA256 Change Detection** — Uses content hashing to detect actual changes. Identical state values don't trigger notifications, even with different object references.
|
|
330
|
+
- **♻️ Selector Memoization** — `select(fn)` caches observables by function reference and shares them via `shareReplay({ refCount: true })`. Multiple subscribers share one upstream subscription.
|
|
331
|
+
- **📦 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 or race conditions.
|
|
333
|
+
- **💾 Atomic Persistence** — WebStore writes complete before in-memory state updates, ensuring consistency even if the process crashes mid-write.
|
|
334
|
+
- **⏸️ Batch Deferred Notifications** — `batch()` suppresses all subscriber notifications until every update in the batch completes.
|
|
246
335
|
|
|
247
|
-
|
|
248
|
-
type AppStateParts = 'user' | 'settings' | 'cart';
|
|
336
|
+
## API Reference
|
|
249
337
|
|
|
250
|
-
|
|
251
|
-
isLoggedIn: boolean;
|
|
252
|
-
username?: string;
|
|
253
|
-
email?: string;
|
|
254
|
-
}
|
|
338
|
+
### `Smartstate<T>`
|
|
255
339
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
340
|
+
| Method / Property | Description |
|
|
341
|
+
|-------------------|-------------|
|
|
342
|
+
| `getStatePart(name, initial?, initMode?)` | Get or create a typed state part |
|
|
343
|
+
| `batch(fn)` | Batch state updates, defer all notifications until complete |
|
|
344
|
+
| `computed(sources, fn)` | Create a computed observable from multiple state parts |
|
|
345
|
+
| `isBatching` | `boolean` — whether a batch is currently active |
|
|
346
|
+
| `statePartMap` | Registry of all created state parts |
|
|
260
347
|
|
|
261
|
-
|
|
262
|
-
const appState = new Smartstate<AppStateParts>();
|
|
348
|
+
### `StatePart<TName, TPayload>`
|
|
263
349
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
350
|
+
| Method | Description |
|
|
351
|
+
|--------|-------------|
|
|
352
|
+
| `getState()` | Get current state (returns `TPayload \| undefined`) |
|
|
353
|
+
| `setState(newState)` | Set state — runs middleware → validates → persists → notifies |
|
|
354
|
+
| `select(selectorFn?, options?)` | Returns an Observable of state or derived values. Options: `{ signal?: AbortSignal }` |
|
|
355
|
+
| `createAction(actionDef)` | Create a reusable, typed state action |
|
|
356
|
+
| `dispatchAction(action, payload)` | Dispatch an action and return the new state |
|
|
357
|
+
| `addMiddleware(fn)` | Add a middleware interceptor. Returns a removal function |
|
|
358
|
+
| `waitUntilPresent(selectorFn?, opts?)` | Wait for a state condition. Opts: `number` (timeout) or `{ timeoutMs?, signal? }` |
|
|
359
|
+
| `notifyChange()` | Manually trigger a change notification (with hash dedup) |
|
|
360
|
+
| `notifyChangeCumulative()` | Debounced notification — fires at end of call stack |
|
|
361
|
+
| `stateSetup(fn)` | Async state initialization with action serialization |
|
|
268
362
|
|
|
269
|
-
|
|
270
|
-
items: [],
|
|
271
|
-
total: 0
|
|
272
|
-
}, 'persistent'); // Persists across sessions
|
|
273
|
-
|
|
274
|
-
// Create actions
|
|
275
|
-
const loginAction = userState.createAction<{ username: string; email: string }>(
|
|
276
|
-
async (statePart, payload) => {
|
|
277
|
-
// Simulate API call
|
|
278
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
279
|
-
|
|
280
|
-
return {
|
|
281
|
-
isLoggedIn: true,
|
|
282
|
-
username: payload.username,
|
|
283
|
-
email: payload.email
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
);
|
|
363
|
+
### `StateAction<TState, TPayload>`
|
|
287
364
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
});
|
|
365
|
+
| Method | Description |
|
|
366
|
+
|--------|-------------|
|
|
367
|
+
| `trigger(payload)` | Dispatch the action on its associated state part |
|
|
292
368
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
369
|
+
### Standalone Functions
|
|
370
|
+
|
|
371
|
+
| Function | Description |
|
|
372
|
+
|----------|-------------|
|
|
373
|
+
| `computed(sources, fn)` | Create a computed observable from multiple state parts |
|
|
374
|
+
| `attachContextProvider(element, options)` | Bridge a state part to the W3C Context Protocol |
|
|
296
375
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
|
300
|
-
|
|
301
|
-
|
|
|
302
|
-
|
|
|
303
|
-
|
|
|
304
|
-
|
|
|
305
|
-
| 🧩 **Modular** | Organize state into logical, reusable parts |
|
|
306
|
-
| ✅ **Validated** | Built-in state validation with extensible validation logic |
|
|
307
|
-
| 🎭 **Flexible init modes** | Choose how state parts are initialized |
|
|
308
|
-
| 📦 **Zero config** | Works out of the box with sensible defaults |
|
|
309
|
-
| 🛡️ **Race condition safe** | Concurrent state part creation is handled safely |
|
|
310
|
-
| ⏱️ **Timeout support** | `waitUntilPresent` supports optional timeouts |
|
|
376
|
+
### Exported Types
|
|
377
|
+
|
|
378
|
+
| Type | Description |
|
|
379
|
+
|------|-------------|
|
|
380
|
+
| `TInitMode` | `'soft' \| 'mandatory' \| 'force' \| 'persistent'` |
|
|
381
|
+
| `TMiddleware<TPayload>` | `(newState, oldState) => TPayload \| Promise<TPayload>` |
|
|
382
|
+
| `IActionDef<TState, TPayload>` | Action definition function signature |
|
|
383
|
+
| `IContextProviderOptions<TPayload>` | Options for `attachContextProvider` |
|
|
311
384
|
|
|
312
385
|
## License and Legal Information
|
|
313
386
|
|
|
314
|
-
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [
|
|
387
|
+
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
|
315
388
|
|
|
316
389
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
|
317
390
|
|
|
@@ -323,7 +396,7 @@ Use of these trademarks must comply with Task Venture Capital GmbH's Trademark G
|
|
|
323
396
|
|
|
324
397
|
### Company Information
|
|
325
398
|
|
|
326
|
-
Task Venture Capital GmbH
|
|
399
|
+
Task Venture Capital GmbH
|
|
327
400
|
Registered at District Court Bremen HRB 35230 HB, Germany
|
|
328
401
|
|
|
329
402
|
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.
|
|
7
|
-
description: 'A
|
|
6
|
+
version: '2.1.1',
|
|
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
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as plugins from './smartstate.plugins.js';
|
|
2
|
+
import { combineLatest, map } from 'rxjs';
|
|
3
|
+
import type { StatePart } from './smartstate.classes.statepart.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* creates a computed observable derived from multiple state parts.
|
|
7
|
+
* the observable is lazy — it only subscribes to sources when subscribed to.
|
|
8
|
+
*/
|
|
9
|
+
export function computed<TResult>(
|
|
10
|
+
sources: StatePart<any, any>[],
|
|
11
|
+
computeFn: (...states: any[]) => TResult,
|
|
12
|
+
): plugins.smartrx.rxjs.Observable<TResult> {
|
|
13
|
+
return combineLatest(sources.map((sp) => sp.select())).pipe(
|
|
14
|
+
map((states) => computeFn(...states)),
|
|
15
|
+
) as plugins.smartrx.rxjs.Observable<TResult>;
|
|
16
|
+
}
|