@montra-interactive/deepstate 0.1.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/src/helpers.ts ADDED
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Convenience helpers for deepstate.
3
+ *
4
+ * These are optional utilities that build on top of the core state() function.
5
+ * They can be imported from 'deepstate' or 'deepstate/helpers'.
6
+ */
7
+
8
+ import { Observable, combineLatest } from "rxjs";
9
+ import { distinctUntilChanged, map } from "rxjs/operators";
10
+
11
+ // Deep equality check (duplicated to keep helpers independent)
12
+ function deepEqual(a: unknown, b: unknown): boolean {
13
+ if (a === b) return true;
14
+ if (a === null || b === null) return false;
15
+ if (typeof a !== "object" || typeof b !== "object") return false;
16
+
17
+ if (Array.isArray(a) && Array.isArray(b)) {
18
+ if (a.length !== b.length) return false;
19
+ return a.every((item, i) => deepEqual(item, b[i]));
20
+ }
21
+
22
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
23
+
24
+ const keysA = Object.keys(a);
25
+ const keysB = Object.keys(b);
26
+ if (keysA.length !== keysB.length) return false;
27
+
28
+ return keysA.every((key) =>
29
+ deepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key])
30
+ );
31
+ }
32
+
33
+ // Helper to check if something is an Observable
34
+ function isObservable(obj: unknown): obj is Observable<unknown> {
35
+ return (
36
+ obj !== null &&
37
+ typeof obj === "object" &&
38
+ "subscribe" in obj &&
39
+ typeof (obj as Record<string, unknown>).subscribe === "function"
40
+ );
41
+ }
42
+
43
+ // Helper type to extract the value type from an Observable
44
+ type ObservableValue<T> = T extends Observable<infer U> ? U : never;
45
+
46
+ // Type for array of observables -> tuple of their values
47
+ type ObservableValues<T extends Observable<unknown>[]> = {
48
+ [K in keyof T]: ObservableValue<T[K]>;
49
+ };
50
+
51
+ // Type for object of observables -> object of their values
52
+ type ObservableObjectValues<T extends Record<string, Observable<unknown>>> = {
53
+ [K in keyof T]: ObservableValue<T[K]>;
54
+ };
55
+
56
+ /**
57
+ * Select multiple observables and combine them into a single observable.
58
+ *
59
+ * @example Array form - returns tuple
60
+ * ```ts
61
+ * select(myState.user.name, myState.user.profile.address.city)
62
+ * .subscribe(([name, city]) => console.log(name, city));
63
+ * ```
64
+ *
65
+ * @example Object form - returns object with named keys
66
+ * ```ts
67
+ * select({
68
+ * name: myState.user.name,
69
+ * city: myState.user.profile.address.city,
70
+ * }).subscribe(({ name, city }) => console.log(name, city));
71
+ * ```
72
+ */
73
+ export function select<T extends Observable<unknown>[]>(
74
+ ...observables: T
75
+ ): Observable<ObservableValues<T>>;
76
+ export function select<T extends Record<string, Observable<unknown>>>(
77
+ observables: T
78
+ ): Observable<ObservableObjectValues<T>>;
79
+ export function select(
80
+ ...args: Observable<unknown>[] | [Record<string, Observable<unknown>>]
81
+ ): Observable<unknown> {
82
+ // Object form: select({ name: obs1, city: obs2 })
83
+ if (args.length === 1 && !isObservable(args[0])) {
84
+ const obj = args[0] as Record<string, Observable<unknown>>;
85
+ const keys = Object.keys(obj);
86
+ const observables = keys.map((k) => obj[k]);
87
+
88
+ return combineLatest(observables).pipe(
89
+ map((values) => {
90
+ const result: Record<string, unknown> = {};
91
+ keys.forEach((key, i) => {
92
+ result[key] = values[i];
93
+ });
94
+ return result;
95
+ })
96
+ );
97
+ }
98
+
99
+ // Array form: select(obs1, obs2, obs3)
100
+ return combineLatest(args as Observable<unknown>[]);
101
+ }
102
+
103
+ /**
104
+ * Select a derived value from each item in an array, with precise change detection.
105
+ * Only emits when the selected/derived values actually change, not when other
106
+ * properties of the array items change.
107
+ *
108
+ * @example Select a single property from each item
109
+ * ```ts
110
+ * selectFromEach(myState.items, item => item.price)
111
+ * .subscribe(prices => {
112
+ * const total = prices.reduce((a, b) => a + b, 0);
113
+ * });
114
+ * ```
115
+ *
116
+ * @example Select multiple properties / derived values
117
+ * ```ts
118
+ * selectFromEach(myState.items, item => ({
119
+ * name: item.name,
120
+ * total: item.price * item.qty
121
+ * })).subscribe(summaries => console.log(summaries));
122
+ * ```
123
+ *
124
+ * @param arrayNode - An RxArray node from your state (e.g., `myState.items`)
125
+ * @param selector - A function that extracts/derives a value from each item
126
+ * @returns An Observable that emits an array of selected values
127
+ */
128
+ export function selectFromEach<T, U>(
129
+ arrayNode: Observable<ReadonlyArray<T>>,
130
+ selector: (item: T, index: number) => U
131
+ ): Observable<U[]> {
132
+ return arrayNode.pipe(
133
+ map((items) => items.map(selector)),
134
+ distinctUntilChanged((a, b) =>
135
+ a.length === b.length && a.every((v, i) => deepEqual(v, b[i]))
136
+ )
137
+ );
138
+ }
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * deepstate - Proxy-based RxJS state management
3
+ *
4
+ * Core exports:
5
+ * - state() - Create reactive state from plain objects
6
+ * - nullable() - Mark a property as nullable (can transition between null and object)
7
+ * - RxState, Draft - Type exports
8
+ *
9
+ * Helper exports:
10
+ * - select() - Combine multiple observables
11
+ * - selectFromEach() - Select from each array item with precise change detection
12
+ */
13
+
14
+ export { state, nullable, type RxState, type Draft } from "./deepstate";
15
+ export { select, selectFromEach } from "./helpers";