chem-rx 0.0.1 → 0.0.2

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
@@ -1,6 +1,6 @@
1
1
  # chem-rx
2
2
 
3
- `chem-rx` wraps`rx.js` to provide a state management solution focused on
3
+ `chem-rx` wraps `rxjs` to provide a state management solution focused on
4
4
  simplicity. Useable with or without React!
5
5
 
6
6
  ## Atom
@@ -14,10 +14,10 @@ Atoms are state containers that take any value - object, array, or primitive.
14
14
  ```
15
15
  import { Atom } from 'chem-rx'
16
16
 
17
- const numberAtom: BaseAtom = Atom(0)
18
- const stringAtom: BaseAtom = Atom('hello')
19
- const arrayAtom: ArrayAtom = Atom(['hello', 'world'])
20
- const objectAtom: ObjectAtom = Atom({ 'hello': 'world', 'world': 'hello' })
17
+ const number$: BaseAtom = Atom(0)
18
+ const string$: BaseAtom = Atom('hello')
19
+ const array$: ArrayAtom = Atom(['hello', 'world'])
20
+ const object$: ObjectAtom = Atom({ 'hello': 'world', 'world': 'hello' })
21
21
  ```
22
22
 
23
23
  ### Getting & setting values
@@ -26,55 +26,293 @@ const objectAtom: ObjectAtom = Atom({ 'hello': 'world', 'world': 'hello' })
26
26
  `ObjectAtom` depending on the input.
27
27
 
28
28
  ```
29
- /*
30
- * BaseAtom
31
- */
32
- numberAtom.set(2)
33
- numberAtom.value()
29
+ // Base Atom
30
+ number$.set(2)
31
+ number$.value()
34
32
  // 2
35
33
 
36
- /*
37
- * ArrayAtom
38
- */
39
- arrayAtom.push('!')
40
- arrayAtom.value()
34
+ // ArrayAtom
35
+ array$.push('!')
36
+ array$.value()
41
37
  // ['hello', 'world', '!']
42
- arrayAtom.get(1)
38
+ array$.get(1)
43
39
  // 'world'
44
40
 
45
- /*
46
- * ObjectAtom
47
- */
48
- objectAtom.set('world', 'hi')
49
- objectAtom.value()
41
+ // ObjectAtom
42
+ object$.set('world', 'hi')
43
+ object$.value()
50
44
  // {'hello': 'world', 'world': 'hi'}
51
45
 
52
- objectAtom.set('sup', 'earth')
46
+ object$.set('sup', 'earth')
53
47
  // {'hello': 'world', 'world': 'hi', 'sup': 'earth'}
54
48
 
55
- objectAtom.get('world')
49
+ object$.get('world')
56
50
  // 'hi'
57
51
  ```
58
52
 
59
- ### Transforming Atoms
53
+ ### Selecting Atoms
54
+
55
+ You can select Object and Array atoms to return new Atoms that wrap the values
56
+ at that key. This can be useful for working with different parts of nested Array
57
+ and Object atoms.
58
+
59
+ ```
60
+ const nestedData = Atom({
61
+ stacy: {
62
+ nickname: "stace",
63
+ education: {
64
+ school: "Penn",
65
+ graduation: 2014,
66
+ },
67
+ },
68
+ });
69
+
70
+ const stacy = nestedData.select("stacy");
71
+ console.log(stacy.get('nickname'))
72
+ // 'stace'
73
+
74
+ const stacySchool = nestedData.select("stacy").select("education");
75
+ console.log(stacySchool.get('school'))
76
+ // 'Penn'
77
+ ```
78
+
79
+ ### Derived Atoms (read-only)
80
+
81
+ You can derive new Atoms from any existing atoms. Any time the original atoms
82
+ change, your derived atoms will automatically update with new values!
83
+
84
+ ```
85
+ const atom$ = Atom(3);
86
+
87
+ // square it
88
+ const squared$ = atom$.derive((x) => x * x);
89
+
90
+ // "9"
91
+ console.log(squared$.value())
92
+
93
+ // Update the original value
94
+ atom$.set(4)
95
+
96
+ // "16"
97
+ console.log(squared$.value())
98
+ ```
99
+
100
+ Every derived atom is **read-only**. This prevents you from overriding the
101
+ derived output value, since it is automatically derived from another input!
102
+
103
+ ```
104
+ // ERR: Property 'set' does not exist on type ReadOnlyAtom
105
+ squared$.set(2)
106
+ ```
60
107
 
61
- ### Composing Atoms & ReadOnlyAtoms
108
+ You can optionally enforce `readOnly` on an atom at creation time if needed
109
+
110
+ ```
111
+ const atom$ = Atom(3, true);
112
+
113
+ // ERR: Property 'set' does not exist on type ReadOnlyAtom
114
+ atom$.set(2)
115
+ ```
116
+
117
+ ### Combining Atoms
118
+
119
+ Multiple atoms can also be **combined** to create brand new atoms!
120
+
121
+ Here's an example of joining a set of normalized data models
122
+
123
+ ```
124
+ const pets$ = Atom<{ [name: string]: { type: "dog" | "cat"; age: number } }>({
125
+ spot: {name: 'spot', type: "dog", age: 5 },
126
+ tabby: {name: 'tabby', type: "cat", age: 12 },
127
+ });
128
+
129
+ const people$ = Atom<{ [name: string]: { pets: string[] } }>({
130
+ mary: { pets: ["spot"] },
131
+ cam: { pets: ["tabby"] },
132
+ });
133
+
134
+ const mary$ = Atom.combine(pets$, people$.select("mary")).derive(
135
+ ([pets, mary]) => {
136
+ return {
137
+ ...mary,
138
+ pets: mary.pets.map((petName) => pets[petName]),
139
+ };
140
+ }
141
+ );
142
+
143
+ console.log(mary$.select('pets').value())
144
+ /*
145
+ * [{
146
+ * name: "spot",
147
+ * type: "dog",
148
+ * age: 5,
149
+ * }]
150
+ */
151
+ ```
62
152
 
63
153
  ### Subscribing to updates
64
154
 
155
+ Atoms emit values each time they're updated. You can subscribe callbacks to them
156
+ to act on updates
157
+
158
+ ```
159
+ const atom$ = Atom(3);
160
+
161
+ const subscription = atom$.subscribe(val => {
162
+ console.log("Received value: ", val)
163
+ })
164
+
165
+ atom$.set(4)
166
+ // "Received value: 4"
167
+
168
+ // Unsubscribe to clean up
169
+ subscription.unsubscribe();
170
+ ```
171
+
172
+ ### Signals
173
+
174
+ Sometimes, all you want is something to ping you when there's an update. Signals
175
+ are stateless transceivers for signaling updates.
176
+
177
+ ```
178
+ const signal$ = new Signal();
179
+
180
+ const subscription = signal$.subscribe(() => {
181
+ console.log("PONG")
182
+ })
183
+
184
+ signal$.ping()
185
+ // "PONG"
186
+
187
+ // Unsubscribe to clean up
188
+ subscription.unsubscribe();
189
+ ```
190
+
191
+ Signals can also send values if needed.
192
+
193
+ ```
194
+ const signal$ = new Signal();
195
+
196
+ const subscription = signal$.subscribe((value) => {
197
+ console.log("PONGED: ", value)
198
+ })
199
+
200
+ signal$.ping("hello")
201
+ // "PONGED: hello"
202
+
203
+ // Unsubscribe to clean up
204
+ subscription.unsubscribe();
205
+ ```
206
+
65
207
  ## Use with React
66
208
 
67
209
  ### useAtom
68
210
 
211
+ `useAtom` automatically updates with new values in your react components.
212
+
213
+ If you want to update your atoms, you can simply call the same `set`
214
+ (Base/ObjectAtom) or `push` (ArrayAtom) methods you would typically use outside
215
+ of react.
216
+
217
+ ```
218
+ import { Atom, useAtom } from 'chem-rx'
219
+
220
+ const count$ = Atom(0)
221
+
222
+ function Counter() {
223
+ const count = useAtom(count$)
224
+ return (
225
+ <h1>
226
+ {count}
227
+ <button onClick={() => count$.set(count$.value() + 1)}>one up</button> ...
228
+ ```
229
+
230
+ Remember that you can mix and match for any of your needs
231
+
232
+ ### useSelectAtom
233
+
234
+ With `useSelect` can select a specific key from an atom, and still have it live
235
+ update in your react component.
236
+
237
+ ```
238
+ import { Atom, useAtom } from 'chem-rx'
239
+
240
+ const count$ = Atom({ inner: 0 })
241
+
242
+ function Counter() {
243
+ const count = useSelectAtom(count$, 'inner')
244
+ return (
245
+ <h1>
246
+ {count}
247
+ <button onClick={() => count$.set('inner', count + 2)}>one up</button> ...
248
+ ```
249
+
69
250
  ### hydrateAtoms
70
251
 
71
- ## Use with rx.js
252
+ With SSR, your atoms will likely need to be properly hydrated to prevent
253
+ server/client mismatches. You can use `hydrateAtoms` as a simple solution for
254
+ seeding your client-side Atoms with the correct data.
255
+
256
+ ```
257
+ import { Atom, useAtom, hydrateAtoms } from 'chem-rx'
258
+
259
+ const count$ = Atom(0)
260
+ const CounterPage = ({ countFromServer }) => {
261
+ hydrateAtoms([[count$, countFromServer]])
262
+ const count = useAtom(count$)
263
+ // count would be the value of `countFromServer`, not 0.
264
+ }
265
+ ```
266
+
267
+ ## Suggested Usage
268
+
269
+ There are several suggested "patterns" when using Atoms:
270
+
271
+ 1. Suffix all atoms with `$` (for readability).
272
+ 2. Keep all data management **outside** of your views (e.g, React)
273
+ 3. Avoid using `set` and `push` directly from your client components. Instead,
274
+ create helper functions (actions)
275
+ 4. Name your helper actions as `<atomName>$<actionName>`, to easily see what
276
+ atoms are involved.
277
+ 5. Name your derived atoms as `<baseAtom>_<derivedValue>$` to easily see which
278
+ atoms it derives from.
279
+
280
+ ## Common Issues
281
+
282
+ Here are some common issues you might run into when starting out.
283
+
284
+ 1. Keep your atoms in separate files to prevent circular dependencies.
285
+ 1. I typically create a new file for every action, so I can easily see the
286
+ API surface at a glance
287
+
288
+ ## Advanced Usage with `rxjs`
289
+
290
+ Behind the scenes, `chem-rx` uses
291
+ [rxjs Observables](https://rxjs.dev/guide/operators) to enable reactivity.
292
+ `Atom` abstracts away the majority of Rx intentionally, to extract the most
293
+ common patterns used when managing front-end data.
294
+
295
+ If you're coming in with prior experience and are seeking more complex operators
296
+ enabled by Rx, you're in luck, because every Atom is simply a wrapper around a
297
+ `BehaviorSubject`!
298
+
299
+ You can use any rxjs operations you want with `Atom.pipe`, which wraps
300
+ `Observable.pipe` to return an Atom.
301
+
302
+ ```
303
+ import { map } from "rxjs";
304
+
305
+ const atom$ = Atom(3);
306
+
307
+ // Replace `map` with any operators from rxjs
308
+ const squared$: Atom<number> = atom.pipe(map((x) => x * x));
309
+
310
+ // "9"
311
+ console.log(squared$.value());
312
+ ```
72
313
 
73
314
  ## Why...?
74
315
 
75
316
  This library spawned out of a love for the flexibility and expressiveness of
76
- [rx.js](https://github.com/ReactiveX/rxjs) Observables, and the simplicity of
317
+ [rxjs](https://github.com/ReactiveX/rxjs) Observables, and the simplicity of
77
318
  atomic libraries like [jotai](https://github.com/pmndrs/jotai).
78
-
79
- Its primary focus is on ease of use and code cleanliness, and is my go-to
80
- library for all client-side state management
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chem-rx",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "react state primitives powered by rx.js",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/Atom.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  import {
2
2
  BehaviorSubject,
3
3
  combineLatest,
4
+ distinctUntilKeyChanged,
4
5
  isObservable,
5
6
  map,
6
7
  Observable,
7
8
  OperatorFunction,
8
9
  Subscription,
9
10
  } from "rxjs";
10
- import { OverloadedParameters, OverloadedReturnType } from "./types";
11
+ import { OverloadedParameters } from "./types";
11
12
 
12
13
  export type AtomTuple<T> = {
13
14
  [K in keyof T]: ReadOnlyAtom<T[K]>;
@@ -112,7 +113,6 @@ export class ReadOnlyAtom<T> {
112
113
  op9: OperatorFunction<H, I>,
113
114
  ...operations: OperatorFunction<any, any>[]
114
115
  ): ReadOnlyAtom<unknown>;
115
-
116
116
  pipe(
117
117
  ...operations: OverloadedParameters<Observable<T>["pipe"]>
118
118
  ): ReadOnlyAtom<any> {
@@ -123,8 +123,8 @@ export class ReadOnlyAtom<T> {
123
123
  return newAtom;
124
124
  }
125
125
 
126
- transform(transformFn: (value: T, index: number) => any): ReadOnlyAtom<any> {
127
- return this.pipe(map(transformFn));
126
+ derive<A>(deriveFn: (value: T, index: number) => A): ReadOnlyAtom<A> {
127
+ return this.pipe(map(deriveFn));
128
128
  }
129
129
 
130
130
  subscribe(...params: Parameters<BehaviorSubject<T>["subscribe"]>) {
@@ -139,6 +139,45 @@ export class ReadOnlyAtom<T> {
139
139
  dispose() {
140
140
  this._fromObservableSubscription?.unsubscribe();
141
141
  }
142
+
143
+ get(
144
+ key: T extends (infer W)[]
145
+ ? number
146
+ : T extends { [key in keyof T]: infer W }
147
+ ? keyof T
148
+ : undefined
149
+ ): T extends (infer W)[]
150
+ ? T[number]
151
+ : T extends { [key in keyof T]: infer W }
152
+ ? T[keyof T]
153
+ : undefined {
154
+ const val = this.value() as T;
155
+ // @ts-ignore Can't figure out this type so i'm REALLY cheating
156
+ return val[key] as T extends (infer W)[]
157
+ ? T[number]
158
+ : T extends { [key in keyof T]: infer W }
159
+ ? T[keyof T]
160
+ : undefined;
161
+ }
162
+
163
+ select<K extends keyof T>(
164
+ key: K
165
+ ): T[K] extends (infer W)[]
166
+ ? ArrayAtom<W>
167
+ : T[K] extends { [key: string | symbol]: infer W }
168
+ ? ObjectAtom<T[K]>
169
+ : BaseAtom<T[K]> {
170
+ const newObs = this._behavior$.pipe(
171
+ distinctUntilKeyChanged(key),
172
+ map((k) => k?.[key])
173
+ );
174
+ // Can't get typescript to recognize the types here so I'm cheating
175
+ return Atom(newObs) as unknown as T[K] extends (infer W)[]
176
+ ? ArrayAtom<W>
177
+ : T[K] extends { [key: string | symbol]: infer W }
178
+ ? ObjectAtom<T[K]>
179
+ : BaseAtom<T[K]>;
180
+ }
142
181
  }
143
182
 
144
183
  export class BaseAtom<T> extends ReadOnlyAtom<T> {
@@ -148,17 +187,51 @@ export class BaseAtom<T> extends ReadOnlyAtom<T> {
148
187
  }
149
188
 
150
189
  export class ArrayAtom<T> extends ReadOnlyAtom<T[]> {
190
+ constructor(initialValue: T[]) {
191
+ super(initialValue);
192
+ }
193
+
151
194
  push(nextVal: T) {
152
195
  this._behavior$.next([...this._behavior$.getValue(), nextVal]);
153
196
  }
154
- get(idx: number) {
155
- return this.value()[idx];
156
- }
157
197
  }
198
+ //
199
+ // export class ReadOnlyObjectAtom<
200
+ // T extends {
201
+ // [key in K]: V;
202
+ // },
203
+ // K extends string | number | symbol = keyof T,
204
+ // V = T[K]
205
+ // > extends ReadOnlyAtom<T> {
206
+ // get(nextKey: K) {
207
+ // return this.value()[nextKey];
208
+ // }
209
+ //
210
+ // select<K extends keyof T>(
211
+ // key: K
212
+ // ): T[K] extends (infer W)[]
213
+ // ? ArrayAtom<W>
214
+ // : T[K] extends { [key in infer L]?: infer W }
215
+ // ? ObjectAtom<T[K]>
216
+ // : BaseAtom<T> {
217
+ // const newObs = this._behavior$.pipe(
218
+ // distinctUntilKeyChanged(key),
219
+ // map((k) => k?.[key])
220
+ // );
221
+ // // Can't get typescript to recognize the types here so I'm cheating
222
+ // return Atom(newObs) as unknown as T[K] extends (infer W)[]
223
+ // ? ArrayAtom<W>
224
+ // : T[K] extends { [key in infer L]?: infer W }
225
+ // ? ObjectAtom<T[K]>
226
+ // : BaseAtom<T>;
227
+ // }
228
+ // }
158
229
 
159
230
  export class ObjectAtom<
160
- T extends Record<K, V>,
161
- K extends keyof T = keyof T,
231
+ T extends {
232
+ [key in K]: V;
233
+ },
234
+ K extends string | number | symbol = keyof T,
162
235
  V = T[K]
163
236
  > extends ReadOnlyAtom<T> {
164
237
  set(nextKey: K, nextValue: V) {
@@ -167,21 +240,50 @@ export class ObjectAtom<
167
240
  [nextKey]: nextValue,
168
241
  });
169
242
  }
170
- get(nextKey: K) {
171
- return this.value()[nextKey];
172
- }
173
243
  }
174
244
 
175
- export function Atom<T>(value: T[]): ArrayAtom<T>;
176
- export function Atom<T extends object, K extends keyof T = keyof T>(
177
- _value: T
178
- ): ObjectAtom<T>;
179
- export function Atom<T>(_value: Observable<T> | T): BaseAtom<T>;
245
+ // catch-all for developers
246
+ // export type AnyAtom<T> = BaseAtom<T> | ArrayAtom<T> | ObjectAtom<T>;
247
+
248
+ // observable type (primitive)
180
249
  export function Atom<T>(
181
- _value: T | Observable<T>
182
- ): BaseAtom<T> | ArrayAtom<T> | ObjectAtom<T> {
250
+ value: T extends { [key: string]: infer V } | any[] ? never : Observable<T>
251
+ ): BaseAtom<T>;
252
+
253
+ // observable<array> type
254
+ export function Atom<T extends any[]>(
255
+ value: Observable<T>
256
+ ): ArrayAtom<T[number]>;
257
+
258
+ // observable<object> type
259
+ export function Atom<T>(
260
+ value: T extends {
261
+ [key in keyof T]: infer V;
262
+ }
263
+ ? Observable<T>
264
+ : never
265
+ ): ObjectAtom<T>;
266
+
267
+ // array type
268
+ export function Atom<T extends any[]>(value: T): ArrayAtom<T[number]>;
269
+
270
+ // object type
271
+ export function Atom<T extends { [key: string]: T[keyof T] }>(
272
+ value: T
273
+ ): ObjectAtom<T>;
274
+
275
+ // primitive type
276
+ export function Atom<T>(value: T): BaseAtom<T>;
277
+
278
+ // readonly type
279
+ export function Atom<T>(value: T, readOnly?: boolean): ReadOnlyAtom<T>;
280
+
281
+ // function definition
282
+ export function Atom<T>(_value: T, readOnly: boolean = false) {
183
283
  let atom;
184
- if (Array.isArray(_value)) {
284
+ if (readOnly) {
285
+ atom = new ReadOnlyAtom(_value);
286
+ } else if (Array.isArray(_value)) {
185
287
  atom = new ArrayAtom<T>(_value); // For arrays
186
288
  } else if (typeof _value === "object" && _value !== null) {
187
289
  atom = new ObjectAtom<T>(_value); // For objects
package/src/Signal.ts CHANGED
@@ -1,13 +1,6 @@
1
- import {
2
- Subject,
3
- isObservable,
4
- Observable,
5
- OperatorFunction,
6
- Subscription,
7
- } from "rxjs";
8
- import { OverloadedParameters, OverloadedReturnType } from "./types";
1
+ import { Subject } from "rxjs";
9
2
 
10
- export class Signal<T = void> {
3
+ export class BaseSignal<T = any> {
11
4
  _subject$: Subject<T>;
12
5
 
13
6
  constructor() {
@@ -22,9 +15,6 @@ export class Signal<T = void> {
22
15
  subscribe(...params: Parameters<Subject<T>["subscribe"]>) {
23
16
  return this._subject$.subscribe(...params);
24
17
  }
25
-
26
- // not needed?
27
- dispose() {
28
- this._subject$.unsubscribe();
29
- }
30
18
  }
19
+
20
+ export function Signal<T>() {}
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { Atom } from "./Atom";
2
2
  export { useAtom } from "./useAtom";
3
+ export { useSelectAtom } from "./useSelectAtom";
3
4
  export { hydrateAtoms } from "./hydrateAtoms";
4
5
  export { Signal } from "./Signal";
package/src/useAtom.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { useEffect, useState } from "react";
2
- import { Atom } from "./Atom";
2
+ import { ReadOnlyAtom } from "./Atom";
3
3
 
4
- export function useAtom<T>(atom: Atom<T>): T {
5
- const [value, setValue] = useState<T>(atom.getValue());
4
+ export function useAtom<T>(atom: ReadOnlyAtom<T>): T {
5
+ const [value, setValue] = useState<T>(atom.value());
6
6
 
7
7
  useEffect(() => {
8
8
  const subscription = atom.subscribe((val) => {
@@ -0,0 +1,24 @@
1
+ import { useEffect, useState } from "react";
2
+ import { ObjectAtom } from "./Atom";
3
+
4
+ export function useSelectAtom<
5
+ T extends {
6
+ [key in K]: V;
7
+ },
8
+ K extends string | number | symbol = keyof T,
9
+ V = T[K]
10
+ >(atom: ObjectAtom<T>, key: K): T[K] {
11
+ const [value, setValue] = useState<T[K]>(atom.get(key));
12
+
13
+ useEffect(() => {
14
+ const subscription = atom.select(key).subscribe((val) => {
15
+ setValue(val as T[K]);
16
+ });
17
+
18
+ return () => {
19
+ subscription.unsubscribe();
20
+ };
21
+ }, [atom]);
22
+
23
+ return value;
24
+ }
@@ -5,7 +5,7 @@ import {
5
5
  ObjectAtom,
6
6
  ReadOnlyAtom,
7
7
  } from "../src/Atom";
8
- import { map, of } from "rxjs";
8
+ import { BehaviorSubject, map } from "rxjs";
9
9
 
10
10
  test("Base Atom values test", () => {
11
11
  const atom = Atom("aweofij");
@@ -17,6 +17,15 @@ test("Base Atom values test", () => {
17
17
  expect(atom.value()).toBe("apro");
18
18
  });
19
19
 
20
+ test("Test readonly", () => {
21
+ const atom = Atom({ a: 10 }, true);
22
+ expect(atom instanceof ReadOnlyAtom).toBe(true);
23
+ expect(atom instanceof BaseAtom).toBe(false);
24
+ expect(atom.get("a")).toBe(10);
25
+
26
+ expect(atom).not.toHaveProperty("set");
27
+ });
28
+
20
29
  test("Object Atom values test", () => {
21
30
  const atom = Atom<{ [key: string]: string }>({
22
31
  firstKey: "firstValue",
@@ -53,8 +62,27 @@ test("Object Atom get function test", () => {
53
62
  expect(atom.get("thirdKey")).toBe("thirdValue");
54
63
  });
55
64
 
65
+ test("Object Enum Atom test", () => {
66
+ enum testEnum {
67
+ first,
68
+ second,
69
+ }
70
+ const atom = Atom({
71
+ [testEnum.first]: "firstValue",
72
+ [testEnum.second]: "secondValue",
73
+ });
74
+ expect(atom instanceof ObjectAtom).toBe(true);
75
+ expect(atom.get(testEnum.first)).toBe("firstValue");
76
+
77
+ expect(atom.get(testEnum.second)).toBe("secondValue");
78
+ atom.set(testEnum.second, "newSecondValue");
79
+ expect(atom.get(testEnum.second)).toBe("newSecondValue");
80
+ });
81
+
56
82
  test("Array Atom values test", () => {
57
- const atom = Atom(["first"]);
83
+ const atom = Atom<string[]>(["first"]);
84
+ // this is not allowed
85
+ // atom.push(1);
58
86
  expect(atom instanceof ArrayAtom).toBe(true);
59
87
  expect(atom.value().length).toBe(1);
60
88
  expect(atom.value()[0]).toBe("first");
@@ -65,7 +93,8 @@ test("Array Atom values test", () => {
65
93
  });
66
94
 
67
95
  test("Array Atom get index test", () => {
68
- const atom = Atom(["first"]);
96
+ const atom = Atom<string[]>(["first"]);
97
+
69
98
  expect(atom instanceof ArrayAtom).toBe(true);
70
99
  expect(atom.value().length).toBe(1);
71
100
  expect(atom.get(0)).toBe("first");
@@ -80,23 +109,38 @@ test("Test native pipe", () => {
80
109
  expect(atom instanceof BaseAtom).toBe(true);
81
110
  expect(atom.value()).toBe(3);
82
111
 
83
- const transformedAtom = atom.pipe(map((x) => x * x));
84
- expect(transformedAtom instanceof ReadOnlyAtom).toBe(true);
85
- expect(transformedAtom).not.toHaveProperty("push");
112
+ const derivedAtom = atom.pipe(map((x) => x * x));
113
+ expect(derivedAtom instanceof ReadOnlyAtom).toBe(true);
114
+ expect(derivedAtom).not.toHaveProperty("set");
86
115
 
87
- expect(transformedAtom.value()).toBe(9);
116
+ expect(derivedAtom.value()).toBe(9);
88
117
  });
89
118
 
90
- test("Test transform", () => {
119
+ test("Test derive", () => {
91
120
  const atom = Atom(3);
92
121
  expect(atom instanceof BaseAtom).toBe(true);
93
122
  expect(atom.value()).toBe(3);
94
123
 
95
- const transformedAtom = atom.transform((x) => x * x);
96
- expect(transformedAtom instanceof ReadOnlyAtom).toBe(true);
97
- expect(transformedAtom).not.toHaveProperty("push");
124
+ const derivedAtom = atom.derive((x) => x * x);
125
+ expect(derivedAtom instanceof ReadOnlyAtom).toBe(true);
126
+ expect(derivedAtom).not.toHaveProperty("set");
98
127
 
99
- expect(transformedAtom.value()).toBe(9);
128
+ expect(derivedAtom.value()).toBe(9);
129
+ });
130
+
131
+ test("Test derive update", () => {
132
+ const atom = Atom(3);
133
+ expect(atom instanceof BaseAtom).toBe(true);
134
+ expect(atom.value()).toBe(3);
135
+
136
+ const derivedAtom = atom.derive((x) => x * x);
137
+ expect(derivedAtom instanceof ReadOnlyAtom).toBe(true);
138
+ expect(derivedAtom).not.toHaveProperty("set");
139
+
140
+ expect(derivedAtom.value()).toBe(9);
141
+
142
+ atom.set(4);
143
+ expect(derivedAtom.value()).toBe(16);
100
144
  });
101
145
 
102
146
  test("Test combine", () => {
@@ -106,20 +150,199 @@ test("Test combine", () => {
106
150
  c: { name: "c" },
107
151
  });
108
152
  const ids = Atom<string[]>(["a", "b", "c"]);
109
- const combined = Atom.combine(normalizedData, ids).transform(
110
- ([normed, ids]) => {
111
- return ids.map((id) => normed[id]);
112
- }
113
- );
114
- expect(combined).not.toHaveProperty("push");
153
+ const combined = Atom.combine(normalizedData, ids).derive(([normed, ids]) => {
154
+ return ids.map((id) => normed[id]);
155
+ });
156
+ expect(combined).not.toHaveProperty("set");
115
157
 
116
158
  expect(combined instanceof ReadOnlyAtom).toBe(true);
117
159
 
118
160
  expect(combined instanceof ReadOnlyAtom).toBe(true);
119
- expect(combined).not.toHaveProperty("push");
161
+ expect(combined).not.toHaveProperty("set");
120
162
  const combinedValue = combined.value();
121
163
  expect(combinedValue.length).toBe(3);
122
164
  expect(combinedValue[0].name).toBe("a");
123
165
  expect(combinedValue[1].name).toBe("b");
124
166
  expect(combinedValue[2].name).toBe("c");
125
167
  });
168
+
169
+ test("Test combine example", () => {
170
+ const pets$ = Atom<{ [name: string]: { type: "dog" | "cat"; age: number } }>({
171
+ spot: { type: "dog", age: 5 },
172
+ fido: { type: "dog", age: 3 },
173
+ tabby: { type: "cat", age: 12 },
174
+ });
175
+
176
+ const people$ = Atom<{ [name: string]: { pets: string[] } }>({
177
+ fred: { pets: [] },
178
+ mary: { pets: ["spot", "fido"] },
179
+ cam: { pets: ["tabby"] },
180
+ });
181
+
182
+ const mary$ = Atom.combine(pets$, people$.select("mary")).derive(
183
+ ([pets, mary]) => {
184
+ return {
185
+ ...mary,
186
+ pets: mary.pets.map((petName) => pets[petName]),
187
+ };
188
+ }
189
+ );
190
+ {
191
+ mary: {
192
+ pets: [
193
+ {
194
+ name: "spot",
195
+ type: "dog",
196
+ age: 5,
197
+ },
198
+ ];
199
+ }
200
+ }
201
+
202
+ const pets = mary$.select("pets").get(0);
203
+ expect(mary$.select("pets").value().length).toBe(2);
204
+ expect(mary$.select("pets").get(0)).toHaveProperty("type");
205
+ expect(mary$.select("pets").get(0)).toHaveProperty("age");
206
+ expect(mary$.select("pets").get(0).type).toBe("dog");
207
+ expect(mary$.select("pets").get(0).age).toBe(5);
208
+ expect(mary$.select("pets").get(1)).toHaveProperty("type");
209
+ expect(mary$.select("pets").get(1)).toHaveProperty("age");
210
+ expect(mary$.select("pets").get(1).type).toBe("dog");
211
+ expect(mary$.select("pets").get(1).age).toBe(3);
212
+ });
213
+
214
+ test("Test select (simple)", () => {
215
+ enum TEST_ENUM {
216
+ a = "weoifj",
217
+ b = "oh",
218
+ c = "ohoij",
219
+ }
220
+
221
+ // test types
222
+ const enumTest = Atom(TEST_ENUM.a);
223
+
224
+ const arrayTest = Atom([1, 2, 34]);
225
+ const arrayTest2 = Atom([{ a: "jwoi" }, 2, 34]);
226
+
227
+ const arrayAtom = Atom<{ [key: string]: number[] }>({
228
+ a: [0, 1, 2],
229
+ b: [1, 3, 4],
230
+ c: [5, 3, 4],
231
+ });
232
+
233
+ const primitiveNum = new BehaviorSubject(4);
234
+ const primitiveNumAtom = Atom<number>(primitiveNum);
235
+
236
+ const primitiveStr = new BehaviorSubject("a");
237
+ const primitiveStringAtom = Atom<string>(primitiveStr);
238
+
239
+ const arrayObs = new BehaviorSubject(["a"]);
240
+ const arrayObsAtom = Atom<string[]>(arrayObs);
241
+
242
+ const objObs = new BehaviorSubject({ a: 1 });
243
+ const objObsAtom2 = Atom(objObs);
244
+ const objObsAtom1 = Atom<{ [id: string]: number }>(objObs);
245
+
246
+ const stringData = Atom<{
247
+ [key: string]: { [key: string]: string };
248
+ }>({
249
+ a: { a: "a" },
250
+ b: { b: "b" },
251
+ c: { c: "c" },
252
+ });
253
+
254
+ const selectedString: ObjectAtom<{ [key: string]: string }> =
255
+ stringData.select("b");
256
+
257
+ const normalizedOptionalStringData = Atom<{
258
+ [key: string]: { [key in "a" | "b" | "c"]?: string };
259
+ }>({
260
+ a: { a: "a" },
261
+ b: { b: "b" },
262
+ c: { c: "c" },
263
+ });
264
+
265
+ const selectedOptionalString = normalizedOptionalStringData.select("b");
266
+
267
+ const normalizedEnumData = Atom<{
268
+ [key: string]: { [key in TEST_ENUM]?: string };
269
+ }>({
270
+ a: { [TEST_ENUM.a]: "a" },
271
+ b: { [TEST_ENUM.b]: "b" },
272
+ c: { [TEST_ENUM.c]: "c" },
273
+ });
274
+ const selected = normalizedEnumData.select("b");
275
+ const selected2 = normalizedEnumData.select("a");
276
+ const selectedArray = arrayAtom.select("b");
277
+
278
+ // THIS IS HTE LAST ONE THT STILL OES NOTWOK
279
+ const selectedArr = arrayAtom.select("a");
280
+
281
+ const sel3 = objObsAtom2.select("a");
282
+
283
+ expect(selected instanceof ObjectAtom).toBe(true);
284
+
285
+ expect(selected instanceof ObjectAtom).toBe(true);
286
+
287
+ // const combined = Atom.combine(normalizedData, ids).derive(([normed, ids]) => {
288
+ // return ids.map((id) => normed[id]);
289
+ // });
290
+ //
291
+ // expect(combined).not.toHaveProperty("push");
292
+ //
293
+ // expect(combined instanceof ReadOnlyAtom).toBe(true);
294
+ //
295
+ // expect(combined instanceof ReadOnlyAtom).toBe(true);
296
+ // expect(combined).not.toHaveProperty("push");
297
+ // const combinedValue = combined.value();
298
+ // expect(combinedValue.length).toBe(3);
299
+ // expect(combinedValue[0].name).toBe("a");
300
+ // expect(combinedValue[1].name).toBe("b");
301
+ // expect(combinedValue[2].name).toBe("c");
302
+ });
303
+
304
+ test("Test select (nested objects)", () => {
305
+ const nestedData = Atom<{
306
+ [key: string]: {
307
+ nickname: string;
308
+ education: {
309
+ school: string;
310
+ graduation: number;
311
+ };
312
+ };
313
+ }>({
314
+ stacy: {
315
+ nickname: "stace",
316
+ education: {
317
+ school: "Penn",
318
+ graduation: 2014,
319
+ },
320
+ },
321
+ annie: {
322
+ nickname: "ann",
323
+ education: {
324
+ school: "Brown",
325
+ graduation: 2015,
326
+ },
327
+ },
328
+ prabhu: {
329
+ nickname: "prab",
330
+ education: {
331
+ school: "MIT",
332
+ graduation: 2016,
333
+ },
334
+ },
335
+ });
336
+ const stacy = nestedData.select("stacy");
337
+ const stacySchool = nestedData.select("stacy").select("education");
338
+
339
+ expect(stacy.get("nickname")).toBe("stace");
340
+ expect(stacySchool.get("school")).toBe("Penn");
341
+ expect(stacySchool.get("graduation")).toBe(2014);
342
+ });
343
+
344
+ /*
345
+ * TODO:
346
+ * - test react hooks
347
+ * - test subscriptions
348
+ */
@@ -0,0 +1,123 @@
1
+ export class ReadOnlyClass<T> {
2
+ value: T;
3
+ constructor(_value: T | Wrapper<T>) {
4
+ if (isWrapper(_value)) {
5
+ } else {
6
+ this.value = _value;
7
+ }
8
+ }
9
+ }
10
+
11
+ export class ArrayClass<T> extends ReadOnlyClass<T[]> {}
12
+
13
+ export class BaseClass<T> extends ReadOnlyClass<T> {}
14
+
15
+ export class ObjectClass<
16
+ K extends string | number | symbol,
17
+ V,
18
+ T extends {
19
+ [key in K]: V;
20
+ } = {
21
+ [key in K]: V;
22
+ }
23
+ > extends ReadOnlyClass<T> {
24
+ select(
25
+ key: K
26
+ ): T[K] extends { [key: string]: infer V } ? ObjectClass<K, V> : BaseClass<V>;
27
+ select(key: K) {
28
+ const newObs = new Wrapper(this.value[key]);
29
+ return Class(newObs);
30
+ }
31
+ }
32
+
33
+ export class Wrapper<T> {
34
+ val: T;
35
+ constructor(val: T) {
36
+ this.val = val;
37
+ }
38
+ getValue(): T {
39
+ return this.val;
40
+ }
41
+ }
42
+
43
+ function isWrapper(val: any): val is Wrapper<unknown> {
44
+ if (val instanceof Wrapper) {
45
+ return true;
46
+ } else {
47
+ return false;
48
+ }
49
+ }
50
+
51
+ // wrapper type (primitive)
52
+ export function Class<T>(
53
+ value: T extends { [key: string]: infer V } | any[] ? never : Wrapper<T>
54
+ ): BaseClass<T>;
55
+
56
+ // wrapper<array> type
57
+ export function Class<T extends any[]>(
58
+ value: Wrapper<T>
59
+ ): ArrayClass<T[number]>;
60
+
61
+ // wrapper<object> type
62
+ export function Class<T>(
63
+ value: T extends {
64
+ [key in keyof T]: infer V;
65
+ }
66
+ ? Wrapper<T>
67
+ : never
68
+ ): ObjectClass<keyof T, T[keyof T], T>;
69
+
70
+ // array type
71
+ export function Class<T extends any[]>(value: T): ArrayClass<T[number]>;
72
+
73
+ // object type
74
+ export function Class<T extends { [key: string]: T[keyof T] }>(
75
+ value: T
76
+ ): ObjectClass<keyof T, T[keyof T]>;
77
+
78
+ // primitive type
79
+ export function Class<T>(value: T): BaseClass<T>;
80
+
81
+ // function definition
82
+ export function Class<T>(_value: T) {
83
+ let cls;
84
+ if (Array.isArray(_value)) {
85
+ cls = new ArrayClass<T>(_value); // For arrays
86
+ } else if (typeof _value === "object" && _value !== null) {
87
+ cls = new ObjectClass<keyof T, T[keyof T]>(_value); // For objects
88
+ } else {
89
+ cls = new BaseClass(_value); // For other types
90
+ }
91
+ return cls;
92
+ }
93
+
94
+ const a = Class<{ [key: string]: { name: string } }>({
95
+ a: { name: "a" },
96
+ b: { name: "b" },
97
+ c: { name: "c" },
98
+ });
99
+
100
+ // this works
101
+ const numbWrapper = new Wrapper(1);
102
+ const numbClass = Class(numbWrapper); // BaseClass<number>
103
+
104
+ // this works
105
+ const arrayWrapper = new Wrapper(["a"]);
106
+ const arrayWrapperClass = Class<string[]>(arrayWrapper); // ArrayClass<number>
107
+
108
+ // this works
109
+ const objObs = new Wrapper({ a: 1 });
110
+ const objObsClass2 = Class(objObs); // ObjectClass<'a', number, { a: number }>
111
+
112
+ // this does not work.
113
+ const selectedItem = a.select("b"); // BaseClass<{name: string}>
114
+
115
+ /*
116
+ * Why is this of type:
117
+ * const selectedItem: BaseClass<{
118
+ * name: string;
119
+ * }>
120
+ *
121
+ * instead of:
122
+ * const selectedItem: BaseClass<ObjectClass<'name', string>
123
+ */