@montra-interactive/deepstate 0.3.0 → 0.3.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 +2 -3
- package/dist/deepstate.d.ts +24 -32
- package/dist/deepstate.d.ts.map +1 -1
- package/dist/helpers.d.ts +1 -1
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +10 -3
- package/dist/index.js +41 -57
- package/package.json +2 -2
- package/src/deepstate.ts +69 -112
- package/src/helpers.ts +16 -5
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Proxy-based reactive state management powered by RxJS. Each property is its own
|
|
|
9
9
|
- **Type-safe**: Full TypeScript support with inferred types
|
|
10
10
|
- **RxJS native**: Every node is an Observable - use `pipe()`, `combineLatest`, etc.
|
|
11
11
|
- **Batched updates**: Group multiple changes into a single emission
|
|
12
|
-
- **
|
|
12
|
+
- **Mutable snapshots**: Reads return plain values; use `.set()` to update state
|
|
13
13
|
- **Nullable objects**: First-class support for `T | null` properties with deep subscription
|
|
14
14
|
- **Debug mode**: Optional logging for development
|
|
15
15
|
|
|
@@ -301,14 +301,13 @@ store.selectedId.get(); // string | null
|
|
|
301
301
|
### Type Exports
|
|
302
302
|
|
|
303
303
|
```ts
|
|
304
|
-
import type { RxState, Draft
|
|
304
|
+
import type { RxState, Draft } from "@montra-interactive/deepstate";
|
|
305
305
|
```
|
|
306
306
|
|
|
307
307
|
| Type | Description |
|
|
308
308
|
|------|-------------|
|
|
309
309
|
| `RxState<T>` | The reactive state type returned by `state()` |
|
|
310
310
|
| `Draft<T>` | Type alias for values in update callbacks |
|
|
311
|
-
| `DeepReadonly<T>` | Deep readonly type for returned values |
|
|
312
311
|
|
|
313
312
|
## Architecture
|
|
314
313
|
|
package/dist/deepstate.d.ts
CHANGED
|
@@ -21,12 +21,9 @@ type IsNullish<T> = HasNull<T> extends true ? true : HasUndefined<T>;
|
|
|
21
21
|
type NonNullPartIsObject<T> = NonNullablePart<T> extends object ? NonNullablePart<T> extends Array<unknown> ? false : true : false;
|
|
22
22
|
type IsNullableObject<T> = IsNullish<T> extends true ? NonNullPartIsObject<T> : false;
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
* Used for return types of get() and subscribe() to prevent accidental mutations.
|
|
24
|
+
* Type alias for values returned by get() and subscribe().
|
|
26
25
|
*/
|
|
27
|
-
export type DeepReadonly<T> =
|
|
28
|
-
readonly [K in keyof T]: DeepReadonly<T[K]>;
|
|
29
|
-
} : T;
|
|
26
|
+
export type DeepReadonly<T> = T;
|
|
30
27
|
/**
|
|
31
28
|
* A mutable draft of state T for use in update callbacks.
|
|
32
29
|
*/
|
|
@@ -38,20 +35,20 @@ interface NodeCore<T> {
|
|
|
38
35
|
subscribeOnce?(callback: (value: T) => void): Subscription;
|
|
39
36
|
}
|
|
40
37
|
declare const NODE: unique symbol;
|
|
41
|
-
type RxLeaf<T> = Observable<
|
|
38
|
+
type RxLeaf<T> = Observable<T> & {
|
|
42
39
|
/** Get current value synchronously */
|
|
43
|
-
get():
|
|
40
|
+
get(): T;
|
|
44
41
|
/** Set value */
|
|
45
42
|
set(value: T): void;
|
|
46
43
|
/** Subscribe to a single emission, then automatically unsubscribe */
|
|
47
|
-
subscribeOnce(callback: (value:
|
|
44
|
+
subscribeOnce(callback: (value: T) => void): Subscription;
|
|
48
45
|
[NODE]: NodeCore<T>;
|
|
49
46
|
};
|
|
50
47
|
type RxObject<T extends object> = {
|
|
51
48
|
[K in keyof T]: RxNodeFor<T[K]>;
|
|
52
|
-
} & Observable<
|
|
49
|
+
} & Observable<T> & {
|
|
53
50
|
/** Get current value synchronously */
|
|
54
|
-
get():
|
|
51
|
+
get(): T;
|
|
55
52
|
/** Set value */
|
|
56
53
|
set(value: T): void;
|
|
57
54
|
/**
|
|
@@ -64,14 +61,14 @@ type RxObject<T extends object> = {
|
|
|
64
61
|
* draft.age.set(31);
|
|
65
62
|
* });
|
|
66
63
|
*/
|
|
67
|
-
update(callback: (draft: RxObject<T>) => void):
|
|
64
|
+
update(callback: (draft: RxObject<T>) => void): T;
|
|
68
65
|
/** Subscribe to a single emission, then automatically unsubscribe */
|
|
69
|
-
subscribeOnce(callback: (value:
|
|
66
|
+
subscribeOnce(callback: (value: T) => void): Subscription;
|
|
70
67
|
[NODE]: NodeCore<T>;
|
|
71
68
|
};
|
|
72
|
-
type RxArray<T> = Observable<
|
|
69
|
+
type RxArray<T> = Observable<T[]> & {
|
|
73
70
|
/** Get current value synchronously */
|
|
74
|
-
get():
|
|
71
|
+
get(): T[];
|
|
75
72
|
/** Set value */
|
|
76
73
|
set(value: T[]): void;
|
|
77
74
|
/**
|
|
@@ -84,9 +81,9 @@ type RxArray<T> = Observable<DeepReadonly<T[]>> & {
|
|
|
84
81
|
* draft.push({ id: 2, name: "New" });
|
|
85
82
|
* });
|
|
86
83
|
*/
|
|
87
|
-
update(callback: (draft: RxArray<T>) => void):
|
|
84
|
+
update(callback: (draft: RxArray<T>) => void): T[];
|
|
88
85
|
/** Subscribe to a single emission, then automatically unsubscribe */
|
|
89
|
-
subscribeOnce(callback: (value:
|
|
86
|
+
subscribeOnce(callback: (value: T[]) => void): Subscription;
|
|
90
87
|
/** Get reactive node for array element at index */
|
|
91
88
|
at(index: number): RxNodeFor<T> | undefined;
|
|
92
89
|
/** Get current length (also observable) */
|
|
@@ -96,11 +93,11 @@ type RxArray<T> = Observable<DeepReadonly<T[]>> & {
|
|
|
96
93
|
/** Push items and return new length */
|
|
97
94
|
push(...items: T[]): number;
|
|
98
95
|
/** Pop last item */
|
|
99
|
-
pop():
|
|
96
|
+
pop(): T | undefined;
|
|
100
97
|
/** Map over current values (non-reactive, use .subscribe for reactive) */
|
|
101
|
-
map<U>(fn: (item:
|
|
98
|
+
map<U>(fn: (item: T, index: number) => U): U[];
|
|
102
99
|
/** Filter current values */
|
|
103
|
-
filter(fn: (item:
|
|
100
|
+
filter(fn: (item: T, index: number) => boolean): T[];
|
|
104
101
|
[NODE]: NodeCore<T[]>;
|
|
105
102
|
};
|
|
106
103
|
/**
|
|
@@ -124,13 +121,13 @@ type RxArray<T> = Observable<DeepReadonly<T[]>> & {
|
|
|
124
121
|
* store.user.name.get(); // "Alice"
|
|
125
122
|
* store.user.name.set("Bob"); // Works!
|
|
126
123
|
*/
|
|
127
|
-
type RxNullable<T, TNonNull extends object = NonNullablePart<T> & object> = Observable<
|
|
124
|
+
type RxNullable<T, TNonNull extends object = NonNullablePart<T> & object> = Observable<T> & {
|
|
128
125
|
/** Get current value (may be null/undefined) */
|
|
129
|
-
get():
|
|
126
|
+
get(): T;
|
|
130
127
|
/** Set value (can be null/undefined or the full object) */
|
|
131
128
|
set(value: T): void;
|
|
132
129
|
/** Subscribe to a single emission, then automatically unsubscribe */
|
|
133
|
-
subscribeOnce(callback: (value:
|
|
130
|
+
subscribeOnce(callback: (value: T) => void): Subscription;
|
|
134
131
|
/**
|
|
135
132
|
* Update multiple properties in a single emission.
|
|
136
133
|
* @example
|
|
@@ -139,7 +136,7 @@ type RxNullable<T, TNonNull extends object = NonNullablePart<T> & object> = Obse
|
|
|
139
136
|
* user.age.set(31);
|
|
140
137
|
* });
|
|
141
138
|
*/
|
|
142
|
-
update(callback: (draft: RxObject<TNonNull>) => void):
|
|
139
|
+
update(callback: (draft: RxObject<TNonNull>) => void): T;
|
|
143
140
|
[NODE]: NodeCore<T>;
|
|
144
141
|
} & {
|
|
145
142
|
[K in keyof TNonNull]: RxNullableChild<TNonNull[K]>;
|
|
@@ -156,21 +153,16 @@ type RxNullableChild<T> = IsNullableObject<T> extends true ? RxNullable<T> : [T]
|
|
|
156
153
|
* The object itself might be undefined (if parent is null), but if present
|
|
157
154
|
* it has all the normal object methods and children.
|
|
158
155
|
*/
|
|
159
|
-
type RxNullableChildObject<T extends object> = Observable<
|
|
160
|
-
get():
|
|
156
|
+
type RxNullableChildObject<T extends object> = Observable<T | undefined> & {
|
|
157
|
+
get(): T | undefined;
|
|
161
158
|
set(value: T): void;
|
|
162
|
-
subscribeOnce(callback: (value:
|
|
159
|
+
subscribeOnce(callback: (value: T | undefined) => void): Subscription;
|
|
163
160
|
[NODE]: NodeCore<T | undefined>;
|
|
164
161
|
} & {
|
|
165
162
|
[K in keyof T]: RxNullableChild<T[K]>;
|
|
166
163
|
};
|
|
167
164
|
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
|
-
export type RxState<T extends object> = RxObject<T
|
|
169
|
-
/** Reset the store to its initial state */
|
|
170
|
-
reset(): void;
|
|
171
|
-
/** Get the initial state (frozen copy) */
|
|
172
|
-
getInitialState(): DeepReadonly<T>;
|
|
173
|
-
};
|
|
165
|
+
export type RxState<T extends object> = RxObject<T>;
|
|
174
166
|
export interface StateOptions {
|
|
175
167
|
/** Enable debug logging for this store */
|
|
176
168
|
debug?: boolean;
|
package/dist/deepstate.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deepstate.d.ts","sourceRoot":"","sources":["../src/deepstate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAmB,UAAU,EAAqB,YAAY,EAAE,MAAM,MAAM,CAAC;AAapF,eAAO,IAAI,iBAAiB,QAAI,CAAC;AACjC,wBAAgB,sBAAsB,SAErC;
|
|
1
|
+
{"version":3,"file":"deepstate.d.ts","sourceRoot":"","sources":["../src/deepstate.ts"],"names":[],"mappings":"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"}
|
package/dist/helpers.d.ts
CHANGED
|
@@ -56,6 +56,6 @@ export declare function select<T extends Record<string, Observable<unknown>>>(ob
|
|
|
56
56
|
* @param selector - A function that extracts/derives a value from each item
|
|
57
57
|
* @returns An Observable that emits an array of selected values
|
|
58
58
|
*/
|
|
59
|
-
export declare function selectFromEach<T, U>(arrayNode: Observable<
|
|
59
|
+
export declare function selectFromEach<T, U>(arrayNode: Observable<T[]>, selector: (item: T, index: number) => U): Observable<U[]>;
|
|
60
60
|
export {};
|
|
61
61
|
//# sourceMappingURL=helpers.d.ts.map
|
package/dist/helpers.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,UAAU,EAAiB,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,UAAU,EAAiB,MAAM,MAAM,CAAC;AA+CjD,KAAK,eAAe,CAAC,CAAC,IAAI,CAAC,SAAS,UAAU,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAGpE,KAAK,gBAAgB,CAAC,CAAC,SAAS,UAAU,CAAC,OAAO,CAAC,EAAE,IAAI;KACtD,CAAC,IAAI,MAAM,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CACtC,CAAC;AAGF,KAAK,sBAAsB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC,IAAI;KAC1E,CAAC,IAAI,MAAM,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CACtC,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,MAAM,CAAC,CAAC,SAAS,UAAU,CAAC,OAAO,CAAC,EAAE,EACpD,GAAG,WAAW,EAAE,CAAC,GAChB,UAAU,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC;AACnC,wBAAgB,MAAM,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC,EAClE,WAAW,EAAE,CAAC,GACb,UAAU,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAC;AAyBzC;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,CAAC,EACjC,SAAS,EAAE,UAAU,CAAC,CAAC,EAAE,CAAC,EAC1B,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,CAAC,GACtC,UAAU,CAAC,CAAC,EAAE,CAAC,CAOjB"}
|
package/dist/helpers.js
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
// src/helpers.ts
|
|
2
2
|
import { combineLatest } from "rxjs";
|
|
3
3
|
import { distinctUntilChanged, map } from "rxjs/operators";
|
|
4
|
-
function deepEqual(a, b) {
|
|
4
|
+
function deepEqual(a, b, seen = new WeakMap) {
|
|
5
5
|
if (a === b)
|
|
6
6
|
return true;
|
|
7
7
|
if (a === null || b === null)
|
|
8
8
|
return false;
|
|
9
9
|
if (typeof a !== "object" || typeof b !== "object")
|
|
10
10
|
return false;
|
|
11
|
+
const seenWithA = seen.get(a);
|
|
12
|
+
if (seenWithA?.has(b))
|
|
13
|
+
return true;
|
|
14
|
+
if (!seen.has(a)) {
|
|
15
|
+
seen.set(a, new WeakSet);
|
|
16
|
+
}
|
|
17
|
+
seen.get(a).add(b);
|
|
11
18
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
12
19
|
if (a.length !== b.length)
|
|
13
20
|
return false;
|
|
14
|
-
return a.every((item, i) => deepEqual(item, b[i]));
|
|
21
|
+
return a.every((item, i) => deepEqual(item, b[i], seen));
|
|
15
22
|
}
|
|
16
23
|
if (Array.isArray(a) !== Array.isArray(b))
|
|
17
24
|
return false;
|
|
@@ -19,7 +26,7 @@ function deepEqual(a, b) {
|
|
|
19
26
|
const keysB = Object.keys(b);
|
|
20
27
|
if (keysA.length !== keysB.length)
|
|
21
28
|
return false;
|
|
22
|
-
return keysA.every((key) => deepEqual(a[key], b[key]));
|
|
29
|
+
return keysA.every((key) => deepEqual(a[key], b[key], seen));
|
|
23
30
|
}
|
|
24
31
|
function isObservable(obj) {
|
|
25
32
|
return obj !== null && typeof obj === "object" && "subscribe" in obj && typeof obj.subscribe === "function";
|
package/dist/index.js
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
// src/helpers.ts
|
|
2
2
|
import { combineLatest } from "rxjs";
|
|
3
3
|
import { distinctUntilChanged, map } from "rxjs/operators";
|
|
4
|
-
function deepEqual(a, b) {
|
|
4
|
+
function deepEqual(a, b, seen = new WeakMap) {
|
|
5
5
|
if (a === b)
|
|
6
6
|
return true;
|
|
7
7
|
if (a === null || b === null)
|
|
8
8
|
return false;
|
|
9
9
|
if (typeof a !== "object" || typeof b !== "object")
|
|
10
10
|
return false;
|
|
11
|
+
const seenWithA = seen.get(a);
|
|
12
|
+
if (seenWithA?.has(b))
|
|
13
|
+
return true;
|
|
14
|
+
if (!seen.has(a)) {
|
|
15
|
+
seen.set(a, new WeakSet);
|
|
16
|
+
}
|
|
17
|
+
seen.get(a).add(b);
|
|
11
18
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
12
19
|
if (a.length !== b.length)
|
|
13
20
|
return false;
|
|
14
|
-
return a.every((item, i) => deepEqual(item, b[i]));
|
|
21
|
+
return a.every((item, i) => deepEqual(item, b[i], seen));
|
|
15
22
|
}
|
|
16
23
|
if (Array.isArray(a) !== Array.isArray(b))
|
|
17
24
|
return false;
|
|
@@ -19,7 +26,7 @@ function deepEqual(a, b) {
|
|
|
19
26
|
const keysB = Object.keys(b);
|
|
20
27
|
if (keysA.length !== keysB.length)
|
|
21
28
|
return false;
|
|
22
|
-
return keysA.every((key) => deepEqual(a[key], b[key]));
|
|
29
|
+
return keysA.every((key) => deepEqual(a[key], b[key], seen));
|
|
23
30
|
}
|
|
24
31
|
function isObservable(obj) {
|
|
25
32
|
return obj !== null && typeof obj === "object" && "subscribe" in obj && typeof obj.subscribe === "function";
|
|
@@ -84,21 +91,6 @@ function countedDistinctUntilChanged(compareFn) {
|
|
|
84
91
|
return a === b;
|
|
85
92
|
});
|
|
86
93
|
}
|
|
87
|
-
function deepFreeze(obj) {
|
|
88
|
-
if (obj === null || typeof obj !== "object")
|
|
89
|
-
return obj;
|
|
90
|
-
if (Object.isFrozen(obj))
|
|
91
|
-
return obj;
|
|
92
|
-
Object.freeze(obj);
|
|
93
|
-
if (Array.isArray(obj)) {
|
|
94
|
-
obj.forEach((item) => deepFreeze(item));
|
|
95
|
-
} else {
|
|
96
|
-
Object.keys(obj).forEach((key) => {
|
|
97
|
-
deepFreeze(obj[key]);
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
return obj;
|
|
101
|
-
}
|
|
102
94
|
var NODE = Symbol("node");
|
|
103
95
|
function createLeafNode(value) {
|
|
104
96
|
const subject$ = new BehaviorSubject(value);
|
|
@@ -147,25 +139,19 @@ function createObjectNode(value) {
|
|
|
147
139
|
return result;
|
|
148
140
|
}), shareReplay(1));
|
|
149
141
|
$.subscribe();
|
|
150
|
-
const frozen$ = $.pipe(map2(deepFreeze));
|
|
151
142
|
return {
|
|
152
|
-
|
|
143
|
+
$,
|
|
153
144
|
children,
|
|
154
|
-
get: () =>
|
|
145
|
+
get: () => getCurrentValue(),
|
|
155
146
|
set: (v) => {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
for (const [key, child] of children) {
|
|
159
|
-
child.set(v[key]);
|
|
160
|
-
}
|
|
161
|
-
} finally {
|
|
162
|
-
lock$.next(true);
|
|
147
|
+
for (const [key, child] of children) {
|
|
148
|
+
child.set(v[key]);
|
|
163
149
|
}
|
|
164
150
|
},
|
|
165
151
|
lock: () => lock$.next(false),
|
|
166
152
|
unlock: () => lock$.next(true),
|
|
167
153
|
subscribeOnce: (callback) => {
|
|
168
|
-
return
|
|
154
|
+
return $.pipe(take(1)).subscribe(callback);
|
|
169
155
|
}
|
|
170
156
|
};
|
|
171
157
|
}
|
|
@@ -194,7 +180,7 @@ function createArrayNode(value, comparator) {
|
|
|
194
180
|
};
|
|
195
181
|
const lock$ = new BehaviorSubject(true);
|
|
196
182
|
const baseLocked$ = combineLatest2([subject$, lock$]).pipe(filter(([_, unlocked]) => unlocked), map2(([arr, _]) => arr));
|
|
197
|
-
const locked$ = (comparator ? baseLocked$.pipe(distinctUntilChanged2(comparator)) : baseLocked$).pipe(map2(
|
|
183
|
+
const locked$ = (comparator ? baseLocked$.pipe(distinctUntilChanged2(comparator)) : baseLocked$).pipe(map2((arr) => [...arr]), shareReplay(1));
|
|
198
184
|
locked$.subscribe();
|
|
199
185
|
const length$ = locked$.pipe(map2((arr) => arr.length), distinctUntilChanged2(), shareReplay(1));
|
|
200
186
|
length$.subscribe();
|
|
@@ -204,8 +190,11 @@ function createArrayNode(value, comparator) {
|
|
|
204
190
|
return {
|
|
205
191
|
$: locked$,
|
|
206
192
|
childCache,
|
|
207
|
-
get: () =>
|
|
193
|
+
get: () => [...subject$.getValue()],
|
|
208
194
|
set: (v) => {
|
|
195
|
+
if (hasCircularReference(v)) {
|
|
196
|
+
throw new Error("Circular reference detected in array value. " + "Deepstate does not support circular references. " + "Please flatten your data structure or remove the circular reference.");
|
|
197
|
+
}
|
|
209
198
|
childCache.clear();
|
|
210
199
|
subject$.next([...v]);
|
|
211
200
|
},
|
|
@@ -235,13 +224,13 @@ function createArrayNode(value, comparator) {
|
|
|
235
224
|
const last = current[current.length - 1];
|
|
236
225
|
childCache.delete(current.length - 1);
|
|
237
226
|
subject$.next(current.slice(0, -1));
|
|
238
|
-
return
|
|
227
|
+
return last;
|
|
239
228
|
},
|
|
240
229
|
mapItems: (fn) => {
|
|
241
|
-
return subject$.getValue().map((item, i) => fn(
|
|
230
|
+
return subject$.getValue().map((item, i) => fn(item, i));
|
|
242
231
|
},
|
|
243
232
|
filterItems: (fn) => {
|
|
244
|
-
return
|
|
233
|
+
return subject$.getValue().filter((item, i) => fn(item, i));
|
|
245
234
|
},
|
|
246
235
|
lock: () => lock$.next(false),
|
|
247
236
|
unlock: () => lock$.next(true)
|
|
@@ -289,7 +278,7 @@ function createNullableObjectNode(initialValue) {
|
|
|
289
278
|
if (b === null || b === undefined)
|
|
290
279
|
return false;
|
|
291
280
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
292
|
-
}),
|
|
281
|
+
}), shareReplay(1));
|
|
293
282
|
$.subscribe();
|
|
294
283
|
const nodeState = { children };
|
|
295
284
|
const updateChildrenRef = () => {
|
|
@@ -318,7 +307,7 @@ function createNullableObjectNode(initialValue) {
|
|
|
318
307
|
get children() {
|
|
319
308
|
return nodeState.children;
|
|
320
309
|
},
|
|
321
|
-
get: () =>
|
|
310
|
+
get: () => getCurrentValue(),
|
|
322
311
|
set: (value) => {
|
|
323
312
|
if (value === null || value === undefined) {
|
|
324
313
|
subject$.next(value);
|
|
@@ -553,6 +542,17 @@ function createNestedArrayProjection(parentArray$, index, key, initialValue) {
|
|
|
553
542
|
}
|
|
554
543
|
};
|
|
555
544
|
}
|
|
545
|
+
function hasCircularReference(obj, seen = new WeakSet) {
|
|
546
|
+
if (obj === null || typeof obj !== "object")
|
|
547
|
+
return false;
|
|
548
|
+
if (seen.has(obj))
|
|
549
|
+
return true;
|
|
550
|
+
seen.add(obj);
|
|
551
|
+
if (Array.isArray(obj)) {
|
|
552
|
+
return obj.some((item) => hasCircularReference(item, seen));
|
|
553
|
+
}
|
|
554
|
+
return Object.values(obj).some((value) => hasCircularReference(value, seen));
|
|
555
|
+
}
|
|
556
556
|
function createNodeForValue(value, maybeNullable = false) {
|
|
557
557
|
if (isNullableMarked(value)) {
|
|
558
558
|
delete value[NULLABLE_MARKER];
|
|
@@ -567,6 +567,9 @@ function createNodeForValue(value, maybeNullable = false) {
|
|
|
567
567
|
if (typeof value !== "object") {
|
|
568
568
|
return createLeafNode(value);
|
|
569
569
|
}
|
|
570
|
+
if (hasCircularReference(value)) {
|
|
571
|
+
throw new Error("Circular reference detected in state value. " + "Deepstate does not support circular references because each property becomes a reactive node. " + "Please flatten your data structure or remove the circular reference.");
|
|
572
|
+
}
|
|
570
573
|
if (Array.isArray(value)) {
|
|
571
574
|
if (isArrayMarked(value)) {
|
|
572
575
|
const options = value[ARRAY_MARKER];
|
|
@@ -658,7 +661,7 @@ function wrapNullableWithProxy(node, path = "", debugLog) {
|
|
|
658
661
|
});
|
|
659
662
|
return proxy;
|
|
660
663
|
}
|
|
661
|
-
function wrapWithProxy(node, path = "", debugLog
|
|
664
|
+
function wrapWithProxy(node, path = "", debugLog) {
|
|
662
665
|
if (isNullableNode(node)) {
|
|
663
666
|
return wrapNullableWithProxy(node, path, debugLog);
|
|
664
667
|
}
|
|
@@ -731,12 +734,6 @@ function wrapWithProxy(node, path = "", debugLog, rootMethods) {
|
|
|
731
734
|
return node.subscribeOnce;
|
|
732
735
|
if (prop === NODE)
|
|
733
736
|
return node;
|
|
734
|
-
if (rootMethods) {
|
|
735
|
-
if (prop === "reset")
|
|
736
|
-
return rootMethods.reset;
|
|
737
|
-
if (prop === "getInitialState")
|
|
738
|
-
return rootMethods.getInitialState;
|
|
739
|
-
}
|
|
740
737
|
if (prop === Symbol.observable || prop === "@@observable") {
|
|
741
738
|
return () => node.$;
|
|
742
739
|
}
|
|
@@ -787,21 +784,8 @@ function wrapWithProxy(node, path = "", debugLog, rootMethods) {
|
|
|
787
784
|
}
|
|
788
785
|
function state(initialState, options) {
|
|
789
786
|
const debugLog = options?.debug ? createDebugLog({ enabled: true, storeName: options.name }) : undefined;
|
|
790
|
-
const frozenInitialState = deepFreeze(structuredClone(initialState));
|
|
791
787
|
const node = createObjectNode(initialState);
|
|
792
|
-
|
|
793
|
-
reset: () => {
|
|
794
|
-
debugLog?.("root", "reset", node.get(), frozenInitialState);
|
|
795
|
-
node.lock();
|
|
796
|
-
try {
|
|
797
|
-
node.set(structuredClone(frozenInitialState));
|
|
798
|
-
} finally {
|
|
799
|
-
node.unlock();
|
|
800
|
-
}
|
|
801
|
-
},
|
|
802
|
-
getInitialState: () => frozenInitialState
|
|
803
|
-
};
|
|
804
|
-
return wrapWithProxy(node, "", debugLog, rootMethods);
|
|
788
|
+
return wrapWithProxy(node, "", debugLog);
|
|
805
789
|
}
|
|
806
790
|
var NULLABLE_MARKER = Symbol("nullable");
|
|
807
791
|
function nullable(value) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@montra-interactive/deepstate",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"homepage": "https://github.com/Montra-Interactive/deepstate/tree/main/packages/core",
|
|
20
20
|
"repository": {
|
|
21
21
|
"type": "git",
|
|
22
|
-
"url": "https://github.com/Montra-Interactive/deepstate",
|
|
22
|
+
"url": "git+https://github.com/Montra-Interactive/deepstate.git",
|
|
23
23
|
"directory": "packages/core"
|
|
24
24
|
},
|
|
25
25
|
"module": "dist/index.js",
|
package/src/deepstate.ts
CHANGED
|
@@ -75,27 +75,6 @@ function countedDistinctUntilChanged<T>(compareFn?: (a: T, b: T) => boolean) {
|
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
// =============================================================================
|
|
79
|
-
// Deep Freeze
|
|
80
|
-
// =============================================================================
|
|
81
|
-
|
|
82
|
-
function deepFreeze<T>(obj: T): T {
|
|
83
|
-
if (obj === null || typeof obj !== "object") return obj;
|
|
84
|
-
if (Object.isFrozen(obj)) return obj;
|
|
85
|
-
|
|
86
|
-
Object.freeze(obj);
|
|
87
|
-
|
|
88
|
-
if (Array.isArray(obj)) {
|
|
89
|
-
obj.forEach((item) => deepFreeze(item));
|
|
90
|
-
} else {
|
|
91
|
-
Object.keys(obj).forEach((key) => {
|
|
92
|
-
deepFreeze((obj as Record<string, unknown>)[key]);
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return obj;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
78
|
// =============================================================================
|
|
100
79
|
// Types
|
|
101
80
|
// =============================================================================
|
|
@@ -123,16 +102,9 @@ type IsNullableObject<T> = IsNullish<T> extends true
|
|
|
123
102
|
: false;
|
|
124
103
|
|
|
125
104
|
/**
|
|
126
|
-
*
|
|
127
|
-
* Used for return types of get() and subscribe() to prevent accidental mutations.
|
|
105
|
+
* Type alias for values returned by get() and subscribe().
|
|
128
106
|
*/
|
|
129
|
-
export type DeepReadonly<T> =
|
|
130
|
-
? T
|
|
131
|
-
: [T] extends [Array<infer U>]
|
|
132
|
-
? ReadonlyArray<DeepReadonly<U>>
|
|
133
|
-
: [T] extends [object]
|
|
134
|
-
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
|
|
135
|
-
: T;
|
|
107
|
+
export type DeepReadonly<T> = T;
|
|
136
108
|
|
|
137
109
|
/**
|
|
138
110
|
* A mutable draft of state T for use in update callbacks.
|
|
@@ -151,21 +123,21 @@ interface NodeCore<T> {
|
|
|
151
123
|
const NODE = Symbol("node");
|
|
152
124
|
|
|
153
125
|
// External API types
|
|
154
|
-
type RxLeaf<T> = Observable<
|
|
126
|
+
type RxLeaf<T> = Observable<T> & {
|
|
155
127
|
/** Get current value synchronously */
|
|
156
|
-
get():
|
|
128
|
+
get(): T;
|
|
157
129
|
/** Set value */
|
|
158
130
|
set(value: T): void;
|
|
159
131
|
/** Subscribe to a single emission, then automatically unsubscribe */
|
|
160
|
-
subscribeOnce(callback: (value:
|
|
132
|
+
subscribeOnce(callback: (value: T) => void): Subscription;
|
|
161
133
|
[NODE]: NodeCore<T>;
|
|
162
134
|
};
|
|
163
135
|
|
|
164
136
|
type RxObject<T extends object> = {
|
|
165
137
|
[K in keyof T]: RxNodeFor<T[K]>;
|
|
166
|
-
} & Observable<
|
|
138
|
+
} & Observable<T> & {
|
|
167
139
|
/** Get current value synchronously */
|
|
168
|
-
get():
|
|
140
|
+
get(): T;
|
|
169
141
|
/** Set value */
|
|
170
142
|
set(value: T): void;
|
|
171
143
|
/**
|
|
@@ -178,15 +150,15 @@ type RxObject<T extends object> = {
|
|
|
178
150
|
* draft.age.set(31);
|
|
179
151
|
* });
|
|
180
152
|
*/
|
|
181
|
-
update(callback: (draft: RxObject<T>) => void):
|
|
153
|
+
update(callback: (draft: RxObject<T>) => void): T;
|
|
182
154
|
/** Subscribe to a single emission, then automatically unsubscribe */
|
|
183
|
-
subscribeOnce(callback: (value:
|
|
155
|
+
subscribeOnce(callback: (value: T) => void): Subscription;
|
|
184
156
|
[NODE]: NodeCore<T>;
|
|
185
157
|
};
|
|
186
158
|
|
|
187
|
-
type RxArray<T> = Observable<
|
|
159
|
+
type RxArray<T> = Observable<T[]> & {
|
|
188
160
|
/** Get current value synchronously */
|
|
189
|
-
get():
|
|
161
|
+
get(): T[];
|
|
190
162
|
/** Set value */
|
|
191
163
|
set(value: T[]): void;
|
|
192
164
|
/**
|
|
@@ -199,9 +171,9 @@ type RxArray<T> = Observable<DeepReadonly<T[]>> & {
|
|
|
199
171
|
* draft.push({ id: 2, name: "New" });
|
|
200
172
|
* });
|
|
201
173
|
*/
|
|
202
|
-
update(callback: (draft: RxArray<T>) => void):
|
|
174
|
+
update(callback: (draft: RxArray<T>) => void): T[];
|
|
203
175
|
/** Subscribe to a single emission, then automatically unsubscribe */
|
|
204
|
-
subscribeOnce(callback: (value:
|
|
176
|
+
subscribeOnce(callback: (value: T[]) => void): Subscription;
|
|
205
177
|
/** Get reactive node for array element at index */
|
|
206
178
|
at(index: number): RxNodeFor<T> | undefined;
|
|
207
179
|
/** Get current length (also observable) */
|
|
@@ -209,11 +181,11 @@ type RxArray<T> = Observable<DeepReadonly<T[]>> & {
|
|
|
209
181
|
/** Push items and return new length */
|
|
210
182
|
push(...items: T[]): number;
|
|
211
183
|
/** Pop last item */
|
|
212
|
-
pop():
|
|
184
|
+
pop(): T | undefined;
|
|
213
185
|
/** Map over current values (non-reactive, use .subscribe for reactive) */
|
|
214
|
-
map<U>(fn: (item:
|
|
186
|
+
map<U>(fn: (item: T, index: number) => U): U[];
|
|
215
187
|
/** Filter current values */
|
|
216
|
-
filter(fn: (item:
|
|
188
|
+
filter(fn: (item: T, index: number) => boolean): T[];
|
|
217
189
|
[NODE]: NodeCore<T[]>;
|
|
218
190
|
};
|
|
219
191
|
|
|
@@ -238,13 +210,13 @@ type RxArray<T> = Observable<DeepReadonly<T[]>> & {
|
|
|
238
210
|
* store.user.name.get(); // "Alice"
|
|
239
211
|
* store.user.name.set("Bob"); // Works!
|
|
240
212
|
*/
|
|
241
|
-
type RxNullable<T, TNonNull extends object = NonNullablePart<T> & object> = Observable<
|
|
213
|
+
type RxNullable<T, TNonNull extends object = NonNullablePart<T> & object> = Observable<T> & {
|
|
242
214
|
/** Get current value (may be null/undefined) */
|
|
243
|
-
get():
|
|
215
|
+
get(): T;
|
|
244
216
|
/** Set value (can be null/undefined or the full object) */
|
|
245
217
|
set(value: T): void;
|
|
246
218
|
/** Subscribe to a single emission, then automatically unsubscribe */
|
|
247
|
-
subscribeOnce(callback: (value:
|
|
219
|
+
subscribeOnce(callback: (value: T) => void): Subscription;
|
|
248
220
|
/**
|
|
249
221
|
* Update multiple properties in a single emission.
|
|
250
222
|
* @example
|
|
@@ -253,7 +225,7 @@ type RxNullable<T, TNonNull extends object = NonNullablePart<T> & object> = Obse
|
|
|
253
225
|
* user.age.set(31);
|
|
254
226
|
* });
|
|
255
227
|
*/
|
|
256
|
-
update(callback: (draft: RxObject<TNonNull>) => void):
|
|
228
|
+
update(callback: (draft: RxObject<TNonNull>) => void): T;
|
|
257
229
|
[NODE]: NodeCore<T>;
|
|
258
230
|
} & {
|
|
259
231
|
/**
|
|
@@ -291,10 +263,10 @@ type RxNullableChild<T> =
|
|
|
291
263
|
* The object itself might be undefined (if parent is null), but if present
|
|
292
264
|
* it has all the normal object methods and children.
|
|
293
265
|
*/
|
|
294
|
-
type RxNullableChildObject<T extends object> = Observable<
|
|
295
|
-
get():
|
|
266
|
+
type RxNullableChildObject<T extends object> = Observable<T | undefined> & {
|
|
267
|
+
get(): T | undefined;
|
|
296
268
|
set(value: T): void;
|
|
297
|
-
subscribeOnce(callback: (value:
|
|
269
|
+
subscribeOnce(callback: (value: T | undefined) => void): Subscription;
|
|
298
270
|
[NODE]: NodeCore<T | undefined>;
|
|
299
271
|
} & {
|
|
300
272
|
[K in keyof T]: RxNullableChild<T[K]>;
|
|
@@ -316,12 +288,7 @@ type RxNodeFor<T> =
|
|
|
316
288
|
// Fallback
|
|
317
289
|
: RxLeaf<T>;
|
|
318
290
|
|
|
319
|
-
export type RxState<T extends object> = RxObject<T
|
|
320
|
-
/** Reset the store to its initial state */
|
|
321
|
-
reset(): void;
|
|
322
|
-
/** Get the initial state (frozen copy) */
|
|
323
|
-
getInitialState(): DeepReadonly<T>;
|
|
324
|
-
};
|
|
291
|
+
export type RxState<T extends object> = RxObject<T>;
|
|
325
292
|
|
|
326
293
|
// =============================================================================
|
|
327
294
|
// Node Creation
|
|
@@ -408,29 +375,20 @@ function createObjectNode<T extends object>(value: T): NodeCore<T> & {
|
|
|
408
375
|
// Force subscription to make it hot (so emissions work even before external subscribers)
|
|
409
376
|
$.subscribe();
|
|
410
377
|
|
|
411
|
-
// Create a version that freezes on emission
|
|
412
|
-
const frozen$ = $.pipe(map(deepFreeze));
|
|
413
|
-
|
|
414
378
|
return {
|
|
415
|
-
|
|
379
|
+
$,
|
|
416
380
|
children: children as Map<string, NodeCore<unknown>>,
|
|
417
|
-
get: () =>
|
|
381
|
+
get: () => getCurrentValue(),
|
|
418
382
|
set: (v: T) => {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
try {
|
|
422
|
-
for (const [key, child] of children) {
|
|
423
|
-
child.set(v[key]);
|
|
424
|
-
}
|
|
425
|
-
} finally {
|
|
426
|
-
lock$.next(true);
|
|
383
|
+
for (const [key, child] of children) {
|
|
384
|
+
child.set(v[key]);
|
|
427
385
|
}
|
|
428
386
|
},
|
|
429
387
|
lock: () => lock$.next(false),
|
|
430
388
|
unlock: () => lock$.next(true),
|
|
431
389
|
// Note: update() is implemented in wrapWithProxy since it needs the proxy reference
|
|
432
390
|
subscribeOnce: (callback: (value: T) => void): Subscription => {
|
|
433
|
-
return
|
|
391
|
+
return $.pipe(take(1)).subscribe(callback);
|
|
434
392
|
},
|
|
435
393
|
};
|
|
436
394
|
}
|
|
@@ -499,11 +457,11 @@ function createArrayNode<T>(
|
|
|
499
457
|
);
|
|
500
458
|
|
|
501
459
|
// Apply distinct comparison if provided
|
|
502
|
-
const locked$ = (comparator
|
|
460
|
+
const locked$ = (comparator
|
|
503
461
|
? baseLocked$.pipe(distinctUntilChanged(comparator))
|
|
504
462
|
: baseLocked$
|
|
505
463
|
).pipe(
|
|
506
|
-
map(
|
|
464
|
+
map((arr) => [...arr]),
|
|
507
465
|
shareReplay(1)
|
|
508
466
|
);
|
|
509
467
|
locked$.subscribe(); // Keep hot
|
|
@@ -523,8 +481,16 @@ function createArrayNode<T>(
|
|
|
523
481
|
return {
|
|
524
482
|
$: locked$ as Observable<T[]>,
|
|
525
483
|
childCache,
|
|
526
|
-
get: () =>
|
|
484
|
+
get: () => [...subject$.getValue()] as T[],
|
|
527
485
|
set: (v: T[]) => {
|
|
486
|
+
// Check for circular references in array items
|
|
487
|
+
if (hasCircularReference(v)) {
|
|
488
|
+
throw new Error(
|
|
489
|
+
'Circular reference detected in array value. ' +
|
|
490
|
+
'Deepstate does not support circular references. ' +
|
|
491
|
+
'Please flatten your data structure or remove the circular reference.'
|
|
492
|
+
);
|
|
493
|
+
}
|
|
528
494
|
// Clear child cache when array is replaced
|
|
529
495
|
childCache.clear();
|
|
530
496
|
subject$.next([...v]);
|
|
@@ -555,13 +521,13 @@ function createArrayNode<T>(
|
|
|
555
521
|
// Clear cached node for popped index
|
|
556
522
|
childCache.delete(current.length - 1);
|
|
557
523
|
subject$.next(current.slice(0, -1));
|
|
558
|
-
return
|
|
524
|
+
return last as T;
|
|
559
525
|
},
|
|
560
526
|
mapItems: <U>(fn: (item: T, index: number) => U): U[] => {
|
|
561
|
-
return subject$.getValue().map((item, i) => fn(
|
|
527
|
+
return subject$.getValue().map((item, i) => fn(item as T, i));
|
|
562
528
|
},
|
|
563
529
|
filterItems: (fn: (item: T, index: number) => boolean): T[] => {
|
|
564
|
-
return
|
|
530
|
+
return subject$.getValue().filter((item, i) => fn(item as T, i)) as T[];
|
|
565
531
|
},
|
|
566
532
|
lock: () => lock$.next(false),
|
|
567
533
|
unlock: () => lock$.next(true),
|
|
@@ -663,7 +629,6 @@ function createNullableObjectNode<T>(
|
|
|
663
629
|
if (b === null || b === undefined) return false;
|
|
664
630
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
665
631
|
}),
|
|
666
|
-
map(deepFreeze),
|
|
667
632
|
shareReplay(1)
|
|
668
633
|
);
|
|
669
634
|
$.subscribe(); // Keep hot
|
|
@@ -706,7 +671,7 @@ function createNullableObjectNode<T>(
|
|
|
706
671
|
$,
|
|
707
672
|
get children() { return nodeState.children; },
|
|
708
673
|
|
|
709
|
-
get: () =>
|
|
674
|
+
get: () => getCurrentValue(),
|
|
710
675
|
|
|
711
676
|
set: (value: T) => {
|
|
712
677
|
if (value === null || value === undefined) {
|
|
@@ -1078,6 +1043,19 @@ function createNestedArrayProjection<T>(
|
|
|
1078
1043
|
};
|
|
1079
1044
|
}
|
|
1080
1045
|
|
|
1046
|
+
// Helper to detect circular references in an object
|
|
1047
|
+
function hasCircularReference(obj: unknown, seen = new WeakSet<object>()): boolean {
|
|
1048
|
+
if (obj === null || typeof obj !== 'object') return false;
|
|
1049
|
+
if (seen.has(obj as object)) return true;
|
|
1050
|
+
seen.add(obj as object);
|
|
1051
|
+
|
|
1052
|
+
if (Array.isArray(obj)) {
|
|
1053
|
+
return obj.some(item => hasCircularReference(item, seen));
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
return Object.values(obj).some(value => hasCircularReference(value, seen));
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1081
1059
|
// Factory to create the right node type
|
|
1082
1060
|
// When maybeNullable is true and value is null/undefined, creates a NullableNodeCore
|
|
1083
1061
|
// that can later be upgraded to an object with children
|
|
@@ -1099,6 +1077,16 @@ function createNodeForValue<T>(value: T, maybeNullable: boolean = false): NodeCo
|
|
|
1099
1077
|
if (typeof value !== "object") {
|
|
1100
1078
|
return createLeafNode(value as Primitive) as NodeCore<T>;
|
|
1101
1079
|
}
|
|
1080
|
+
|
|
1081
|
+
// Check for circular references before creating nodes
|
|
1082
|
+
if (hasCircularReference(value)) {
|
|
1083
|
+
throw new Error(
|
|
1084
|
+
'Circular reference detected in state value. ' +
|
|
1085
|
+
'Deepstate does not support circular references because each property becomes a reactive node. ' +
|
|
1086
|
+
'Please flatten your data structure or remove the circular reference.'
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1102
1090
|
if (Array.isArray(value)) {
|
|
1103
1091
|
// Check if array was marked with options via array() helper
|
|
1104
1092
|
if (isArrayMarked(value)) {
|
|
@@ -1217,12 +1205,7 @@ function wrapNullableWithProxy<T>(node: NullableNodeCore<T>, path: string = '',
|
|
|
1217
1205
|
return proxy as unknown as RxNullable<T>;
|
|
1218
1206
|
}
|
|
1219
1207
|
|
|
1220
|
-
|
|
1221
|
-
reset: () => void;
|
|
1222
|
-
getInitialState: () => DeepReadonly<T>;
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
function wrapWithProxy<T>(node: NodeCore<T>, path: string = '', debugLog?: DebugLogFn, rootMethods?: RootMethods<T>): RxNodeFor<T> {
|
|
1208
|
+
function wrapWithProxy<T>(node: NodeCore<T>, path: string = '', debugLog?: DebugLogFn): RxNodeFor<T> {
|
|
1226
1209
|
// Check for nullable node first (before checking value, since value might be null)
|
|
1227
1210
|
if (isNullableNode(node)) {
|
|
1228
1211
|
return wrapNullableWithProxy(node, path, debugLog) as RxNodeFor<T>;
|
|
@@ -1319,12 +1302,6 @@ function wrapWithProxy<T>(node: NodeCore<T>, path: string = '', debugLog?: Debug
|
|
|
1319
1302
|
if (prop === "update") return updateFn;
|
|
1320
1303
|
if (prop === "subscribeOnce") return node.subscribeOnce;
|
|
1321
1304
|
if (prop === NODE) return node;
|
|
1322
|
-
|
|
1323
|
-
// Root store methods (reset, getInitialState)
|
|
1324
|
-
if (rootMethods) {
|
|
1325
|
-
if (prop === "reset") return rootMethods.reset;
|
|
1326
|
-
if (prop === "getInitialState") return rootMethods.getInitialState;
|
|
1327
|
-
}
|
|
1328
1305
|
|
|
1329
1306
|
// Symbol.observable for RxJS interop
|
|
1330
1307
|
if (prop === Symbol.observable || prop === "@@observable") {
|
|
@@ -1404,28 +1381,8 @@ export function state<T extends object>(initialState: T, options?: StateOptions)
|
|
|
1404
1381
|
? createDebugLog({ enabled: true, storeName: options.name })
|
|
1405
1382
|
: undefined;
|
|
1406
1383
|
|
|
1407
|
-
// Deep clone and freeze the initial state for later reset
|
|
1408
|
-
const frozenInitialState = deepFreeze(structuredClone(initialState));
|
|
1409
|
-
|
|
1410
1384
|
const node = createObjectNode(initialState);
|
|
1411
|
-
|
|
1412
|
-
// Create reset functions to pass to wrapWithProxy
|
|
1413
|
-
const rootMethods = {
|
|
1414
|
-
reset: () => {
|
|
1415
|
-
debugLog?.('root', 'reset', node.get(), frozenInitialState);
|
|
1416
|
-
// Batch the reset so only one emission occurs
|
|
1417
|
-
node.lock();
|
|
1418
|
-
try {
|
|
1419
|
-
// Deep clone so we don't pass frozen objects to set()
|
|
1420
|
-
node.set(structuredClone(frozenInitialState) as T);
|
|
1421
|
-
} finally {
|
|
1422
|
-
node.unlock();
|
|
1423
|
-
}
|
|
1424
|
-
},
|
|
1425
|
-
getInitialState: () => frozenInitialState as DeepReadonly<T>,
|
|
1426
|
-
};
|
|
1427
|
-
|
|
1428
|
-
return wrapWithProxy(node as NodeCore<T>, '', debugLog, rootMethods) as unknown as RxState<T>;
|
|
1385
|
+
return wrapWithProxy(node as NodeCore<T>, '', debugLog) as RxState<T>;
|
|
1429
1386
|
}
|
|
1430
1387
|
|
|
1431
1388
|
// Symbol to mark a value as nullable
|
package/src/helpers.ts
CHANGED
|
@@ -8,15 +8,26 @@
|
|
|
8
8
|
import { Observable, combineLatest } from "rxjs";
|
|
9
9
|
import { distinctUntilChanged, map } from "rxjs/operators";
|
|
10
10
|
|
|
11
|
-
// Deep equality check
|
|
12
|
-
function deepEqual(a: unknown, b: unknown): boolean {
|
|
11
|
+
// Deep equality check with circular reference protection
|
|
12
|
+
function deepEqual(a: unknown, b: unknown, seen = new WeakMap<object, WeakSet<object>>()): boolean {
|
|
13
13
|
if (a === b) return true;
|
|
14
14
|
if (a === null || b === null) return false;
|
|
15
15
|
if (typeof a !== "object" || typeof b !== "object") return false;
|
|
16
16
|
|
|
17
|
+
// Circular reference protection: if we've already compared these two objects, return true
|
|
18
|
+
// (they're equal as far as we've seen, and going deeper would be infinite)
|
|
19
|
+
const seenWithA = seen.get(a as object);
|
|
20
|
+
if (seenWithA?.has(b as object)) return true;
|
|
21
|
+
|
|
22
|
+
// Track this comparison
|
|
23
|
+
if (!seen.has(a as object)) {
|
|
24
|
+
seen.set(a as object, new WeakSet());
|
|
25
|
+
}
|
|
26
|
+
seen.get(a as object)!.add(b as object);
|
|
27
|
+
|
|
17
28
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
18
29
|
if (a.length !== b.length) return false;
|
|
19
|
-
return a.every((item, i) => deepEqual(item, b[i]));
|
|
30
|
+
return a.every((item, i) => deepEqual(item, b[i], seen));
|
|
20
31
|
}
|
|
21
32
|
|
|
22
33
|
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
@@ -26,7 +37,7 @@ function deepEqual(a: unknown, b: unknown): boolean {
|
|
|
26
37
|
if (keysA.length !== keysB.length) return false;
|
|
27
38
|
|
|
28
39
|
return keysA.every((key) =>
|
|
29
|
-
deepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key])
|
|
40
|
+
deepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key], seen)
|
|
30
41
|
);
|
|
31
42
|
}
|
|
32
43
|
|
|
@@ -126,7 +137,7 @@ export function select(
|
|
|
126
137
|
* @returns An Observable that emits an array of selected values
|
|
127
138
|
*/
|
|
128
139
|
export function selectFromEach<T, U>(
|
|
129
|
-
arrayNode: Observable<
|
|
140
|
+
arrayNode: Observable<T[]>,
|
|
130
141
|
selector: (item: T, index: number) => U
|
|
131
142
|
): Observable<U[]> {
|
|
132
143
|
return arrayNode.pipe(
|