@montra-interactive/deepstate 0.1.0 → 0.2.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.
@@ -166,7 +166,13 @@ type RxNullableChildObject<T extends object> = Observable<DeepReadonly<T> | unde
166
166
  };
167
167
  type RxNodeFor<T> = IsNullableObject<T> extends true ? RxNullable<T> : [T] extends [Primitive] ? RxLeaf<T> : [T] extends [Array<infer U>] ? RxArray<U> : [T] extends [object] ? RxObject<T> : RxLeaf<T>;
168
168
  export type RxState<T extends object> = RxObject<T>;
169
- export declare function state<T extends object>(initialState: T): RxState<T>;
169
+ export interface StateOptions {
170
+ /** Enable debug logging for this store */
171
+ debug?: boolean;
172
+ /** Optional name for this store (used in debug logs) */
173
+ name?: string;
174
+ }
175
+ export declare function state<T extends object>(initialState: T, options?: StateOptions): RxState<T>;
170
176
  /**
171
177
  * Marks a value as nullable, allowing it to transition between null and object.
172
178
  * Use this when you want to start with an object value but later set it to null.
@@ -185,5 +191,44 @@ export declare function state<T extends object>(initialState: T): RxState<T>;
185
191
  * store.user?.name.set("Charlie"); // After ?. on user, children are directly accessible
186
192
  */
187
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[];
188
233
  export {};
189
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 } 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,MAAM,aAAa,CAAC;AACxE,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
@@ -53,6 +53,29 @@ import {
53
53
  filter
54
54
  } from "rxjs/operators";
55
55
  var distinctCallCount = 0;
56
+ function createDebugLog(ctx) {
57
+ return (path, action, oldValue, newValue) => {
58
+ if (!ctx.enabled)
59
+ return;
60
+ const prefix = ctx.storeName ? `[deepstate:${ctx.storeName}]` : "[deepstate]";
61
+ const formatValue = (v) => {
62
+ if (v === undefined)
63
+ return "undefined";
64
+ if (v === null)
65
+ return "null";
66
+ if (typeof v === "object") {
67
+ try {
68
+ const str = JSON.stringify(v);
69
+ return str.length > 50 ? str.slice(0, 50) + "..." : str;
70
+ } catch {
71
+ return "[circular]";
72
+ }
73
+ }
74
+ return String(v);
75
+ };
76
+ console.log(`${prefix} ${action} ${path}: ${formatValue(oldValue)} → ${formatValue(newValue)}`);
77
+ };
78
+ }
56
79
  function countedDistinctUntilChanged(compareFn) {
57
80
  return distinctUntilChanged2((a, b) => {
58
81
  distinctCallCount++;
@@ -141,7 +164,7 @@ function createObjectNode(value) {
141
164
  }
142
165
  };
143
166
  }
144
- function createArrayNode(value) {
167
+ function createArrayNode(value, comparator) {
145
168
  const subject$ = new BehaviorSubject([...value]);
146
169
  const childCache = new Map;
147
170
  const createChildProjection = (index) => {
@@ -165,7 +188,8 @@ function createArrayNode(value) {
165
188
  };
166
189
  };
167
190
  const lock$ = new BehaviorSubject(true);
168
- 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));
169
193
  locked$.subscribe();
170
194
  const length$ = locked$.pipe(map2((arr) => arr.length), distinctUntilChanged2(), shareReplay(1));
171
195
  length$.subscribe();
@@ -311,6 +335,8 @@ function createNullableObjectNode(initialValue) {
311
335
  }
312
336
  }
313
337
  subject$.next(value);
338
+ } else {
339
+ subject$.next(value);
314
340
  }
315
341
  },
316
342
  getChild: (key) => {
@@ -537,11 +563,22 @@ function createNodeForValue(value, maybeNullable = false) {
537
563
  return createLeafNode(value);
538
564
  }
539
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
+ }
540
572
  return createArrayNode(value);
541
573
  }
542
574
  return createObjectNode(value);
543
575
  }
544
- function wrapNullableWithProxy(node) {
576
+ function wrapNullableWithProxy(node, path = "", debugLog) {
577
+ const wrappedSet = (v) => {
578
+ const oldValue = node.get();
579
+ debugLog?.(path || "root", "set", oldValue, v);
580
+ node.set(v);
581
+ };
545
582
  const update = (callback) => {
546
583
  node.lock();
547
584
  try {
@@ -550,7 +587,8 @@ function wrapNullableWithProxy(node) {
550
587
  if (typeof prop === "string") {
551
588
  const child = node.getChild(prop);
552
589
  if (child) {
553
- return wrapWithProxy(child);
590
+ const childPath = path ? `${path}.${prop}` : prop;
591
+ return wrapWithProxy(child, childPath, debugLog);
554
592
  }
555
593
  }
556
594
  return;
@@ -573,7 +611,7 @@ function wrapNullableWithProxy(node) {
573
611
  if (prop === "get")
574
612
  return node.get;
575
613
  if (prop === "set")
576
- return node.set;
614
+ return wrappedSet;
577
615
  if (prop === "update")
578
616
  return update;
579
617
  if (prop === "subscribeOnce")
@@ -585,7 +623,8 @@ function wrapNullableWithProxy(node) {
585
623
  }
586
624
  if (typeof prop === "string") {
587
625
  const child = node.getOrCreateChild(prop);
588
- return wrapWithProxy(child);
626
+ const childPath = path ? `${path}.${prop}` : prop;
627
+ return wrapWithProxy(child, childPath, debugLog);
589
628
  }
590
629
  if (prop in target) {
591
630
  const val = target[prop];
@@ -614,15 +653,20 @@ function wrapNullableWithProxy(node) {
614
653
  });
615
654
  return proxy;
616
655
  }
617
- function wrapWithProxy(node) {
656
+ function wrapWithProxy(node, path = "", debugLog) {
618
657
  if (isNullableNode(node)) {
619
- return wrapNullableWithProxy(node);
658
+ return wrapNullableWithProxy(node, path, debugLog);
620
659
  }
621
660
  const value = node.get();
661
+ const wrappedSet = (v) => {
662
+ const oldValue = node.get();
663
+ debugLog?.(path || "root", "set", oldValue, v);
664
+ node.set(v);
665
+ };
622
666
  if (value === null || typeof value !== "object") {
623
667
  return Object.assign(node.$, {
624
668
  get: node.get,
625
- set: node.set,
669
+ set: wrappedSet,
626
670
  subscribe: node.$.subscribe.bind(node.$),
627
671
  pipe: node.$.pipe.bind(node.$),
628
672
  subscribeOnce: node.subscribeOnce,
@@ -633,7 +677,7 @@ function wrapWithProxy(node) {
633
677
  const arrayNode = node;
634
678
  const wrapped = Object.assign(node.$, {
635
679
  get: node.get,
636
- set: node.set,
680
+ set: wrappedSet,
637
681
  subscribe: node.$.subscribe.bind(node.$),
638
682
  pipe: node.$.pipe.bind(node.$),
639
683
  subscribeOnce: node.subscribeOnce,
@@ -641,7 +685,8 @@ function wrapWithProxy(node) {
641
685
  const child = arrayNode.at(index);
642
686
  if (!child)
643
687
  return;
644
- return wrapWithProxy(child);
688
+ const childPath = path ? `${path}[${index}]` : `[${index}]`;
689
+ return wrapWithProxy(child, childPath, debugLog);
645
690
  },
646
691
  length: arrayNode.length$,
647
692
  push: arrayNode.push,
@@ -674,7 +719,7 @@ function wrapWithProxy(node) {
674
719
  if (prop === "get")
675
720
  return node.get;
676
721
  if (prop === "set")
677
- return node.set;
722
+ return wrappedSet;
678
723
  if (prop === "update")
679
724
  return updateFn;
680
725
  if (prop === "subscribeOnce")
@@ -687,7 +732,8 @@ function wrapWithProxy(node) {
687
732
  if (objectNode.children && typeof prop === "string") {
688
733
  const child = objectNode.children.get(prop);
689
734
  if (child) {
690
- return wrapWithProxy(child);
735
+ const childPath = path ? `${path}.${prop}` : prop;
736
+ return wrapWithProxy(child, childPath, debugLog);
691
737
  }
692
738
  }
693
739
  if (prop in target) {
@@ -728,9 +774,10 @@ function wrapWithProxy(node) {
728
774
  }
729
775
  return proxy;
730
776
  }
731
- function state(initialState) {
777
+ function state(initialState, options) {
778
+ const debugLog = options?.debug ? createDebugLog({ enabled: true, storeName: options.name }) : undefined;
732
779
  const node = createObjectNode(initialState);
733
- return wrapWithProxy(node);
780
+ return wrapWithProxy(node, "", debugLog);
734
781
  }
735
782
  var NULLABLE_MARKER = Symbol("nullable");
736
783
  function nullable(value) {
@@ -742,9 +789,30 @@ function nullable(value) {
742
789
  function isNullableMarked(value) {
743
790
  return value !== null && typeof value === "object" && NULLABLE_MARKER in value;
744
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
+ }
745
812
  export {
746
813
  state,
747
814
  selectFromEach,
748
815
  select,
749
- nullable
816
+ nullable,
817
+ array
750
818
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@montra-interactive/deepstate",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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
@@ -29,6 +29,43 @@ export function resetDistinctCallCount() {
29
29
  distinctCallCount = 0;
30
30
  }
31
31
 
32
+ // =============================================================================
33
+ // Debug Context
34
+ // =============================================================================
35
+
36
+ interface DebugContext {
37
+ enabled: boolean;
38
+ storeName?: string;
39
+ }
40
+
41
+ function createDebugLog(ctx: DebugContext) {
42
+ return (path: string, action: string, oldValue: unknown, newValue: unknown) => {
43
+ if (!ctx.enabled) return;
44
+
45
+ const prefix = ctx.storeName
46
+ ? `[deepstate:${ctx.storeName}]`
47
+ : '[deepstate]';
48
+
49
+ const formatValue = (v: unknown): string => {
50
+ if (v === undefined) return 'undefined';
51
+ if (v === null) return 'null';
52
+ if (typeof v === 'object') {
53
+ try {
54
+ const str = JSON.stringify(v);
55
+ return str.length > 50 ? str.slice(0, 50) + '...' : str;
56
+ } catch {
57
+ return '[circular]';
58
+ }
59
+ }
60
+ return String(v);
61
+ };
62
+
63
+ console.log(`${prefix} ${action} ${path}: ${formatValue(oldValue)} → ${formatValue(newValue)}`);
64
+ };
65
+ }
66
+
67
+ type DebugLogFn = ReturnType<typeof createDebugLog>;
68
+
32
69
  // Wrap distinctUntilChanged to count calls
33
70
  function countedDistinctUntilChanged<T>(compareFn?: (a: T, b: T) => boolean) {
34
71
  return distinctUntilChanged<T>((a, b) => {
@@ -387,7 +424,10 @@ function createObjectNode<T extends object>(value: T): NodeCore<T> & {
387
424
  };
388
425
  }
389
426
 
390
- function createArrayNode<T>(value: T[]): NodeCore<T[]> & {
427
+ function createArrayNode<T>(
428
+ value: T[],
429
+ comparator?: (a: T[], b: T[]) => boolean
430
+ ): NodeCore<T[]> & {
391
431
  at(index: number): NodeCore<T> | undefined;
392
432
  childCache: Map<number, NodeCore<T>>;
393
433
  length$: Observable<number> & { get(): number };
@@ -441,10 +481,17 @@ function createArrayNode<T>(value: T[]): NodeCore<T[]> & {
441
481
  // Lock for batching updates - when false, emissions are filtered out
442
482
  const lock$ = new BehaviorSubject<boolean>(true);
443
483
 
444
- // Create observable that respects lock
445
- const locked$ = combineLatest([subject$, lock$]).pipe(
484
+ // Create observable that respects lock, with optional distinct comparison
485
+ const baseLocked$ = combineLatest([subject$, lock$]).pipe(
446
486
  filter(([_, unlocked]) => unlocked),
447
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(
448
495
  map(deepFreeze),
449
496
  shareReplay(1)
450
497
  );
@@ -682,6 +729,10 @@ function createNullableObjectNode<T>(
682
729
  // This maintains reactivity for subscribers to those keys
683
730
  }
684
731
  subject$.next(value);
732
+ } else {
733
+ // Setting to a primitive value (string, number, boolean, etc.)
734
+ // This handles cases like `string | null` where null was the initial value
735
+ subject$.next(value);
685
736
  }
686
737
  },
687
738
 
@@ -1038,6 +1089,14 @@ function createNodeForValue<T>(value: T, maybeNullable: boolean = false): NodeCo
1038
1089
  return createLeafNode(value as Primitive) as NodeCore<T>;
1039
1090
  }
1040
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
+ }
1041
1100
  return createArrayNode(value) as unknown as NodeCore<T>;
1042
1101
  }
1043
1102
  return createObjectNode(value as object) as unknown as NodeCore<T>;
@@ -1053,7 +1112,14 @@ function createNodeForValue<T>(value: T, maybeNullable: boolean = false): NodeCo
1053
1112
  * - Creates/returns wrapped children when value is non-null
1054
1113
  * - Provides update() for batched updates
1055
1114
  */
1056
- function wrapNullableWithProxy<T>(node: NullableNodeCore<T>): RxNullable<T> {
1115
+ function wrapNullableWithProxy<T>(node: NullableNodeCore<T>, path: string = '', debugLog?: DebugLogFn): RxNullable<T> {
1116
+ // Create a wrapped set function that logs
1117
+ const wrappedSet = (v: T) => {
1118
+ const oldValue = node.get();
1119
+ debugLog?.(path || 'root', 'set', oldValue, v);
1120
+ node.set(v);
1121
+ };
1122
+
1057
1123
  // Create update function
1058
1124
  const update = (callback: (draft: object) => void): T => {
1059
1125
  node.lock();
@@ -1064,7 +1130,8 @@ function wrapNullableWithProxy<T>(node: NullableNodeCore<T>): RxNullable<T> {
1064
1130
  if (typeof prop === "string") {
1065
1131
  const child = node.getChild(prop);
1066
1132
  if (child) {
1067
- return wrapWithProxy(child);
1133
+ const childPath = path ? `${path}.${prop}` : prop;
1134
+ return wrapWithProxy(child, childPath, debugLog);
1068
1135
  }
1069
1136
  }
1070
1137
  return undefined;
@@ -1086,7 +1153,7 @@ function wrapNullableWithProxy<T>(node: NullableNodeCore<T>): RxNullable<T> {
1086
1153
 
1087
1154
  // Node methods
1088
1155
  if (prop === "get") return node.get;
1089
- if (prop === "set") return node.set;
1156
+ if (prop === "set") return wrappedSet;
1090
1157
  if (prop === "update") return update;
1091
1158
  if (prop === "subscribeOnce") return node.subscribeOnce;
1092
1159
  if (prop === NODE) return node;
@@ -1100,7 +1167,8 @@ function wrapNullableWithProxy<T>(node: NullableNodeCore<T>): RxNullable<T> {
1100
1167
  // This means store.user.age.subscribe() works even when user is null
1101
1168
  if (typeof prop === "string") {
1102
1169
  const child = node.getOrCreateChild(prop);
1103
- return wrapWithProxy(child);
1170
+ const childPath = path ? `${path}.${prop}` : prop;
1171
+ return wrapWithProxy(child, childPath, debugLog);
1104
1172
  }
1105
1173
 
1106
1174
  // Fallback to observable properties
@@ -1138,19 +1206,26 @@ function wrapNullableWithProxy<T>(node: NullableNodeCore<T>): RxNullable<T> {
1138
1206
  return proxy as unknown as RxNullable<T>;
1139
1207
  }
1140
1208
 
1141
- function wrapWithProxy<T>(node: NodeCore<T>): RxNodeFor<T> {
1209
+ function wrapWithProxy<T>(node: NodeCore<T>, path: string = '', debugLog?: DebugLogFn): RxNodeFor<T> {
1142
1210
  // Check for nullable node first (before checking value, since value might be null)
1143
1211
  if (isNullableNode(node)) {
1144
- return wrapNullableWithProxy(node) as RxNodeFor<T>;
1212
+ return wrapNullableWithProxy(node, path, debugLog) as RxNodeFor<T>;
1145
1213
  }
1146
1214
 
1147
1215
  const value = node.get();
1216
+
1217
+ // Create a wrapped set function that logs
1218
+ const wrappedSet = (v: T) => {
1219
+ const oldValue = node.get();
1220
+ debugLog?.(path || 'root', 'set', oldValue, v);
1221
+ node.set(v);
1222
+ };
1148
1223
 
1149
1224
  // Primitive - just attach methods to observable
1150
1225
  if (value === null || typeof value !== "object") {
1151
1226
  return Object.assign(node.$, {
1152
1227
  get: node.get,
1153
- set: node.set,
1228
+ set: wrappedSet,
1154
1229
  subscribe: node.$.subscribe.bind(node.$),
1155
1230
  pipe: node.$.pipe.bind(node.$),
1156
1231
  subscribeOnce: node.subscribeOnce,
@@ -1175,14 +1250,15 @@ function wrapWithProxy<T>(node: NodeCore<T>): RxNodeFor<T> {
1175
1250
  // Create the wrapped result first so we can reference it in update
1176
1251
  const wrapped = Object.assign(node.$, {
1177
1252
  get: node.get,
1178
- set: node.set,
1253
+ set: wrappedSet,
1179
1254
  subscribe: node.$.subscribe.bind(node.$),
1180
1255
  pipe: node.$.pipe.bind(node.$),
1181
1256
  subscribeOnce: node.subscribeOnce,
1182
1257
  at: (index: number) => {
1183
1258
  const child = arrayNode.at(index);
1184
1259
  if (!child) return undefined;
1185
- return wrapWithProxy(child);
1260
+ const childPath = path ? `${path}[${index}]` : `[${index}]`;
1261
+ return wrapWithProxy(child, childPath, debugLog);
1186
1262
  },
1187
1263
  length: arrayNode.length$,
1188
1264
  push: arrayNode.push,
@@ -1223,7 +1299,7 @@ function wrapWithProxy<T>(node: NodeCore<T>): RxNodeFor<T> {
1223
1299
 
1224
1300
  // Node methods
1225
1301
  if (prop === "get") return node.get;
1226
- if (prop === "set") return node.set;
1302
+ if (prop === "set") return wrappedSet;
1227
1303
  if (prop === "update") return updateFn;
1228
1304
  if (prop === "subscribeOnce") return node.subscribeOnce;
1229
1305
  if (prop === NODE) return node;
@@ -1237,7 +1313,8 @@ function wrapWithProxy<T>(node: NodeCore<T>): RxNodeFor<T> {
1237
1313
  if (objectNode.children && typeof prop === "string") {
1238
1314
  const child = objectNode.children.get(prop);
1239
1315
  if (child) {
1240
- return wrapWithProxy(child);
1316
+ const childPath = path ? `${path}.${prop}` : prop;
1317
+ return wrapWithProxy(child, childPath, debugLog);
1241
1318
  }
1242
1319
  }
1243
1320
 
@@ -1292,9 +1369,21 @@ function wrapWithProxy<T>(node: NodeCore<T>): RxNodeFor<T> {
1292
1369
  // Public API
1293
1370
  // =============================================================================
1294
1371
 
1295
- export function state<T extends object>(initialState: T): RxState<T> {
1372
+ export interface StateOptions {
1373
+ /** Enable debug logging for this store */
1374
+ debug?: boolean;
1375
+ /** Optional name for this store (used in debug logs) */
1376
+ name?: string;
1377
+ }
1378
+
1379
+ export function state<T extends object>(initialState: T, options?: StateOptions): RxState<T> {
1380
+ // Create debug log function if debug is enabled
1381
+ const debugLog = options?.debug
1382
+ ? createDebugLog({ enabled: true, storeName: options.name })
1383
+ : undefined;
1384
+
1296
1385
  const node = createObjectNode(initialState);
1297
- return wrapWithProxy(node as NodeCore<T>) as RxState<T>;
1386
+ return wrapWithProxy(node as NodeCore<T>, '', debugLog) as RxState<T>;
1298
1387
  }
1299
1388
 
1300
1389
  // Symbol to mark a value as nullable
@@ -1330,3 +1419,81 @@ export function nullable<T extends object>(value: T | null): T | null {
1330
1419
  function isNullableMarked<T>(value: T): boolean {
1331
1420
  return value !== null && typeof value === "object" && NULLABLE_MARKER in value;
1332
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 } 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";