@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/readme.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @push.rocks/smartstate
2
2
 
3
- A powerful TypeScript library for elegant state management using RxJS and reactive programming patterns 🚀
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
- # Using npm
18
- npm install @push.rocks/smartstate --save
15
+ Or with npm:
19
16
 
20
- # Using yarn
21
- yarn add @push.rocks/smartstate
17
+ ```bash
18
+ npm install @push.rocks/smartstate --save
22
19
  ```
23
20
 
24
21
  ## Usage
25
22
 
26
- The `@push.rocks/smartstate` library provides an elegant way to handle state within your JavaScript or TypeScript projects, leveraging the power of Reactive Extensions (RxJS) and a structured state management strategy.
23
+ ### Quick Start
27
24
 
28
- ### Getting Started
25
+ ```typescript
26
+ import { Smartstate } from '@push.rocks/smartstate';
29
27
 
30
- Import the necessary components from the library:
28
+ // 1. Define your state part names
29
+ type AppParts = 'user' | 'settings';
31
30
 
32
- ```typescript
33
- import { Smartstate, StatePart, StateAction } from '@push.rocks/smartstate';
34
- ```
31
+ // 2. Create the root instance
32
+ const state = new Smartstate<AppParts>();
35
33
 
36
- ### Creating a SmartState Instance
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
- `Smartstate` acts as the container for your state parts. Think of it as the root of your state management structure:
40
+ // 4. Subscribe to changes
41
+ userState.select((s) => s.name).subscribe((name) => {
42
+ console.log('Name changed:', name);
43
+ });
39
44
 
40
- ```typescript
41
- const myAppSmartState = new Smartstate<YourStatePartNamesEnum>();
45
+ // 5. Update state
46
+ await userState.setState({ name: 'Alice', loggedIn: true });
42
47
  ```
43
48
 
44
- ### Understanding Init Modes
49
+ ### 🧩 State Parts & Init Modes
45
50
 
46
- When creating state parts, you can specify different initialization modes:
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
- | Mode | Description |
49
- |------|-------------|
50
- | `'soft'` | Default. Returns existing state part if it exists, creates new if not |
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
- ### Defining State Parts
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
- State parts represent separable sections of your state, making it easier to manage and modularize. Define state part names using either enums or string literal types:
64
+ You can use either enums or string literal types for state part names:
58
65
 
59
66
  ```typescript
60
- // Option 1: Using enums
61
- enum AppStateParts {
62
- UserState = 'UserState',
63
- SettingsState = 'SettingsState'
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
- Create a state part within your `Smartstate` instance:
78
+ #### 💾 Persistent State
71
79
 
72
80
  ```typescript
73
- interface IUserState {
74
- isLoggedIn: boolean;
75
- username?: string;
76
- }
81
+ const settings = await state.getStatePart('settings', { theme: 'dark', fontSize: 14 }, 'persistent');
77
82
 
78
- const userStatePart = await myAppSmartState.getStatePart<IUserState>(
79
- AppStateParts.UserState,
80
- { isLoggedIn: false }, // Initial state
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
- ### Subscribing to State Changes
88
+ ### 🔭 Selecting State
86
89
 
87
- Subscribe to changes in a state part to perform actions accordingly:
90
+ `select()` returns an RxJS Observable that emits the current value immediately and on every subsequent change:
88
91
 
89
92
  ```typescript
90
- // The select() method automatically filters out undefined states
91
- userStatePart.select().subscribe((currentState) => {
92
- console.log(`User Logged In: ${currentState.isLoggedIn}`);
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
- Select a specific part of your state with a selector function:
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
- userStatePart.select(state => state.username).subscribe((username) => {
100
- if (username) {
101
- console.log(`Current user: ${username}`);
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
- ### Modifying State with Actions
117
+ ### Actions
107
118
 
108
- Create actions to modify the state in a controlled manner:
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 loginUserAction = userStatePart.createAction<ILoginPayload>(async (statePart, payload) => {
116
- return { ...statePart.getState(), isLoggedIn: true, username: payload.username };
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
- // Dispatch the action to update the state
120
- const newState = await loginUserAction.trigger({ username: 'johnDoe' });
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
- ### Dispatching Actions
139
+ Both `trigger()` and `dispatchAction()` return a Promise with the new state.
124
140
 
125
- There are two ways to dispatch actions:
141
+ ### 🛡️ Middleware
142
+
143
+ Intercept every `setState()` call to transform, validate, log, or reject state changes:
126
144
 
127
145
  ```typescript
128
- // Method 1: Using trigger on the action (returns promise)
129
- const newState = await loginUserAction.trigger({ username: 'johnDoe' });
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
- // Method 2: Using dispatchAction on the state part (returns promise)
132
- const newState = await userStatePart.dispatchAction(loginUserAction, { username: 'johnDoe' });
169
+ // Removal addMiddleware() returns a dispose function
170
+ const remove = userState.addMiddleware(myMiddleware);
171
+ remove(); // middleware no longer runs
133
172
  ```
134
173
 
135
- Both methods return a Promise with the new state payload.
174
+ Middleware runs **sequentially** in insertion order. If any middleware throws, the state remains unchanged — the operation is **atomic**.
136
175
 
137
- ### Additional State Methods
176
+ ### 🧮 Computed / Derived State
138
177
 
139
- `StatePart` provides several useful methods for state management:
178
+ Derive reactive values from one or more state parts using `combineLatest` under the hood:
140
179
 
141
180
  ```typescript
142
- // Get current state (may be undefined initially)
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
- // Wait for a specific property to be present
152
- await userStatePart.waitUntilPresent(state => state.username);
183
+ const userState = await state.getStatePart('user', { firstName: 'Jane', lastName: 'Doe' });
184
+ const settingsState = await state.getStatePart('settings', { locale: 'en' });
153
185
 
154
- // Wait with a timeout (throws error if condition not met within timeout)
155
- try {
156
- await userStatePart.waitUntilPresent(state => state.username, 5000); // 5 second timeout
157
- } catch (error) {
158
- console.error('Timed out waiting for username');
159
- }
186
+ // Standalone function
187
+ const greeting$ = computed(
188
+ [userState, settingsState],
189
+ (user, settings) => `Hello, ${user.firstName} (${settings.locale})`,
190
+ );
160
191
 
161
- // Setup initial state with async operations
162
- await userStatePart.stateSetup(async (statePart) => {
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
- // Defer notification to end of call stack (debounced)
168
- userStatePart.notifyChangeCumulative();
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
- ### Persistent State with WebStore
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
- `Smartstate` supports persistent states using WebStore (IndexedDB-based storage), allowing you to maintain state across sessions:
206
+ Update multiple state parts at once while deferring all notifications until the entire batch completes:
174
207
 
175
208
  ```typescript
176
- const settingsStatePart = await myAppSmartState.getStatePart<ISettingsState>(
177
- AppStateParts.SettingsState,
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
- 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)
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 Validation
230
+ ### ⏳ Waiting for State
190
231
 
191
- `Smartstate` includes built-in state validation to ensure data integrity:
232
+ Wait for a specific state condition to be met before proceeding:
192
233
 
193
234
  ```typescript
194
- // Basic validation (built-in) ensures state is not null or undefined
195
- await userStatePart.setState(null); // Throws error: Invalid state structure
235
+ // Wait for any truthy state
236
+ const currentState = await userState.waitUntilPresent();
196
237
 
197
- // Custom validation by extending StatePart
198
- class ValidatedStatePart<T> extends StatePart<string, T> {
199
- protected validateState(stateArg: any): stateArg is T {
200
- return super.validateState(stateArg) && /* your validation */;
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
- ### Performance Optimization
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
- `Smartstate` includes advanced performance optimizations:
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
- - **🔒 Async State Hash Detection**: Uses SHA256 hashing to detect actual state changes, preventing unnecessary notifications when state values haven't truly changed
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
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
- ### RxJS Integration
292
+ ### State Validation
217
293
 
218
- `Smartstate` leverages RxJS for reactive state management:
294
+ Built-in validation prevents `null` and `undefined` from being set as state. For custom validation, extend `StatePart`:
219
295
 
220
296
  ```typescript
221
- // State is exposed as an RxJS Subject
222
- const stateObservable = userStatePart.select();
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
- // Automatically starts with current state value
225
- stateObservable.subscribe((state) => {
226
- console.log('Current state:', state);
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
- // Use selectors for specific properties
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
- ### Complete Example
325
+ ### 🏎️ Performance
241
326
 
242
- Here's a comprehensive example showcasing the power of `@push.rocks/smartstate`:
327
+ Smartstate is built with performance in mind:
243
328
 
244
- ```typescript
245
- import { Smartstate, StatePart, StateAction } from '@push.rocks/smartstate';
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
- // Define your state structure
248
- type AppStateParts = 'user' | 'settings' | 'cart';
336
+ ## API Reference
249
337
 
250
- interface IUserState {
251
- isLoggedIn: boolean;
252
- username?: string;
253
- email?: string;
254
- }
338
+ ### `Smartstate<T>`
255
339
 
256
- interface ICartState {
257
- items: Array<{ id: string; quantity: number }>;
258
- total: number;
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
- // Create the smartstate instance
262
- const appState = new Smartstate<AppStateParts>();
348
+ ### `StatePart<TName, TPayload>`
263
349
 
264
- // Initialize state parts
265
- const userState = await appState.getStatePart<IUserState>('user', {
266
- isLoggedIn: false
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
- const cartState = await appState.getStatePart<ICartState>('cart', {
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
- // Subscribe to changes
289
- userState.select(state => state.isLoggedIn).subscribe(isLoggedIn => {
290
- console.log('Login status changed:', isLoggedIn);
291
- });
365
+ | Method | Description |
366
+ |--------|-------------|
367
+ | `trigger(payload)` | Dispatch the action on its associated state part |
292
368
 
293
- // Dispatch actions
294
- await loginAction.trigger({ username: 'john', email: 'john@example.com' });
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
- ## Key Features
298
-
299
- | Feature | Description |
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 |
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 [license](./license) file.
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.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartstate',
6
- version: '2.0.31',
7
- description: 'A package for handling and managing state in applications.'
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
@@ -1,3 +1,5 @@
1
1
  export * from './smartstate.classes.smartstate.js';
2
2
  export * from './smartstate.classes.statepart.js';
3
3
  export * from './smartstate.classes.stateaction.js';
4
+ export * from './smartstate.classes.computed.js';
5
+ export * from './smartstate.contextprovider.js';
@@ -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
+ }