@montra-interactive/deepstate 0.3.4 → 0.4.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.
package/README.md CHANGED
@@ -191,7 +191,7 @@ const store = state({
191
191
  | `"deep"` | JSON comparison: `JSON.stringify(a) === JSON.stringify(b)` |
192
192
  | `(a, b) => boolean` | Custom comparator function |
193
193
 
194
- ### `nullable(value)` - Nullable Objects
194
+ ### `nullable(value, options?)` - Nullable Objects
195
195
 
196
196
  For properties that can be `null` or an object:
197
197
 
@@ -204,6 +204,9 @@ const store = state({
204
204
 
205
205
  // Start as object, can become null
206
206
  profile: nullable({ bio: "Hello", avatar: "url" }),
207
+
208
+ // With distinct option to control emission deduplication
209
+ settings: nullable({ theme: "dark" }, { distinct: "shallow" }),
207
210
  });
208
211
 
209
212
  // Deep subscription works even when null!
@@ -217,6 +220,17 @@ store.user.name.set("Bob"); // Update nested
217
220
  store.user.set(null); // Back to null
218
221
  ```
219
222
 
223
+ **Distinct Options:**
224
+
225
+ | Value | Description |
226
+ |-------|-------------|
227
+ | `false` | No deduplication (always emits on set) |
228
+ | `"shallow"` | Shallow key-by-key `===` comparison |
229
+ | `"deep"` | JSON comparison: `JSON.stringify(a) === JSON.stringify(b)` (same as default) |
230
+ | `(a, b) => boolean` | Custom comparator; `a`/`b` may be `null` |
231
+
232
+ Without options, nullable nodes use `JSON.stringify` deduplication by default.
233
+
220
234
  ### `select(...observables)` - Combine Observables
221
235
 
222
236
  ```ts
@@ -170,6 +170,19 @@ export interface StateOptions {
170
170
  name?: string;
171
171
  }
172
172
  export declare function state<T extends object>(initialState: T, options?: StateOptions): RxState<T>;
173
+ /** Comparison mode for nullable object distinct checking */
174
+ export type NullableDistinct<T> = false | 'shallow' | 'deep' | ((a: T | null, b: T | null) => boolean);
175
+ /** Options for the nullable() helper */
176
+ export interface NullableOptions<T> {
177
+ /**
178
+ * How to compare nullable object values to prevent duplicate emissions.
179
+ * - false: No deduplication (always emits on set)
180
+ * - 'shallow': Shallow key-by-key === comparison
181
+ * - 'deep': JSON.stringify comparison (default behavior without options)
182
+ * - function: Custom comparator (a, b) => boolean where a/b may be null
183
+ */
184
+ distinct?: NullableDistinct<T>;
185
+ }
173
186
  /**
174
187
  * Marks a value as nullable, allowing it to transition between null and object.
175
188
  * Use this when you want to start with an object value but later set it to null.
@@ -180,6 +193,8 @@ export declare function state<T extends object>(initialState: T, options?: State
180
193
  * user: nullable({ name: "Alice", age: 30 }),
181
194
  * // Can start with null and later be set to object
182
195
  * profile: nullable<{ bio: string }>(null),
196
+ * // With distinct option to control emission deduplication
197
+ * settings: nullable({ theme: 'dark', lang: 'en' }, { distinct: 'shallow' }),
183
198
  * });
184
199
  *
185
200
  * // Use ?. on the nullable property, then access children directly
@@ -187,7 +202,7 @@ export declare function state<T extends object>(initialState: T, options?: State
187
202
  * store.user?.set({ name: "Bob", age: 25 }); // Works!
188
203
  * store.user?.name.set("Charlie"); // After ?. on user, children are directly accessible
189
204
  */
190
- export declare function nullable<T extends object>(value: T | null): T | null;
205
+ export declare function nullable<T extends object>(value: T | null, options?: NullableOptions<T>): T | null;
191
206
  /** Comparison mode for array distinct checking */
192
207
  export type ArrayDistinct<T> = false | 'shallow' | 'deep' | ((a: T[], b: T[]) => boolean);
193
208
  /** Options for the array() helper */
@@ -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
@@ -11,6 +11,6 @@
11
11
  * - select() - Combine multiple observables
12
12
  * - selectFromEach() - Select from each array item with precise change detection
13
13
  */
14
- export { state, nullable, array, type RxState, type Draft, type StateOptions, type ArrayOptions, type ArrayDistinct } from "./deepstate";
14
+ export { state, nullable, array, type RxState, type Draft, type StateOptions, type ArrayOptions, type ArrayDistinct, type NullableOptions, type NullableDistinct } from "./deepstate";
15
15
  export { select, selectFromEach } from "./helpers";
16
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
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"}
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,KAAK,eAAe,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACtL,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC"}
package/dist/index.js CHANGED
@@ -238,7 +238,7 @@ function createArrayNode(value, comparator) {
238
238
  };
239
239
  }
240
240
  var NULLABLE_NODE = Symbol("nullableNode");
241
- function createNullableObjectNode(initialValue) {
241
+ function createNullableObjectNode(initialValue, comparator) {
242
242
  const subject$ = new BehaviorSubject(initialValue);
243
243
  let children = null;
244
244
  const pendingChildren = new Map;
@@ -264,7 +264,7 @@ function createNullableObjectNode(initialValue) {
264
264
  }
265
265
  return result;
266
266
  };
267
- const $ = combineLatest2([subject$, lock$]).pipe(filter(([_, unlocked]) => unlocked), map2(([value, _]) => {
267
+ const baseLocked$ = combineLatest2([subject$, lock$]).pipe(filter(([_, unlocked]) => unlocked), map2(([value, _]) => {
268
268
  if (value === null || value === undefined || !children) {
269
269
  return value;
270
270
  }
@@ -273,13 +273,8 @@ function createNullableObjectNode(initialValue) {
273
273
  result[key] = child.get();
274
274
  }
275
275
  return result;
276
- }), distinctUntilChanged2((a, b) => {
277
- if (a === null || a === undefined)
278
- return a === b;
279
- if (b === null || b === undefined)
280
- return false;
281
- return JSON.stringify(a) === JSON.stringify(b);
282
- }), shareReplay(1));
276
+ }));
277
+ const $ = (comparator === undefined ? baseLocked$.pipe(distinctUntilChanged2(nullableDeepComparator)) : comparator ? baseLocked$.pipe(distinctUntilChanged2(comparator)) : baseLocked$).pipe(shareReplay(1));
283
278
  $.subscribe();
284
279
  const nodeState = { children };
285
280
  const updateChildrenRef = () => {
@@ -569,8 +564,11 @@ function findCircularReference(obj, currentPath = "root", seen = new Map) {
569
564
  }
570
565
  function createNodeForValue(value, maybeNullable = false) {
571
566
  if (isNullableMarked(value)) {
567
+ const options = getNullableMarkerOptions(value);
568
+ const comparator = getNullableComparator(options);
572
569
  delete value[NULLABLE_MARKER];
573
- return createNullableObjectNode(value);
570
+ const initialValue = Object.keys(value).length === 0 ? null : value;
571
+ return createNullableObjectNode(initialValue, comparator);
574
572
  }
575
573
  if (value === null || value === undefined) {
576
574
  if (maybeNullable) {
@@ -803,15 +801,59 @@ function state(initialState, options) {
803
801
  return wrapWithProxy(node, "", debugLog);
804
802
  }
805
803
  var NULLABLE_MARKER = Symbol("nullable");
806
- function nullable(value) {
804
+ function nullableDeepComparator(a, b) {
805
+ if (a === null || a === undefined)
806
+ return a === b;
807
+ if (b === null || b === undefined)
808
+ return false;
809
+ return JSON.stringify(a) === JSON.stringify(b);
810
+ }
811
+ function getNullableComparator(options) {
812
+ if (options.distinct === undefined)
813
+ return;
814
+ if (options.distinct === false)
815
+ return false;
816
+ if (options.distinct === "shallow") {
817
+ return (a, b) => {
818
+ if (a === null || a === undefined)
819
+ return a === b;
820
+ if (b === null || b === undefined)
821
+ return false;
822
+ if (typeof a !== "object" || typeof b !== "object")
823
+ return a === b;
824
+ const aKeys = Object.keys(a);
825
+ const bKeys = Object.keys(b);
826
+ if (aKeys.length !== bKeys.length)
827
+ return false;
828
+ return aKeys.every((key) => a[key] === b[key]);
829
+ };
830
+ }
831
+ if (options.distinct === "deep") {
832
+ return nullableDeepComparator;
833
+ }
834
+ const customFn = options.distinct;
835
+ return (a, b) => customFn(a, b);
836
+ }
837
+ function nullable(value, options = {}) {
807
838
  if (value === null) {
839
+ if (Object.keys(options).length > 0) {
840
+ const marker = Object.create(null);
841
+ marker[NULLABLE_MARKER] = options;
842
+ return marker;
843
+ }
808
844
  return null;
809
845
  }
810
- return Object.assign(value, { [NULLABLE_MARKER]: true });
846
+ return Object.assign(value, { [NULLABLE_MARKER]: options });
811
847
  }
812
848
  function isNullableMarked(value) {
813
849
  return value !== null && typeof value === "object" && NULLABLE_MARKER in value;
814
850
  }
851
+ function getNullableMarkerOptions(value) {
852
+ if (value !== null && typeof value === "object" && NULLABLE_MARKER in value) {
853
+ return value[NULLABLE_MARKER];
854
+ }
855
+ return {};
856
+ }
815
857
  var ARRAY_MARKER = Symbol("array");
816
858
  function array(value, options = {}) {
817
859
  const marked = [...value];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@montra-interactive/deepstate",
3
- "version": "0.3.4",
3
+ "version": "0.4.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
@@ -567,7 +567,8 @@ interface NullableNodeCore<T> extends NodeCore<T> {
567
567
  * When value is set to object: children are created lazily from the object's keys
568
568
  */
569
569
  function createNullableObjectNode<T>(
570
- initialValue: T
570
+ initialValue: T,
571
+ comparator?: false | ((a: T, b: T) => boolean),
571
572
  ): NullableNodeCore<T> {
572
573
  // Subject holds the raw value (null or object)
573
574
  const subject$ = new BehaviorSubject<T>(initialValue);
@@ -612,7 +613,7 @@ function createNullableObjectNode<T>(
612
613
  };
613
614
 
614
615
  // Observable that emits the current value, respecting lock
615
- const $ = combineLatest([subject$, lock$]).pipe(
616
+ const baseLocked$ = combineLatest([subject$, lock$]).pipe(
616
617
  filter(([_, unlocked]) => unlocked),
617
618
  map(([value, _]) => {
618
619
  if (value === null || value === undefined || !children) {
@@ -625,11 +626,15 @@ function createNullableObjectNode<T>(
625
626
  }
626
627
  return result as T;
627
628
  }),
628
- distinctUntilChanged((a, b) => {
629
- if (a === null || a === undefined) return a === b;
630
- if (b === null || b === undefined) return false;
631
- return JSON.stringify(a) === JSON.stringify(b);
632
- }),
629
+ );
630
+
631
+ // Apply distinct comparison: use provided comparator, fall back to default, or skip entirely
632
+ const $ = (comparator === undefined
633
+ ? baseLocked$.pipe(distinctUntilChanged(nullableDeepComparator))
634
+ : comparator
635
+ ? baseLocked$.pipe(distinctUntilChanged(comparator))
636
+ : baseLocked$
637
+ ).pipe(
633
638
  shareReplay(1)
634
639
  );
635
640
  $.subscribe(); // Keep hot
@@ -1082,9 +1087,16 @@ function findCircularReference(
1082
1087
  function createNodeForValue<T>(value: T, maybeNullable: boolean = false): NodeCore<T> {
1083
1088
  // Check for nullable marker (from nullable() helper)
1084
1089
  if (isNullableMarked(value)) {
1090
+ const options = getNullableMarkerOptions(value);
1091
+ const comparator = getNullableComparator(options);
1085
1092
  // Remove the marker before creating the node
1086
1093
  delete (value as Record<symbol, unknown>)[NULLABLE_MARKER];
1087
- return createNullableObjectNode(value) as NodeCore<T>;
1094
+ // If the marked value was a sentinel for nullable(null, options), it has no
1095
+ // own enumerable keys after stripping the marker — pass null as initial value.
1096
+ const initialValue = Object.keys(value as object).length === 0
1097
+ ? null as unknown as T
1098
+ : value;
1099
+ return createNullableObjectNode(initialValue, comparator) as NodeCore<T>;
1088
1100
  }
1089
1101
 
1090
1102
  if (value === null || value === undefined) {
@@ -1409,6 +1421,64 @@ export function state<T extends object>(initialState: T, options?: StateOptions)
1409
1421
  // Symbol to mark a value as nullable
1410
1422
  const NULLABLE_MARKER = Symbol("nullable");
1411
1423
 
1424
+ /** Comparison mode for nullable object distinct checking */
1425
+ export type NullableDistinct<T> =
1426
+ | false // No deduplication (always emits on set)
1427
+ | 'shallow' // Shallow key-by-key === comparison
1428
+ | 'deep' // JSON.stringify comparison (same as default behavior without options)
1429
+ | ((a: T | null, b: T | null) => boolean); // Custom comparator
1430
+
1431
+ /** Options for the nullable() helper */
1432
+ export interface NullableOptions<T> {
1433
+ /**
1434
+ * How to compare nullable object values to prevent duplicate emissions.
1435
+ * - false: No deduplication (always emits on set)
1436
+ * - 'shallow': Shallow key-by-key === comparison
1437
+ * - 'deep': JSON.stringify comparison (default behavior without options)
1438
+ * - function: Custom comparator (a, b) => boolean where a/b may be null
1439
+ */
1440
+ distinct?: NullableDistinct<T>;
1441
+ }
1442
+
1443
+ // Null-safe JSON.stringify comparator, shared between default behavior and distinct: 'deep'
1444
+ function nullableDeepComparator<T>(a: T, b: T): boolean {
1445
+ if (a === null || a === undefined) return a === b;
1446
+ if (b === null || b === undefined) return false;
1447
+ return JSON.stringify(a) === JSON.stringify(b);
1448
+ }
1449
+
1450
+ // Get the distinct comparator function from nullable options
1451
+ // Returns undefined to use default (JSON.stringify), false to disable, or a function for custom comparison
1452
+ function getNullableComparator<T>(options: NullableOptions<T>): false | ((a: T, b: T) => boolean) | undefined {
1453
+ if (options.distinct === undefined) return undefined;
1454
+ if (options.distinct === false) return false;
1455
+
1456
+ if (options.distinct === 'shallow') {
1457
+ return (a, b) => {
1458
+ if (a === null || a === undefined) return a === b;
1459
+ if (b === null || b === undefined) return false;
1460
+ if (typeof a !== 'object' || typeof b !== 'object') return a === b;
1461
+ const aKeys = Object.keys(a);
1462
+ const bKeys = Object.keys(b);
1463
+ if (aKeys.length !== bKeys.length) return false;
1464
+ return aKeys.every(key =>
1465
+ (a as Record<string, unknown>)[key] === (b as Record<string, unknown>)[key]
1466
+ );
1467
+ };
1468
+ }
1469
+
1470
+ if (options.distinct === 'deep') {
1471
+ return nullableDeepComparator;
1472
+ }
1473
+
1474
+ // Custom function — wrap to match the (a: T, b: T) => boolean signature
1475
+ const customFn = options.distinct;
1476
+ return (a, b) => customFn(a as T | null, b as T | null);
1477
+ }
1478
+
1479
+ interface MarkedNullable<T> {
1480
+ [NULLABLE_MARKER]: NullableOptions<T>;
1481
+ }
1412
1482
 
1413
1483
  /**
1414
1484
  * Marks a value as nullable, allowing it to transition between null and object.
@@ -1420,6 +1490,8 @@ const NULLABLE_MARKER = Symbol("nullable");
1420
1490
  * user: nullable({ name: "Alice", age: 30 }),
1421
1491
  * // Can start with null and later be set to object
1422
1492
  * profile: nullable<{ bio: string }>(null),
1493
+ * // With distinct option to control emission deduplication
1494
+ * settings: nullable({ theme: 'dark', lang: 'en' }, { distinct: 'shallow' }),
1423
1495
  * });
1424
1496
  *
1425
1497
  * // Use ?. on the nullable property, then access children directly
@@ -1427,12 +1499,24 @@ const NULLABLE_MARKER = Symbol("nullable");
1427
1499
  * store.user?.set({ name: "Bob", age: 25 }); // Works!
1428
1500
  * store.user?.name.set("Charlie"); // After ?. on user, children are directly accessible
1429
1501
  */
1430
- export function nullable<T extends object>(value: T | null): T | null {
1502
+ export function nullable<T extends object>(value: T | null, options: NullableOptions<T> = {}): T | null {
1431
1503
  if (value === null) {
1504
+ // For null values, we can't attach the marker to the value itself.
1505
+ // We store options on a sentinel object that createNodeForValue will check.
1506
+ if (Object.keys(options).length > 0) {
1507
+ // Store options in a module-level map keyed by a unique object
1508
+ // The caller will pass this null through state(), and createNodeForValue
1509
+ // will receive maybeNullable=true for null children of nullable parents.
1510
+ // For top-level nullable(null, options), we need a different approach:
1511
+ // return a special marker object that looks null-ish but carries options.
1512
+ const marker = Object.create(null) as MarkedNullable<T>;
1513
+ marker[NULLABLE_MARKER] = options;
1514
+ return marker as unknown as T | null;
1515
+ }
1432
1516
  return null;
1433
1517
  }
1434
1518
  // Mark the object so createNodeForValue knows to use NullableNodeCore
1435
- return Object.assign(value, { [NULLABLE_MARKER]: true }) as T | null;
1519
+ return Object.assign(value, { [NULLABLE_MARKER]: options }) as T | null;
1436
1520
  }
1437
1521
 
1438
1522
  // Check if a value was marked as nullable
@@ -1440,6 +1524,14 @@ function isNullableMarked<T>(value: T): boolean {
1440
1524
  return value !== null && typeof value === "object" && NULLABLE_MARKER in value;
1441
1525
  }
1442
1526
 
1527
+ // Extract options from a nullable-marked value
1528
+ function getNullableMarkerOptions<T>(value: T): NullableOptions<T> {
1529
+ if (value !== null && typeof value === "object" && NULLABLE_MARKER in value) {
1530
+ return (value as unknown as MarkedNullable<T>)[NULLABLE_MARKER];
1531
+ }
1532
+ return {};
1533
+ }
1534
+
1443
1535
  // Symbol to mark an array with distinct options
1444
1536
  const ARRAY_MARKER = Symbol("array");
1445
1537
 
package/src/index.ts CHANGED
@@ -12,5 +12,5 @@
12
12
  * - selectFromEach() - Select from each array item with precise change detection
13
13
  */
14
14
 
15
- export { state, nullable, array, type RxState, type Draft, type StateOptions, type ArrayOptions, type ArrayDistinct } from "./deepstate";
15
+ export { state, nullable, array, type RxState, type Draft, type StateOptions, type ArrayOptions, type ArrayDistinct, type NullableOptions, type NullableDistinct } from "./deepstate";
16
16
  export { select, selectFromEach } from "./helpers";