@montra-interactive/deepstate 0.2.0 → 0.2.2
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 +343 -0
- package/package.json +2 -1
package/README.md
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
# @montra-interactive/deepstate
|
|
2
|
+
|
|
3
|
+
Proxy-based reactive state management powered by RxJS. Each property is its own observable with O(depth) change propagation.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Fine-grained reactivity**: Subscribe to any property at any depth
|
|
8
|
+
- **O(depth) performance**: Changes only notify ancestors, never siblings
|
|
9
|
+
- **Type-safe**: Full TypeScript support with inferred types
|
|
10
|
+
- **RxJS native**: Every node is an Observable - use `pipe()`, `combineLatest`, etc.
|
|
11
|
+
- **Batched updates**: Group multiple changes into a single emission
|
|
12
|
+
- **Immutable reads**: Values are deeply frozen to prevent accidental mutations
|
|
13
|
+
- **Nullable objects**: First-class support for `T | null` properties with deep subscription
|
|
14
|
+
- **Debug mode**: Optional logging for development
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @montra-interactive/deepstate rxjs
|
|
20
|
+
# or
|
|
21
|
+
bun add @montra-interactive/deepstate rxjs
|
|
22
|
+
# or
|
|
23
|
+
yarn add @montra-interactive/deepstate rxjs
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { state } from "@montra-interactive/deepstate";
|
|
30
|
+
|
|
31
|
+
// Create reactive state
|
|
32
|
+
const store = state({
|
|
33
|
+
user: { name: "Alice", age: 30 },
|
|
34
|
+
todos: [{ id: 1, text: "Learn deepstate", done: false }],
|
|
35
|
+
count: 0,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Subscribe to any property (it's an Observable)
|
|
39
|
+
store.user.name.subscribe(name => console.log("Name:", name));
|
|
40
|
+
|
|
41
|
+
// Get values synchronously
|
|
42
|
+
console.log(store.user.name.get()); // "Alice"
|
|
43
|
+
|
|
44
|
+
// Set values
|
|
45
|
+
store.user.name.set("Bob"); // triggers subscription
|
|
46
|
+
|
|
47
|
+
// Subscribe to parent nodes (emits when any child changes)
|
|
48
|
+
store.user.subscribe(user => console.log("User changed:", user));
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## API Reference
|
|
52
|
+
|
|
53
|
+
### `state<T>(initialState, options?)`
|
|
54
|
+
|
|
55
|
+
Creates a reactive state store.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { state } from "@montra-interactive/deepstate";
|
|
59
|
+
|
|
60
|
+
const store = state({
|
|
61
|
+
user: { name: "Alice", age: 30 },
|
|
62
|
+
items: [{ id: 1, name: "Item 1" }],
|
|
63
|
+
count: 0,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// With debug mode
|
|
67
|
+
const debugStore = state(
|
|
68
|
+
{ count: 0 },
|
|
69
|
+
{ debug: true, name: "counter" }
|
|
70
|
+
);
|
|
71
|
+
// Logs: [deepstate:counter] set count: 0 -> 1
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Options:**
|
|
75
|
+
|
|
76
|
+
| Option | Type | Description |
|
|
77
|
+
|--------|------|-------------|
|
|
78
|
+
| `debug` | `boolean` | Enable debug logging for all set operations |
|
|
79
|
+
| `name` | `string` | Store name used in debug log prefix |
|
|
80
|
+
|
|
81
|
+
### Node Methods
|
|
82
|
+
|
|
83
|
+
Every property on the state is a reactive node with these methods:
|
|
84
|
+
|
|
85
|
+
| Method | Description |
|
|
86
|
+
|--------|-------------|
|
|
87
|
+
| `.get()` | Get current value synchronously |
|
|
88
|
+
| `.set(value)` | Update the value |
|
|
89
|
+
| `.subscribe(callback)` | Subscribe to changes (RxJS Observable) |
|
|
90
|
+
| `.pipe(operators...)` | Chain RxJS operators |
|
|
91
|
+
| `.subscribeOnce(callback)` | Subscribe to a single emission, then auto-unsubscribe |
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
// Primitives
|
|
95
|
+
store.count.get(); // 0
|
|
96
|
+
store.count.set(5); // Updates to 5
|
|
97
|
+
|
|
98
|
+
// Objects
|
|
99
|
+
store.user.get(); // { name: "Alice", age: 30 }
|
|
100
|
+
store.user.name.get(); // "Alice"
|
|
101
|
+
store.user.name.set("Bob");
|
|
102
|
+
|
|
103
|
+
// Subscribe at any level
|
|
104
|
+
store.user.name.subscribe(name => console.log(name));
|
|
105
|
+
store.user.subscribe(user => console.log(user));
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Batched Updates with `.update()`
|
|
109
|
+
|
|
110
|
+
Batch multiple changes into a single emission:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
// Without batching - emits twice
|
|
114
|
+
store.user.name.set("Bob");
|
|
115
|
+
store.user.age.set(31);
|
|
116
|
+
// Subscribers see intermediate state
|
|
117
|
+
|
|
118
|
+
// With batching - emits once
|
|
119
|
+
store.user.update(user => {
|
|
120
|
+
user.name.set("Bob");
|
|
121
|
+
user.age.set(31);
|
|
122
|
+
});
|
|
123
|
+
// Subscribers only see final state
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Arrays
|
|
127
|
+
|
|
128
|
+
Arrays have additional methods:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
const store = state({
|
|
132
|
+
items: [
|
|
133
|
+
{ id: 1, name: "First" },
|
|
134
|
+
{ id: 2, name: "Second" },
|
|
135
|
+
],
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Access by index
|
|
139
|
+
store.items.at(0)?.name.get(); // "First"
|
|
140
|
+
store.items.at(0)?.name.set("Updated");
|
|
141
|
+
|
|
142
|
+
// Array methods
|
|
143
|
+
store.items.push({ id: 3, name: "Third" }); // Returns new length
|
|
144
|
+
store.items.pop(); // Returns removed item
|
|
145
|
+
store.items.length.get(); // Current length
|
|
146
|
+
|
|
147
|
+
// Observable length
|
|
148
|
+
store.items.length.subscribe(len => console.log("Length:", len));
|
|
149
|
+
|
|
150
|
+
// Non-reactive iteration
|
|
151
|
+
store.items.map((item, i) => item.name);
|
|
152
|
+
store.items.filter(item => item.id > 1);
|
|
153
|
+
|
|
154
|
+
// Batched array updates
|
|
155
|
+
store.items.update(items => {
|
|
156
|
+
items.at(0)?.name.set("Modified");
|
|
157
|
+
items.push({ id: 4, name: "New" });
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### `array(value, options?)` - Array with Distinct
|
|
162
|
+
|
|
163
|
+
Control array emission deduplication:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { state, array } from "@montra-interactive/deepstate";
|
|
167
|
+
|
|
168
|
+
const store = state({
|
|
169
|
+
// No deduplication (default)
|
|
170
|
+
items: [1, 2, 3],
|
|
171
|
+
|
|
172
|
+
// Reference equality per element
|
|
173
|
+
tags: array(["a", "b"], { distinct: "shallow" }),
|
|
174
|
+
|
|
175
|
+
// JSON comparison (deep equality)
|
|
176
|
+
settings: array([{ theme: "dark" }], { distinct: "deep" }),
|
|
177
|
+
|
|
178
|
+
// Custom comparator
|
|
179
|
+
custom: array([1, 2, 3], {
|
|
180
|
+
distinct: (a, b) => a.length === b.length
|
|
181
|
+
}),
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Distinct Options:**
|
|
186
|
+
|
|
187
|
+
| Value | Description |
|
|
188
|
+
|-------|-------------|
|
|
189
|
+
| `false` | No deduplication (default) |
|
|
190
|
+
| `"shallow"` | Reference equality: `a[i] === b[i]` |
|
|
191
|
+
| `"deep"` | JSON comparison: `JSON.stringify(a) === JSON.stringify(b)` |
|
|
192
|
+
| `(a, b) => boolean` | Custom comparator function |
|
|
193
|
+
|
|
194
|
+
### `nullable(value)` - Nullable Objects
|
|
195
|
+
|
|
196
|
+
For properties that can be `null` or an object:
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
import { state, nullable } from "@montra-interactive/deepstate";
|
|
200
|
+
|
|
201
|
+
const store = state({
|
|
202
|
+
// Start as null, can become object
|
|
203
|
+
user: nullable<{ name: string; age: number }>(null),
|
|
204
|
+
|
|
205
|
+
// Start as object, can become null
|
|
206
|
+
profile: nullable({ bio: "Hello", avatar: "url" }),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Deep subscription works even when null!
|
|
210
|
+
store.user.name.subscribe(name => {
|
|
211
|
+
console.log(name); // undefined when user is null, value when set
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Transitions
|
|
215
|
+
store.user.set({ name: "Alice", age: 30 }); // Now has value
|
|
216
|
+
store.user.name.set("Bob"); // Update nested
|
|
217
|
+
store.user.set(null); // Back to null
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### `select(...observables)` - Combine Observables
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
import { select } from "@montra-interactive/deepstate";
|
|
224
|
+
|
|
225
|
+
// Array form - returns tuple
|
|
226
|
+
select(store.user.name, store.count).subscribe(([name, count]) => {
|
|
227
|
+
console.log(`${name}: ${count}`);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Object form - returns object
|
|
231
|
+
select({
|
|
232
|
+
name: store.user.name,
|
|
233
|
+
count: store.count,
|
|
234
|
+
}).subscribe(({ name, count }) => {
|
|
235
|
+
console.log(`${name}: ${count}`);
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### `selectFromEach(arrayNode, selector)` - Select from Array Items
|
|
240
|
+
|
|
241
|
+
Derive values from each array item with precise change detection:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
import { selectFromEach } from "@montra-interactive/deepstate";
|
|
245
|
+
|
|
246
|
+
const store = state({
|
|
247
|
+
items: [
|
|
248
|
+
{ name: "A", price: 10, qty: 2 },
|
|
249
|
+
{ name: "B", price: 20, qty: 1 },
|
|
250
|
+
],
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Select single property from each item
|
|
254
|
+
selectFromEach(store.items, item => item.price).subscribe(prices => {
|
|
255
|
+
console.log(prices); // [10, 20]
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Derive computed values
|
|
259
|
+
selectFromEach(store.items, item => item.price * item.qty).subscribe(totals => {
|
|
260
|
+
console.log(totals); // [20, 20]
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Only emits when selected values change
|
|
264
|
+
store.items.at(0)?.name.set("Changed"); // No emission (name wasn't selected)
|
|
265
|
+
store.items.at(0)?.price.set(15); // Emits [15, 20]
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## RxJS Integration
|
|
269
|
+
|
|
270
|
+
Every node is a full RxJS Observable:
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
import { debounceTime, filter, map } from "rxjs/operators";
|
|
274
|
+
|
|
275
|
+
store.user.name
|
|
276
|
+
.pipe(
|
|
277
|
+
debounceTime(300),
|
|
278
|
+
filter(name => name.length > 0),
|
|
279
|
+
map(name => name.toUpperCase())
|
|
280
|
+
)
|
|
281
|
+
.subscribe(name => console.log(name));
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## TypeScript
|
|
285
|
+
|
|
286
|
+
Full type inference from your initial state:
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
const store = state({
|
|
290
|
+
user: { name: "Alice", age: 30 },
|
|
291
|
+
items: [{ id: 1 }],
|
|
292
|
+
selectedId: null as string | null,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
store.user.name.get(); // string
|
|
296
|
+
store.user.age.get(); // number
|
|
297
|
+
store.items.at(0)?.id; // RxLeaf<number> | undefined
|
|
298
|
+
store.selectedId.get(); // string | null
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Type Exports
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
import type { RxState, Draft, DeepReadonly } from "@montra-interactive/deepstate";
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
| Type | Description |
|
|
308
|
+
|------|-------------|
|
|
309
|
+
| `RxState<T>` | The reactive state type returned by `state()` |
|
|
310
|
+
| `Draft<T>` | Type alias for values in update callbacks |
|
|
311
|
+
| `DeepReadonly<T>` | Deep readonly type for returned values |
|
|
312
|
+
|
|
313
|
+
## Architecture
|
|
314
|
+
|
|
315
|
+
deepstate uses a **nested BehaviorSubject architecture**:
|
|
316
|
+
|
|
317
|
+
- **Primitives**: Each has its own `BehaviorSubject`
|
|
318
|
+
- **Objects**: Derived from `combineLatest(children)`
|
|
319
|
+
- **Arrays**: `BehaviorSubject<T[]>` with child projections
|
|
320
|
+
|
|
321
|
+
This gives **O(depth) performance**: updating `store.a.b.c` only notifies `c`, `b`, `a`, and the root - never siblings like `store.x.y.z`.
|
|
322
|
+
|
|
323
|
+
## React Integration
|
|
324
|
+
|
|
325
|
+
See [@montra-interactive/deepstate-react](https://www.npmjs.com/package/@montra-interactive/deepstate-react) for React hooks:
|
|
326
|
+
|
|
327
|
+
```tsx
|
|
328
|
+
import { useSelect, usePipeSelect } from "@montra-interactive/deepstate-react";
|
|
329
|
+
|
|
330
|
+
function UserName() {
|
|
331
|
+
const name = useSelect(store.user.name);
|
|
332
|
+
return <span>{name}</span>;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function DebouncedSearch() {
|
|
336
|
+
const query = usePipeSelect(store.search.pipe(debounceTime(300)));
|
|
337
|
+
return <input value={query ?? ""} />;
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## License
|
|
342
|
+
|
|
343
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@montra-interactive/deepstate",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Proxy-based reactive state management with RxJS. Deep nested state observation with full TypeScript support.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"state",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
],
|
|
17
17
|
"author": "Ronnie Magatti",
|
|
18
18
|
"license": "MIT",
|
|
19
|
+
"homepage": "https://github.com/Montra-Interactive/deepstate/tree/main/packages/core",
|
|
19
20
|
"repository": {
|
|
20
21
|
"type": "git",
|
|
21
22
|
"url": "https://github.com/Montra-Interactive/deepstate",
|