@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.
- package/dist/deepstate.d.ts +46 -1
- package/dist/deepstate.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +84 -16
- package/package.json +1 -1
- package/src/deepstate.ts +183 -16
- package/src/index.ts +2 -1
package/dist/deepstate.d.ts
CHANGED
|
@@ -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
|
|
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
|
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":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAmB,UAAU,EAAqB,YAAY,EAAE,MAAM,MAAM,CAAC;AAapF,eAAO,IAAI,iBAAiB,QAAI,CAAC;AACjC,wBAAgB,sBAAsB,SAErC;AAyED,KAAK,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAGhF,KAAK,eAAe,CAAC,CAAC,IAAI,CAAC,SAAS,IAAI,GAAG,SAAS,GAAG,KAAK,GAAG,CAAC,CAAC;AAGjE,KAAK,OAAO,CAAC,CAAC,IAAI,IAAI,SAAS,CAAC,GAAG,IAAI,GAAG,KAAK,CAAC;AAChD,KAAK,YAAY,CAAC,CAAC,IAAI,SAAS,SAAS,CAAC,GAAG,IAAI,GAAG,KAAK,CAAC;AAC1D,KAAK,SAAS,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,SAAS,IAAI,GAAG,IAAI,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;AAGrE,KAAK,mBAAmB,CAAC,CAAC,IAAI,eAAe,CAAC,CAAC,CAAC,SAAS,MAAM,GAC3D,eAAe,CAAC,CAAC,CAAC,SAAS,KAAK,CAAC,OAAO,CAAC,GACvC,KAAK,GACL,IAAI,GACN,KAAK,CAAC;AAGV,KAAK,gBAAgB,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,SAAS,IAAI,GAChD,mBAAmB,CAAC,CAAC,CAAC,GACtB,KAAK,CAAC;AAEV;;;GAGG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,GACjD,CAAC,GACD,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,GAC1B,aAAa,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,GAC9B,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,GAClB;IAAE,QAAQ,EAAE,CAAC,IAAI,MAAM,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,GAC/C,CAAC,CAAC;AAEV;;GAEG;AACH,MAAM,MAAM,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC;AAGzB,UAAU,QAAQ,CAAC,CAAC;IAClB,QAAQ,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;IAC1B,GAAG,IAAI,CAAC,CAAC;IACT,GAAG,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IACpB,aAAa,CAAC,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,GAAG,YAAY,CAAC;CAC5D;AAGD,QAAA,MAAM,IAAI,eAAiB,CAAC;AAG5B,KAAK,MAAM,CAAC,CAAC,IAAI,UAAU,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,GAAG;IAC7C,sCAAsC;IACtC,GAAG,IAAI,YAAY,CAAC,CAAC,CAAC,CAAC;IACvB,gBAAgB;IAChB,GAAG,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IACpB,qEAAqE;IACrE,aAAa,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,YAAY,CAAC;IACxE,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;CACrB,CAAC;AAEF,KAAK,QAAQ,CAAC,CAAC,SAAS,MAAM,IAAI;KAC/B,CAAC,IAAI,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAChC,GAAG,UAAU,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,GAAG;IAChC,sCAAsC;IACtC,GAAG,IAAI,YAAY,CAAC,CAAC,CAAC,CAAC;IACvB,gBAAgB;IAChB,GAAG,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IACpB;;;;;;;;;OASG;IACH,MAAM,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;IAChE,qEAAqE;IACrE,aAAa,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,YAAY,CAAC;IACxE,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;CACrB,CAAC;AAEF,KAAK,OAAO,CAAC,CAAC,IAAI,UAAU,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG;IAChD,sCAAsC;IACtC,GAAG,IAAI,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC;IACzB,gBAAgB;IAChB,GAAG,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI,CAAC;IACtB;;;;;;;;;OASG;IACH,MAAM,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC;IACjE,qEAAqE;IACrE,aAAa,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC,EAAE,CAAC,KAAK,IAAI,GAAG,YAAY,CAAC;IAC1E,mDAAmD;IACnD,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;IAC5C,2CAA2C;IAC3C,MAAM,EAAE,UAAU,CAAC,MAAM,CAAC,GAAG;QAAE,GAAG,IAAI,MAAM,CAAA;KAAE,CAAC;IAC/C,uCAAuC;IACvC,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,GAAG,MAAM,CAAC;IAC5B,oBAAoB;IACpB,GAAG,IAAI,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;IACnC,0EAA0E;IAC1E,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;IAC7D,4BAA4B;IAC5B,MAAM,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,GAAG,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC;IACjF,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC;CACvB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,KAAK,UAAU,CAAC,CAAC,EAAE,QAAQ,SAAS,MAAM,GAAG,eAAe,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,UAAU,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,GAAG;IACxG,gDAAgD;IAChD,GAAG,IAAI,YAAY,CAAC,CAAC,CAAC,CAAC;IACvB,2DAA2D;IAC3D,GAAG,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IACpB,qEAAqE;IACrE,aAAa,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,YAAY,CAAC;IACxE;;;;;;;OAOG;IACH,MAAM,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,QAAQ,CAAC,KAAK,IAAI,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;IACvE,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;CACrB,GAAG;KAMD,CAAC,IAAI,MAAM,QAAQ,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;CACpD,CAAC;AAEF;;;;;GAKG;AACH,KAAK,eAAe,CAAC,CAAC,IAEpB,gBAAgB,CAAC,CAAC,CAAC,SAAS,IAAI,GAC5B,UAAU,CAAC,CAAC,CAAC,GAEf,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,GACrB,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC,GAEvB,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,GAC1B,OAAO,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,SAAS,CAAC,GAEhC,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,GAClB,qBAAqB,CAAC,CAAC,CAAC,GAE1B,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;AAE1B;;;;GAIG;AACH,KAAK,qBAAqB,CAAC,CAAC,SAAS,MAAM,IAAI,UAAU,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,GAAG;IACvF,GAAG,IAAI,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;IACnC,GAAG,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IACpB,aAAa,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,KAAK,IAAI,GAAG,YAAY,CAAC;IACpF,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;CACjC,GAAG;KACD,CAAC,IAAI,MAAM,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CACtC,CAAC;AAEF,KAAK,SAAS,CAAC,CAAC,IAEd,gBAAgB,CAAC,CAAC,CAAC,SAAS,IAAI,GAC5B,UAAU,CAAC,CAAC,CAAC,GAEf,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,GACrB,MAAM,CAAC,CAAC,CAAC,GAEX,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,GAC1B,OAAO,CAAC,CAAC,CAAC,GAEZ,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,GAClB,QAAQ,CAAC,CAAC,CAAC,GAEb,MAAM,CAAC,CAAC,CAAC,CAAC;AAEd,MAAM,MAAM,OAAO,CAAC,CAAC,SAAS,MAAM,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC;AA6hCpD,MAAM,WAAW,YAAY;IAC3B,0CAA0C;IAC1C,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,wDAAwD;IACxD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,KAAK,CAAC,CAAC,SAAS,MAAM,EAAE,YAAY,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,CAAC,CAAC,CAQ3F;AAMD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,IAAI,CAMpE;AAUD,kDAAkD;AAClD,MAAM,MAAM,aAAa,CAAC,CAAC,IACvB,KAAK,GACL,SAAS,GACT,MAAM,GACN,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,OAAO,CAAC,CAAC;AAElC,qCAAqC;AACrC,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC;CAC7B;AAMD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,OAAO,GAAE,YAAY,CAAC,CAAC,CAAM,GAAG,CAAC,EAAE,CAIvE"}
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
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>(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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";
|