@montra-interactive/deepstate 0.1.1 → 0.2.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 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
@@ -191,5 +191,44 @@ export declare function state<T extends object>(initialState: T, options?: State
191
191
  * store.user?.name.set("Charlie"); // After ?. on user, children are directly accessible
192
192
  */
193
193
  export declare function nullable<T extends object>(value: T | null): T | null;
194
+ /** Comparison mode for array distinct checking */
195
+ export type ArrayDistinct<T> = false | 'shallow' | 'deep' | ((a: T[], b: T[]) => boolean);
196
+ /** Options for the array() helper */
197
+ export interface ArrayOptions<T> {
198
+ /**
199
+ * How to compare arrays to prevent duplicate emissions.
200
+ * - false: No deduplication (default, always emits on set)
201
+ * - 'shallow': Reference equality per element (a[i] === b[i])
202
+ * - 'deep': JSON.stringify comparison (expensive for large arrays)
203
+ * - function: Custom comparator (a, b) => boolean
204
+ */
205
+ distinct?: ArrayDistinct<T>;
206
+ }
207
+ /**
208
+ * Marks an array with options for how it should behave in state.
209
+ * Use this to enable deduplication (prevent emissions when setting same values).
210
+ *
211
+ * @example
212
+ * ```ts
213
+ * const store = state({
214
+ * // Default behavior - emits on every set
215
+ * items: [1, 2, 3],
216
+ *
217
+ * // Shallow comparison - only emits if elements differ by reference
218
+ * tags: array(['a', 'b'], { distinct: 'shallow' }),
219
+ *
220
+ * // Deep comparison - only emits if JSON representation differs
221
+ * settings: array([{ theme: 'dark' }], { distinct: 'deep' }),
222
+ *
223
+ * // Custom comparator - you define equality
224
+ * users: array([{ id: 1, name: 'Alice' }], {
225
+ * distinct: (a, b) =>
226
+ * a.length === b.length &&
227
+ * a.every((user, i) => user.id === b[i].id)
228
+ * }),
229
+ * });
230
+ * ```
231
+ */
232
+ export declare function array<T>(value: T[], options?: ArrayOptions<T>): T[];
194
233
  export {};
195
234
  //# sourceMappingURL=deepstate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"deepstate.d.ts","sourceRoot":"","sources":["../src/deepstate.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"deepstate.d.ts","sourceRoot":"","sources":["../src/deepstate.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -4,12 +4,13 @@
4
4
  * Core exports:
5
5
  * - state() - Create reactive state from plain objects
6
6
  * - nullable() - Mark a property as nullable (can transition between null and object)
7
+ * - array() - Mark an array with deduplication options
7
8
  * - RxState, Draft - Type exports
8
9
  *
9
10
  * Helper exports:
10
11
  * - select() - Combine multiple observables
11
12
  * - selectFromEach() - Select from each array item with precise change detection
12
13
  */
13
- export { state, nullable, type RxState, type Draft, type StateOptions } from "./deepstate";
14
+ export { state, nullable, array, type RxState, type Draft, type StateOptions, type ArrayOptions, type ArrayDistinct } from "./deepstate";
14
15
  export { select, selectFromEach } from "./helpers";
15
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3F,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AACzI,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC"}
package/dist/index.js CHANGED
@@ -164,7 +164,7 @@ function createObjectNode(value) {
164
164
  }
165
165
  };
166
166
  }
167
- function createArrayNode(value) {
167
+ function createArrayNode(value, comparator) {
168
168
  const subject$ = new BehaviorSubject([...value]);
169
169
  const childCache = new Map;
170
170
  const createChildProjection = (index) => {
@@ -188,7 +188,8 @@ function createArrayNode(value) {
188
188
  };
189
189
  };
190
190
  const lock$ = new BehaviorSubject(true);
191
- const locked$ = combineLatest2([subject$, lock$]).pipe(filter(([_, unlocked]) => unlocked), map2(([arr, _]) => arr), map2(deepFreeze), shareReplay(1));
191
+ const baseLocked$ = combineLatest2([subject$, lock$]).pipe(filter(([_, unlocked]) => unlocked), map2(([arr, _]) => arr));
192
+ const locked$ = (comparator ? baseLocked$.pipe(distinctUntilChanged2(comparator)) : baseLocked$).pipe(map2(deepFreeze), shareReplay(1));
192
193
  locked$.subscribe();
193
194
  const length$ = locked$.pipe(map2((arr) => arr.length), distinctUntilChanged2(), shareReplay(1));
194
195
  length$.subscribe();
@@ -562,6 +563,12 @@ function createNodeForValue(value, maybeNullable = false) {
562
563
  return createLeafNode(value);
563
564
  }
564
565
  if (Array.isArray(value)) {
566
+ if (isArrayMarked(value)) {
567
+ const options = value[ARRAY_MARKER];
568
+ const comparator = getArrayComparator(options);
569
+ delete value[ARRAY_MARKER];
570
+ return createArrayNode(value, comparator);
571
+ }
565
572
  return createArrayNode(value);
566
573
  }
567
574
  return createObjectNode(value);
@@ -782,9 +789,30 @@ function nullable(value) {
782
789
  function isNullableMarked(value) {
783
790
  return value !== null && typeof value === "object" && NULLABLE_MARKER in value;
784
791
  }
792
+ var ARRAY_MARKER = Symbol("array");
793
+ function array(value, options = {}) {
794
+ const marked = [...value];
795
+ marked[ARRAY_MARKER] = options;
796
+ return marked;
797
+ }
798
+ function isArrayMarked(value) {
799
+ return Array.isArray(value) && ARRAY_MARKER in value;
800
+ }
801
+ function getArrayComparator(options) {
802
+ if (!options.distinct)
803
+ return;
804
+ if (options.distinct === "shallow") {
805
+ return (a, b) => a.length === b.length && a.every((v, i) => v === b[i]);
806
+ }
807
+ if (options.distinct === "deep") {
808
+ return (a, b) => JSON.stringify(a) === JSON.stringify(b);
809
+ }
810
+ return options.distinct;
811
+ }
785
812
  export {
786
813
  state,
787
814
  selectFromEach,
788
815
  select,
789
- nullable
816
+ nullable,
817
+ array
790
818
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@montra-interactive/deepstate",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Proxy-based reactive state management with RxJS. Deep nested state observation with full TypeScript support.",
5
5
  "keywords": [
6
6
  "state",
package/src/deepstate.ts CHANGED
@@ -424,7 +424,10 @@ function createObjectNode<T extends object>(value: T): NodeCore<T> & {
424
424
  };
425
425
  }
426
426
 
427
- function createArrayNode<T>(value: T[]): NodeCore<T[]> & {
427
+ function createArrayNode<T>(
428
+ value: T[],
429
+ comparator?: (a: T[], b: T[]) => boolean
430
+ ): NodeCore<T[]> & {
428
431
  at(index: number): NodeCore<T> | undefined;
429
432
  childCache: Map<number, NodeCore<T>>;
430
433
  length$: Observable<number> & { get(): number };
@@ -478,10 +481,17 @@ function createArrayNode<T>(value: T[]): NodeCore<T[]> & {
478
481
  // Lock for batching updates - when false, emissions are filtered out
479
482
  const lock$ = new BehaviorSubject<boolean>(true);
480
483
 
481
- // Create observable that respects lock
482
- const locked$ = combineLatest([subject$, lock$]).pipe(
484
+ // Create observable that respects lock, with optional distinct comparison
485
+ const baseLocked$ = combineLatest([subject$, lock$]).pipe(
483
486
  filter(([_, unlocked]) => unlocked),
484
487
  map(([arr, _]) => arr),
488
+ );
489
+
490
+ // Apply distinct comparison if provided
491
+ const locked$ = (comparator
492
+ ? baseLocked$.pipe(distinctUntilChanged(comparator))
493
+ : baseLocked$
494
+ ).pipe(
485
495
  map(deepFreeze),
486
496
  shareReplay(1)
487
497
  );
@@ -1079,6 +1089,14 @@ function createNodeForValue<T>(value: T, maybeNullable: boolean = false): NodeCo
1079
1089
  return createLeafNode(value as Primitive) as NodeCore<T>;
1080
1090
  }
1081
1091
  if (Array.isArray(value)) {
1092
+ // Check if array was marked with options via array() helper
1093
+ if (isArrayMarked(value)) {
1094
+ const options = value[ARRAY_MARKER];
1095
+ const comparator = getArrayComparator(options);
1096
+ // Remove the marker before creating the node
1097
+ delete (value as Record<symbol, unknown>)[ARRAY_MARKER];
1098
+ return createArrayNode(value, comparator) as unknown as NodeCore<T>;
1099
+ }
1082
1100
  return createArrayNode(value) as unknown as NodeCore<T>;
1083
1101
  }
1084
1102
  return createObjectNode(value as object) as unknown as NodeCore<T>;
@@ -1401,3 +1419,81 @@ export function nullable<T extends object>(value: T | null): T | null {
1401
1419
  function isNullableMarked<T>(value: T): boolean {
1402
1420
  return value !== null && typeof value === "object" && NULLABLE_MARKER in value;
1403
1421
  }
1422
+
1423
+ // Symbol to mark an array with distinct options
1424
+ const ARRAY_MARKER = Symbol("array");
1425
+
1426
+ /** Comparison mode for array distinct checking */
1427
+ export type ArrayDistinct<T> =
1428
+ | false // No deduplication (default)
1429
+ | 'shallow' // Reference equality per element
1430
+ | 'deep' // JSON.stringify comparison
1431
+ | ((a: T[], b: T[]) => boolean); // Custom comparator
1432
+
1433
+ /** Options for the array() helper */
1434
+ export interface ArrayOptions<T> {
1435
+ /**
1436
+ * How to compare arrays to prevent duplicate emissions.
1437
+ * - false: No deduplication (default, always emits on set)
1438
+ * - 'shallow': Reference equality per element (a[i] === b[i])
1439
+ * - 'deep': JSON.stringify comparison (expensive for large arrays)
1440
+ * - function: Custom comparator (a, b) => boolean
1441
+ */
1442
+ distinct?: ArrayDistinct<T>;
1443
+ }
1444
+
1445
+ interface MarkedArray<T> extends Array<T> {
1446
+ [ARRAY_MARKER]: ArrayOptions<T>;
1447
+ }
1448
+
1449
+ /**
1450
+ * Marks an array with options for how it should behave in state.
1451
+ * Use this to enable deduplication (prevent emissions when setting same values).
1452
+ *
1453
+ * @example
1454
+ * ```ts
1455
+ * const store = state({
1456
+ * // Default behavior - emits on every set
1457
+ * items: [1, 2, 3],
1458
+ *
1459
+ * // Shallow comparison - only emits if elements differ by reference
1460
+ * tags: array(['a', 'b'], { distinct: 'shallow' }),
1461
+ *
1462
+ * // Deep comparison - only emits if JSON representation differs
1463
+ * settings: array([{ theme: 'dark' }], { distinct: 'deep' }),
1464
+ *
1465
+ * // Custom comparator - you define equality
1466
+ * users: array([{ id: 1, name: 'Alice' }], {
1467
+ * distinct: (a, b) =>
1468
+ * a.length === b.length &&
1469
+ * a.every((user, i) => user.id === b[i].id)
1470
+ * }),
1471
+ * });
1472
+ * ```
1473
+ */
1474
+ export function array<T>(value: T[], options: ArrayOptions<T> = {}): T[] {
1475
+ const marked = [...value] as MarkedArray<T>;
1476
+ marked[ARRAY_MARKER] = options;
1477
+ return marked;
1478
+ }
1479
+
1480
+ // Check if an array was marked with options
1481
+ function isArrayMarked<T>(value: unknown): value is MarkedArray<T> {
1482
+ return Array.isArray(value) && ARRAY_MARKER in value;
1483
+ }
1484
+
1485
+ // Get the distinct comparator function from array options
1486
+ function getArrayComparator<T>(options: ArrayOptions<T>): ((a: T[], b: T[]) => boolean) | undefined {
1487
+ if (!options.distinct) return undefined;
1488
+
1489
+ if (options.distinct === 'shallow') {
1490
+ return (a, b) => a.length === b.length && a.every((v, i) => v === b[i]);
1491
+ }
1492
+
1493
+ if (options.distinct === 'deep') {
1494
+ return (a, b) => JSON.stringify(a) === JSON.stringify(b);
1495
+ }
1496
+
1497
+ // Custom function
1498
+ return options.distinct;
1499
+ }
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * Core exports:
5
5
  * - state() - Create reactive state from plain objects
6
6
  * - nullable() - Mark a property as nullable (can transition between null and object)
7
+ * - array() - Mark an array with deduplication options
7
8
  * - RxState, Draft - Type exports
8
9
  *
9
10
  * Helper exports:
@@ -11,5 +12,5 @@
11
12
  * - selectFromEach() - Select from each array item with precise change detection
12
13
  */
13
14
 
14
- export { state, nullable, type RxState, type Draft, type StateOptions } from "./deepstate";
15
+ export { state, nullable, array, type RxState, type Draft, type StateOptions, type ArrayOptions, type ArrayDistinct } from "./deepstate";
15
16
  export { select, selectFromEach } from "./helpers";