@push.rocks/smartstate 2.0.30 → 2.1.0
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 +19 -10
- package/dist_ts/smartstate.classes.smartstate.js +44 -17
- package/dist_ts/smartstate.classes.statepart.d.ts +26 -9
- package/dist_ts/smartstate.classes.statepart.js +120 -24
- 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 +11 -6
- package/readme.hints.md +48 -31
- package/readme.md +215 -210
- 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 +51 -17
- package/ts/smartstate.classes.statepart.ts +140 -23
- 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,315 @@ 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. Create them with `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 |
|
|
61
|
+
| `'force'` | Always creates new, overwrites existing |
|
|
62
|
+
| `'persistent'` | Like `'soft'` but persists to IndexedDB via WebStore |
|
|
56
63
|
|
|
57
|
-
|
|
64
|
+
#### Persistent State
|
|
58
65
|
|
|
59
66
|
```typescript
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
UserState = 'UserState',
|
|
63
|
-
SettingsState = 'SettingsState'
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Option 2: Using string literal types (simpler approach)
|
|
67
|
-
type AppStateParts = 'UserState' | 'SettingsState';
|
|
67
|
+
const settings = await state.getStatePart('settings', { theme: 'dark' }, 'persistent');
|
|
68
|
+
// Automatically saved to IndexedDB. On next app load, persisted values override defaults.
|
|
68
69
|
```
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
### Selecting State
|
|
72
|
+
|
|
73
|
+
`select()` returns an RxJS Observable that emits the current value immediately and on every change:
|
|
71
74
|
|
|
72
75
|
```typescript
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
username?: string;
|
|
76
|
-
}
|
|
76
|
+
// Full state
|
|
77
|
+
userState.select().subscribe((state) => console.log(state));
|
|
77
78
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
{ isLoggedIn: false }, // Initial state
|
|
81
|
-
'soft' // Init mode (optional, defaults to 'soft')
|
|
82
|
-
);
|
|
79
|
+
// Derived value via selector
|
|
80
|
+
userState.select((s) => s.name).subscribe((name) => console.log(name));
|
|
83
81
|
```
|
|
84
82
|
|
|
85
|
-
|
|
83
|
+
Selectors are **memoized** — calling `select(fn)` with the same function reference returns the same cached Observable, shared across all subscribers.
|
|
86
84
|
|
|
87
|
-
|
|
85
|
+
#### AbortSignal Support
|
|
86
|
+
|
|
87
|
+
Clean up subscriptions without manual `unsubscribe()`:
|
|
88
88
|
|
|
89
89
|
```typescript
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
const controller = new AbortController();
|
|
91
|
+
|
|
92
|
+
userState.select((s) => s.name, { signal: controller.signal }).subscribe((name) => {
|
|
93
|
+
console.log(name); // stops receiving when aborted
|
|
93
94
|
});
|
|
95
|
+
|
|
96
|
+
// Later: clean up
|
|
97
|
+
controller.abort();
|
|
94
98
|
```
|
|
95
99
|
|
|
96
|
-
|
|
100
|
+
### Actions
|
|
101
|
+
|
|
102
|
+
Actions provide controlled, named state mutations:
|
|
97
103
|
|
|
98
104
|
```typescript
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
console.log(`Current user: ${username}`);
|
|
102
|
-
}
|
|
105
|
+
const login = userState.createAction<{ name: string }>(async (statePart, payload) => {
|
|
106
|
+
return { ...statePart.getState(), name: payload.name, loggedIn: true };
|
|
103
107
|
});
|
|
108
|
+
|
|
109
|
+
// Two equivalent ways to dispatch:
|
|
110
|
+
await login.trigger({ name: 'Alice' });
|
|
111
|
+
await userState.dispatchAction(login, { name: 'Alice' });
|
|
104
112
|
```
|
|
105
113
|
|
|
106
|
-
###
|
|
114
|
+
### Middleware
|
|
107
115
|
|
|
108
|
-
|
|
116
|
+
Intercept every `setState()` call to transform, validate, or reject state changes:
|
|
109
117
|
|
|
110
118
|
```typescript
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
119
|
+
// Logging middleware
|
|
120
|
+
userState.addMiddleware((newState, oldState) => {
|
|
121
|
+
console.log('State changing from', oldState, 'to', newState);
|
|
122
|
+
return newState;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Validation middleware — throw to reject the change
|
|
126
|
+
userState.addMiddleware((newState) => {
|
|
127
|
+
if (!newState.name) throw new Error('Name is required');
|
|
128
|
+
return newState;
|
|
129
|
+
});
|
|
114
130
|
|
|
115
|
-
|
|
116
|
-
|
|
131
|
+
// Transform middleware
|
|
132
|
+
userState.addMiddleware((newState) => {
|
|
133
|
+
return { ...newState, name: newState.name.trim() };
|
|
117
134
|
});
|
|
118
135
|
|
|
119
|
-
//
|
|
120
|
-
const
|
|
136
|
+
// Removal — addMiddleware returns a dispose function
|
|
137
|
+
const remove = userState.addMiddleware(myMiddleware);
|
|
138
|
+
remove(); // middleware no longer runs
|
|
121
139
|
```
|
|
122
140
|
|
|
123
|
-
|
|
141
|
+
Middleware runs sequentially in insertion order. If any middleware throws, the state is unchanged (atomic).
|
|
142
|
+
|
|
143
|
+
### Computed / Derived State
|
|
124
144
|
|
|
125
|
-
|
|
145
|
+
Derive reactive values from one or more state parts:
|
|
126
146
|
|
|
127
147
|
```typescript
|
|
128
|
-
|
|
129
|
-
const newState = await loginUserAction.trigger({ username: 'johnDoe' });
|
|
148
|
+
import { computed } from '@push.rocks/smartstate';
|
|
130
149
|
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
```
|
|
150
|
+
const userState = await state.getStatePart('user', { firstName: 'Jane', lastName: 'Doe' });
|
|
151
|
+
const settingsState = await state.getStatePart('settings', { locale: 'en' });
|
|
134
152
|
|
|
135
|
-
|
|
153
|
+
// Standalone function
|
|
154
|
+
const greeting$ = computed(
|
|
155
|
+
[userState, settingsState],
|
|
156
|
+
(user, settings) => `Hello, ${user.firstName} (${settings.locale})`,
|
|
157
|
+
);
|
|
136
158
|
|
|
137
|
-
|
|
159
|
+
greeting$.subscribe((msg) => console.log(msg));
|
|
160
|
+
// => "Hello, Jane (en)"
|
|
138
161
|
|
|
139
|
-
|
|
162
|
+
// Also available as a method on Smartstate:
|
|
163
|
+
const greeting2$ = state.computed([userState, settingsState], (user, settings) => /* ... */);
|
|
164
|
+
```
|
|
140
165
|
|
|
141
|
-
|
|
142
|
-
// Get current state (may be undefined initially)
|
|
143
|
-
const currentState = userStatePart.getState();
|
|
144
|
-
if (currentState) {
|
|
145
|
-
console.log('Current user:', currentState.username);
|
|
146
|
-
}
|
|
166
|
+
Computed observables are **lazy** — they only subscribe to sources when someone subscribes to them.
|
|
147
167
|
|
|
148
|
-
|
|
149
|
-
await userStatePart.waitUntilPresent();
|
|
168
|
+
### Batch Updates
|
|
150
169
|
|
|
151
|
-
|
|
152
|
-
await userStatePart.waitUntilPresent(state => state.username);
|
|
170
|
+
Update multiple state parts without intermediate notifications:
|
|
153
171
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const userData = await fetchUserData();
|
|
164
|
-
return { ...statePart.getState(), ...userData };
|
|
172
|
+
```typescript
|
|
173
|
+
const partA = await state.getStatePart('a', { value: 1 });
|
|
174
|
+
const partB = await state.getStatePart('b', { value: 2 });
|
|
175
|
+
|
|
176
|
+
// Subscribers see no updates during the batch — only after it completes
|
|
177
|
+
await state.batch(async () => {
|
|
178
|
+
await partA.setState({ value: 10 });
|
|
179
|
+
await partB.setState({ value: 20 });
|
|
180
|
+
// Notifications are deferred here
|
|
165
181
|
});
|
|
182
|
+
// Both subscribers now fire with their new values
|
|
166
183
|
|
|
167
|
-
//
|
|
168
|
-
|
|
184
|
+
// Nested batches are supported — flush happens at the outermost level
|
|
185
|
+
await state.batch(async () => {
|
|
186
|
+
await partA.setState({ value: 100 });
|
|
187
|
+
await state.batch(async () => {
|
|
188
|
+
await partB.setState({ value: 200 });
|
|
189
|
+
});
|
|
190
|
+
// Still deferred
|
|
191
|
+
});
|
|
192
|
+
// Now both fire
|
|
169
193
|
```
|
|
170
194
|
|
|
171
|
-
###
|
|
195
|
+
### Waiting for State
|
|
172
196
|
|
|
173
|
-
|
|
197
|
+
Wait for a specific state condition to be met:
|
|
174
198
|
|
|
175
199
|
```typescript
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
{ theme: 'light' }, // Initial/default state
|
|
179
|
-
'persistent' // Mode
|
|
180
|
-
);
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
Persistent state automatically:
|
|
184
|
-
- Saves state changes to IndexedDB
|
|
185
|
-
- Restores state on application restart
|
|
186
|
-
- Merges persisted values with defaults (persisted values take precedence)
|
|
187
|
-
- Ensures atomic writes (persistence happens before memory update)
|
|
188
|
-
|
|
189
|
-
### State Validation
|
|
200
|
+
// Wait for any truthy state
|
|
201
|
+
const currentState = await userState.waitUntilPresent();
|
|
190
202
|
|
|
191
|
-
|
|
203
|
+
// Wait for a specific condition
|
|
204
|
+
const name = await userState.waitUntilPresent((s) => s.name || undefined);
|
|
192
205
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
await userStatePart.setState(null); // Throws error: Invalid state structure
|
|
206
|
+
// With timeout (backward compatible)
|
|
207
|
+
const name = await userState.waitUntilPresent((s) => s.name || undefined, 5000);
|
|
196
208
|
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
209
|
+
// With AbortSignal
|
|
210
|
+
const controller = new AbortController();
|
|
211
|
+
try {
|
|
212
|
+
const name = await userState.waitUntilPresent(
|
|
213
|
+
(s) => s.name || undefined,
|
|
214
|
+
{ timeoutMs: 5000, signal: controller.signal },
|
|
215
|
+
);
|
|
216
|
+
} catch (e) {
|
|
217
|
+
// 'Aborted' or timeout error
|
|
202
218
|
}
|
|
203
219
|
```
|
|
204
220
|
|
|
205
|
-
###
|
|
206
|
-
|
|
207
|
-
`Smartstate` includes advanced performance optimizations:
|
|
221
|
+
### Context Protocol Bridge (Web Components)
|
|
208
222
|
|
|
209
|
-
|
|
210
|
-
- **🚫 Duplicate Prevention**: Identical state updates are automatically filtered out
|
|
211
|
-
- **📦 Cumulative Notifications**: Batch multiple state changes into a single notification using `notifyChangeCumulative()` with automatic debouncing
|
|
212
|
-
- **🎯 Selective Subscriptions**: Use selectors to subscribe only to specific state properties
|
|
213
|
-
- **✨ Undefined State Filtering**: The `select()` method automatically filters out undefined states
|
|
214
|
-
- **⚡ Concurrent Access Safety**: Prevents race conditions when multiple calls request the same state part simultaneously
|
|
215
|
-
|
|
216
|
-
### RxJS Integration
|
|
217
|
-
|
|
218
|
-
`Smartstate` leverages RxJS for reactive state management:
|
|
223
|
+
Expose state parts to web components via the [W3C Context Protocol](https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md):
|
|
219
224
|
|
|
220
225
|
```typescript
|
|
221
|
-
|
|
222
|
-
const stateObservable = userStatePart.select();
|
|
226
|
+
import { attachContextProvider } from '@push.rocks/smartstate';
|
|
223
227
|
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
228
|
+
// Define a context key
|
|
229
|
+
const themeContext = Symbol('theme');
|
|
230
|
+
|
|
231
|
+
// Attach a provider to a DOM element
|
|
232
|
+
const cleanup = attachContextProvider(myElement, {
|
|
233
|
+
context: themeContext,
|
|
234
|
+
statePart: settingsState,
|
|
235
|
+
selectorFn: (s) => s.theme, // optional: provide derived value
|
|
227
236
|
});
|
|
228
237
|
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
+
// Any descendant can request this context:
|
|
239
|
+
myElement.dispatchEvent(
|
|
240
|
+
new CustomEvent('context-request', {
|
|
241
|
+
bubbles: true,
|
|
242
|
+
composed: true,
|
|
243
|
+
detail: {
|
|
244
|
+
context: themeContext,
|
|
245
|
+
callback: (theme) => console.log('Theme:', theme),
|
|
246
|
+
subscribe: true, // receive updates on state changes
|
|
247
|
+
},
|
|
248
|
+
}),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// Cleanup when done
|
|
252
|
+
cleanup();
|
|
238
253
|
```
|
|
239
254
|
|
|
240
|
-
|
|
255
|
+
This works with Lit's `@consume()` decorator, FAST, or any framework implementing the Context Protocol.
|
|
241
256
|
|
|
242
|
-
|
|
257
|
+
### State Validation
|
|
258
|
+
|
|
259
|
+
Built-in null/undefined validation. Extend for custom rules:
|
|
243
260
|
|
|
244
261
|
```typescript
|
|
245
|
-
|
|
262
|
+
class ValidatedPart<T> extends StatePart<string, T> {
|
|
263
|
+
protected validateState(stateArg: any): stateArg is T {
|
|
264
|
+
return super.validateState(stateArg) && typeof stateArg.name === 'string';
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
```
|
|
246
268
|
|
|
247
|
-
|
|
248
|
-
type AppStateParts = 'user' | 'settings' | 'cart';
|
|
269
|
+
### Performance Features
|
|
249
270
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
271
|
+
- **SHA256 Change Detection** — identical state values don't trigger notifications, even with different object references
|
|
272
|
+
- **Selector Memoization** — `select(fn)` caches observables by function reference, sharing one upstream subscription across all subscribers
|
|
273
|
+
- **Cumulative Notifications** — `notifyChangeCumulative()` debounces rapid changes into a single notification
|
|
274
|
+
- **Concurrent Safety** — simultaneous `getStatePart()` calls for the same name return the same promise, preventing duplicate creation
|
|
275
|
+
- **Atomic Persistence** — WebStore writes complete before in-memory state updates, ensuring consistency
|
|
276
|
+
- **Batch Deferred Notifications** — `batch()` suppresses all notifications until the batch completes
|
|
255
277
|
|
|
256
|
-
|
|
257
|
-
items: Array<{ id: string; quantity: number }>;
|
|
258
|
-
total: number;
|
|
259
|
-
}
|
|
278
|
+
## API Reference
|
|
260
279
|
|
|
261
|
-
|
|
262
|
-
const appState = new Smartstate<AppStateParts>();
|
|
280
|
+
### `Smartstate<T>`
|
|
263
281
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
282
|
+
| Method | Description |
|
|
283
|
+
|--------|-------------|
|
|
284
|
+
| `getStatePart(name, initial?, initMode?)` | Get or create a state part |
|
|
285
|
+
| `batch(fn)` | Batch updates, defer notifications |
|
|
286
|
+
| `computed(sources, fn)` | Create computed observable |
|
|
287
|
+
| `isBatching` | Whether a batch is active |
|
|
268
288
|
|
|
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
|
-
);
|
|
289
|
+
### `StatePart<TName, TPayload>`
|
|
287
290
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
291
|
+
| Method | Description |
|
|
292
|
+
|--------|-------------|
|
|
293
|
+
| `getState()` | Get current state (or undefined) |
|
|
294
|
+
| `setState(newState)` | Set state (runs middleware, validates, persists, notifies) |
|
|
295
|
+
| `select(selectorFn?, options?)` | Subscribe to state changes |
|
|
296
|
+
| `createAction(actionDef)` | Create a named action |
|
|
297
|
+
| `dispatchAction(action, payload)` | Dispatch an action |
|
|
298
|
+
| `addMiddleware(fn)` | Add middleware, returns removal function |
|
|
299
|
+
| `waitUntilPresent(selectorFn?, options?)` | Wait for state condition |
|
|
300
|
+
| `notifyChange()` | Manually trigger notification |
|
|
301
|
+
| `notifyChangeCumulative()` | Debounced notification |
|
|
302
|
+
| `stateSetup(fn)` | Async state initialization |
|
|
292
303
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
304
|
+
### `StateAction<TState, TPayload>`
|
|
305
|
+
|
|
306
|
+
| Method | Description |
|
|
307
|
+
|--------|-------------|
|
|
308
|
+
| `trigger(payload)` | Dispatch the action |
|
|
309
|
+
|
|
310
|
+
### Standalone Functions
|
|
296
311
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
|
300
|
-
|
|
301
|
-
| 🎯 **Type-safe** | Full TypeScript support with intelligent type inference |
|
|
302
|
-
| ⚡ **Performance optimized** | Async state hash detection prevents unnecessary re-renders |
|
|
303
|
-
| 💾 **Persistent state** | Built-in IndexedDB support for state persistence |
|
|
304
|
-
| 🔄 **Reactive** | Powered by RxJS for elegant async handling |
|
|
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 |
|
|
312
|
+
| Function | Description |
|
|
313
|
+
|----------|-------------|
|
|
314
|
+
| `computed(sources, fn)` | Create computed observable from state parts |
|
|
315
|
+
| `attachContextProvider(element, options)` | Bridge state to Context Protocol |
|
|
311
316
|
|
|
312
317
|
## License and Legal Information
|
|
313
318
|
|
|
314
|
-
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [
|
|
319
|
+
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
320
|
|
|
316
321
|
**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
322
|
|
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.0
|
|
7
|
-
description: 'A
|
|
6
|
+
version: '2.1.0',
|
|
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
|
+
}
|