@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.
Files changed (2) hide show
  1. package/README.md +343 -0
  2. 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.0",
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",