chem-rx 0.0.17 → 0.0.19

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
@@ -9,55 +9,88 @@ simplicity. Useable with or without React!
9
9
  [jotai](https://github.com/pmndrs/jotai) or
10
10
  [Recoil](https://github.com/facebookexperimental/Recoil).
11
11
 
12
- Atoms are state containers that take any value - object, array, or primitive.
12
+ Atoms are state containers that take any value - object, array, or primitive. You can create them by simply passing a value into `Atom`.
13
13
 
14
14
  ```
15
15
  import { Atom } from 'chem-rx'
16
16
 
17
+ const data$: BaseAtom = Atom('hello')
18
+ ```
19
+
20
+ ## Primitives
21
+
22
+ There are five primitives in `chem-rx`:
23
+
24
+ 1. BaseAtom
25
+ 2. ArrayAtom
26
+ 3. NullableAtom
27
+ 4. ReadOnlyAtom
28
+ 5. Signal
29
+
30
+ Their traits are self-explanatory, and they are generally automatically created for you, depending on how you create your Atom.
31
+
32
+ In its simplest form, `chem-rx` can be used with BaseAtom and ArrayAtom, giving you a primitive for managing atomic data, but can be composed and split in numerous ways for more advanced use cases.
33
+
34
+ ### BaseAtom & ArrayAtom
35
+
36
+ `BaseAtom` is the fundamental type that everything else extends. It contains the primary functionality for interacting with your Atom data.
37
+
38
+ `ArrayAtom` is exactly as it sounds - an atom that holds an array of values (as opposed to an individual)
39
+
40
+ `Atom` will automatically return you an ArrayAtom or BaseAtom based on what you pass it.
41
+
42
+ ```
17
43
  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' })
44
+ const string$: BaseAtom<string> = Atom('hello')
45
+
46
+ // You can skip the type hint on your variable.
47
+ // This returns a `BaseAtom<{hello: string, world: string}>`
48
+ const object$ = Atom({ 'hello': 'world', 'world': 'hello' })
49
+
50
+ // You can enforce a generic type on your atoms
51
+ // Note the ArrayAtom's generic type holds the item type held in the array.
52
+ const array$: ArrayAtom<string> = Atom<string[]>(['hello', 'world'])
21
53
  ```
22
54
 
23
55
  ### Getting & setting values
24
56
 
25
- `Atom` will automatically return an instance of `BaseAtom`, `ArrayAtom`, or
26
- `ObjectAtom` depending on the input.
57
+ `BaseAtom` offers simple helpers to access and modify your data.
27
58
 
28
59
  ```
29
- // Base Atom
30
- number$.set(2)
31
- number$.value()
32
- // 2
60
+ // Primitive values (BaseAtom)
61
+ number$.next(2)
62
+ number$.value() // 2
63
+
64
+
65
+ // Object values (BaseAtom)
66
+ object$.get('hello') // 'world'
67
+ object$.get('fakeKey') // undefined
68
+ object$.set('hello', 'werld')
69
+ object$.get('hello') // 'werld'
70
+
33
71
 
34
72
  // ArrayAtom
35
73
  array$.push('!')
36
- array$.value()
37
- // ['hello', 'world', '!']
38
- array$.get(1)
39
- // 'world'
74
+ array$.value() // ['hello', 'world', '!']
75
+ array$.get(2) // '!'
76
+ array$.get(3) // undefined
77
+ ```
40
78
 
41
- // ObjectAtom
42
- object$.set('world', 'hi')
43
- object$.value()
44
- // {'hello': 'world', 'world': 'hi'}
79
+ ## Composability & ReadOnlyAtom's
45
80
 
46
- object$.set('sup', 'earth')
47
- // {'hello': 'world', 'world': 'hi', 'sup': 'earth'}
81
+ Atoms are intended to be easily composed, split, and transformed to handle complex data needs through a simple API.
48
82
 
49
- object$.get('world')
50
- // 'hi'
51
- ```
83
+ ### Selecting Atoms (read-only)
84
+
85
+ You can `select` keys on `BaseAtom` and `ArrayAtom` that returns an Atom that wrap the values
86
+ at that key. Any time the original atom changes, your selected atom will automatically update with the latest value.
52
87
 
53
- ### Selecting Atoms
88
+ This can be especially useful for working with different parts of nested Array and Object atoms.
54
89
 
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.
90
+ Atoms created with `select` are **read-only** (`ReadOnlyAtom`). This prevents you from modifying original values that the atom was created from.
58
91
 
59
92
  ```
60
- const nestedData = Atom({
93
+ const students = Atom({
61
94
  stacy: {
62
95
  nickname: "stace",
63
96
  education: {
@@ -67,40 +100,48 @@ const nestedData = Atom({
67
100
  },
68
101
  });
69
102
 
70
- const stacy = nestedData.select("stacy");
71
- console.log(stacy.get('nickname'))
72
- // 'stace'
103
+ // ReadOnlyAtom<{nickname: string, education: ...}>
104
+ const stacy = students.select("stacy");
105
+ const stacySchool = stacy.select("education");
106
+
107
+ stacy.get('nickname') // 'stace'
108
+ stacySchool.get('graduation') // 2014
109
+
110
+ students.set("stacy", {
111
+ nickname: "spacey",
112
+ education: {
113
+ ...students.get("stacy").education,
114
+ graduation: 2015,
115
+ },
116
+ });
117
+
118
+ stacy.get('nickname') // 'spacey'
119
+ stacySchool.get('graduation') // 2015
120
+
121
+ // ERR: Property 'set' does not exist on type ReadOnlyAtom
122
+ stacy.set('nickname', 'stacy')
73
123
 
74
- const stacySchool = nestedData.select("stacy").select("education");
75
- console.log(stacySchool.get('school'))
76
- // 'Penn'
77
124
  ```
78
125
 
79
126
  ### Derived Atoms (read-only)
80
127
 
81
128
  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!
129
+ change, your derived atoms will automatically update with new values.
130
+
131
+ Every derived atom is **read-only**. This prevents you from overriding the
132
+ derived output value, since it is automatically derived from another input.
83
133
 
84
134
  ```
85
135
  const atom$ = Atom(3);
86
136
 
87
- // square it
88
137
  const squared$ = atom$.derive((x) => x * x);
89
138
 
90
- // "9"
91
- console.log(squared$.value())
139
+ squared$.value() // "9"
92
140
 
93
- // Update the original value
94
141
  atom$.set(4)
95
142
 
96
- // "16"
97
- console.log(squared$.value())
98
- ```
143
+ squared$.value() // "16"
99
144
 
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
145
  // ERR: Property 'set' does not exist on type ReadOnlyAtom
105
146
  squared$.set(2)
106
147
  ```
@@ -116,7 +157,7 @@ atom$.set(2)
116
157
 
117
158
  ### Combining Atoms
118
159
 
119
- Multiple atoms can also be **combined** to create brand new atoms!
160
+ Multiple atoms can also be **combined** to create brand new atoms.
120
161
 
121
162
  Here's an example of joining a set of normalized data models
122
163
 
@@ -150,6 +191,8 @@ console.log(mary$.select('pets').value())
150
191
  */
151
192
  ```
152
193
 
194
+ ## Pub/Sub
195
+
153
196
  ### Subscribing to updates
154
197
 
155
198
  Atoms emit values each time they're updated. You can subscribe callbacks to them
@@ -162,8 +205,7 @@ const subscription = atom$.subscribe(val => {
162
205
  console.log("Received value: ", val)
163
206
  })
164
207
 
165
- atom$.set(4)
166
- // "Received value: 4"
208
+ atom$.set(4) // "Received value: 4"
167
209
 
168
210
  // Unsubscribe to clean up
169
211
  subscription.unsubscribe();
@@ -181,8 +223,7 @@ const subscription = signal$.subscribe(() => {
181
223
  console.log("PONG")
182
224
  })
183
225
 
184
- signal$.ping()
185
- // "PONG"
226
+ signal$.ping() // "PONG"
186
227
 
187
228
  // Unsubscribe to clean up
188
229
  subscription.unsubscribe();
@@ -204,14 +245,22 @@ signal$.ping("hello")
204
245
  subscription.unsubscribe();
205
246
  ```
206
247
 
248
+ You can selectively subscribe to Signals, and selectively ping subscribers by ID.
249
+
250
+ ```
251
+ const signal = new Signal<string>();
252
+ signal.subscribe(mockCallbackId123, "123");
253
+ signal.subscribe(mockCallbackId456, "456");
254
+ signal.ping("Message for 123", "123");
255
+ ```
256
+
207
257
  ## Use with React
208
258
 
209
259
  ### useAtom
210
260
 
211
261
  `useAtom` automatically updates with new values in your react components.
212
262
 
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
263
+ If you want to update your atoms, you can simply call the same `next`, `set`, or `push` (ArrayAtom) methods you would typically use outside
215
264
  of react.
216
265
 
217
266
  ```
@@ -247,43 +296,81 @@ function Counter() {
247
296
  <button onClick={() => count$.set('inner', count + 2)}>one up</button> ...
248
297
  ```
249
298
 
250
- ### useHydrateAtoms
299
+ ### hydrateAtoms
251
300
 
252
301
  With SSR, your atoms will likely need to be properly hydrated to prevent
253
302
  server/client mismatches. You can use `useHydrateAtoms` as a simple solution for
254
303
  seeding your client-side Atoms with the correct data.
255
304
 
305
+ NOTE: **`hydrateAtoms` caches values and only runs on the initial load by default**, to prevent re-hydration when client-side only changes are made to the component.
306
+
256
307
  ```
257
308
  import { Atom, useAtom, useHydrateAtoms } from 'chem-rx'
258
309
 
259
310
  const count$ = Atom(0)
260
311
  const CounterPage = ({ countFromServer }) => {
261
- useHydrateAtoms([[count$, countFromServer]])
312
+ hydrateAtoms([[count$, countFromServer]])
262
313
  const count = useAtom(count$)
263
314
  // count would be the value of `countFromServer`, not 0.
315
+
316
+ /*
317
+ * ... other code
318
+ */
319
+
320
+ useEffect(() => {
321
+ // This is safe, because hydrateAtoms runs once by default
322
+ count$.next(10)
323
+ }, [otherDeps])
324
+
264
325
  }
265
326
  ```
266
327
 
267
- ## Suggested Usage
328
+ #### `force` hydrateAtoms
268
329
 
269
- There are several suggested "patterns" when using Atoms:
330
+ If you want to force re-hydration, you can optionally pass in a `{force: true}` (for example - you have a top level page wrapper that receives server data )
270
331
 
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.
332
+ ```
333
+ export default function CasesPage({ cases }: Props) {
334
+ // force it to rehydrate with newest value every time this client page is loaded
335
+ hydrateAtoms([[cases$, cases]], { force: true });
336
+ const router = useRouter();
337
+ return (
338
+ <>
339
+ <div className="flex justify-between w-full mb-4 px-6 pt-8">
340
+ <h1 className="text-2xl">Cases</h1>
341
+ <Button
342
+ onClick={() => {
343
+ router.push("/cases/new");
344
+ }}
345
+ >
346
+ <PlusCircle className="w-4 h-4 mr-2" /> Add Cases
347
+ </Button>
348
+ </div>
349
+ <CasesTable />
350
+ </>
351
+ );
352
+ }
353
+ ```
354
+
355
+ In this example, `CasePage` is a top level client component rendering the home page from a SPA, which gets redirected to when coming from another page. We want it to render with the latest server-rendered data.
279
356
 
280
- ## Common Issues
357
+ NOTE: `force` should only be used when the component is expected to only re-render when new data comes from the server - **and never anytime else**.
281
358
 
282
- Here are some common issues you might run into when starting out.
359
+ ## Suggested Usage
360
+
361
+ There are several suggested "patterns" when using Atoms:
283
362
 
284
363
  1. Keep your atoms in separate files to prevent circular dependencies.
285
364
  1. I typically create a new file for every action, so I can easily see the
286
365
  API surface at a glance
366
+ 2. Suffix all atoms with `$` (for readability).
367
+ 3. Keep all data management **outside** of your views (e.g, React)
368
+ 4. Avoid updating atoms (`next`, `set`, and `push`) inside your client components. Instead,
369
+ create an API of helper functions (actions) and call them.
370
+ 5. Name your helper actions as `<atomName>$<actionName>`, to easily see what
371
+ atoms are involved.
372
+ 6. Name your derived atoms as `<baseAtom>_<derivedValue>$` to easily see which
373
+ atoms it derives from.
287
374
 
288
375
  ## Advanced Usage with `rxjs`
289
376
 
@@ -313,6 +400,4 @@ console.log(squared$.value());
313
400
 
314
401
  ## Why...?
315
402
 
316
- This library spawned out of a love for the flexibility and expressiveness of
317
- [rxjs](https://github.com/ReactiveX/rxjs) Observables, and the simplicity of
318
- atomic libraries like [jotai](https://github.com/pmndrs/jotai).
403
+ [`rxjs`](https://github.com/ReactiveX/rxjs) is awesome, and I wanted a framework-agnostic [jotai](https://github.com/pmndrs/jotai)-like solution with a simpler API.
package/dist/Signal.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { Subject } from "rxjs";
2
- export declare class BaseSignal<T = any> {
3
- _subject$: Subject<T>;
1
+ import { Subscription } from "rxjs";
2
+ export declare class Signal<T = any> {
3
+ private _subjects;
4
+ private _defaultSubject;
4
5
  constructor();
5
- ping(value: T): void;
6
- subscribe(...params: Parameters<Subject<T>["subscribe"]>): import("rxjs").Subscription;
6
+ ping(value: T, id?: string): void;
7
+ subscribe(callback: (value: T) => void, id?: string): Subscription;
7
8
  }
8
- export declare function Signal<T>(): void;
9
9
  //# sourceMappingURL=Signal.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Signal.d.ts","sourceRoot":"","sources":["../src/Signal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAE/B,qBAAa,UAAU,CAAC,CAAC,GAAG,GAAG;IAC7B,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;;IAOtB,IAAI,CAAC,KAAK,EAAE,CAAC;IAIb,SAAS,CAAC,GAAG,MAAM,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;CAGzD;AAED,wBAAgB,MAAM,CAAC,CAAC,UAAM"}
1
+ {"version":3,"file":"Signal.d.ts","sourceRoot":"","sources":["../src/Signal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,YAAY,EAAE,MAAM,MAAM,CAAC;AAE7C,qBAAa,MAAM,CAAC,CAAC,GAAG,GAAG;IACzB,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,eAAe,CAAa;;IAOpC,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,MAAM;IAY1B,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,YAAY;CAcnE"}
package/dist/index.cjs.js CHANGED
@@ -323,19 +323,55 @@ function useSelectAtom(atom, key) {
323
323
  }
324
324
 
325
325
  var hydratedAtomsSet = new WeakSet();
326
- function hydrateAtoms(values) {
326
+ function hydrateAtoms(values, options) {
327
327
  for (var _iterator = _createForOfIteratorHelperLoose(values), _step; !(_step = _iterator()).done;) {
328
328
  var _step$value = _step.value,
329
329
  atom = _step$value[0],
330
330
  value = _step$value[1];
331
- if (!hydratedAtomsSet.has(atom)) {
331
+ if (!hydratedAtomsSet.has(atom) || options != null && options.force) {
332
332
  hydratedAtomsSet.add(atom);
333
333
  atom._behavior$.next(value);
334
334
  }
335
335
  }
336
336
  }
337
337
 
338
- function Signal() {}
338
+ var Signal = /*#__PURE__*/function () {
339
+ function Signal() {
340
+ this._subjects = new Map();
341
+ this._defaultSubject = new rxjs.Subject();
342
+ }
343
+ var _proto = Signal.prototype;
344
+ _proto.ping = function ping(value, id) {
345
+ if (id && this._subjects.has(id)) {
346
+ var _this$_subjects$get;
347
+ // If an ID is provided and exists, notify only that subscription.
348
+ (_this$_subjects$get = this._subjects.get(id)) == null ? void 0 : _this$_subjects$get.next(value);
349
+ } else {
350
+ // No ID provided, notify all default subscribers.
351
+ this._defaultSubject.next(value);
352
+ // Additionally, notify all subscriptions as a broadcast.
353
+ this._subjects.forEach(function (subject) {
354
+ return subject.next(value);
355
+ });
356
+ }
357
+ };
358
+ _proto.subscribe = function subscribe(callback, id) {
359
+ if (id) {
360
+ // If an ID is provided, subscribe to the specific ID.
361
+ if (!this._subjects.has(id)) {
362
+ this._subjects.set(id, new rxjs.Subject());
363
+ }
364
+ return this._subjects.get(id).subscribe(callback);
365
+ } else {
366
+ // No ID provided, subscribe to the default subject.
367
+ return this._defaultSubject.subscribe(callback);
368
+ }
369
+ }
370
+
371
+ // Optionally, implement unsubscribe logic to manage subscriptions.
372
+ ;
373
+ return Signal;
374
+ }();
339
375
 
340
376
  exports.Atom = Atom;
341
377
  exports.Signal = Signal;
@@ -321,19 +321,55 @@ var chemicalRx = (function (exports, rxjs, react) {
321
321
  }
322
322
 
323
323
  var hydratedAtomsSet = new WeakSet();
324
- function hydrateAtoms(values) {
324
+ function hydrateAtoms(values, options) {
325
325
  for (var _iterator = _createForOfIteratorHelperLoose(values), _step; !(_step = _iterator()).done;) {
326
326
  var _step$value = _step.value,
327
327
  atom = _step$value[0],
328
328
  value = _step$value[1];
329
- if (!hydratedAtomsSet.has(atom)) {
329
+ if (!hydratedAtomsSet.has(atom) || options != null && options.force) {
330
330
  hydratedAtomsSet.add(atom);
331
331
  atom._behavior$.next(value);
332
332
  }
333
333
  }
334
334
  }
335
335
 
336
- function Signal() {}
336
+ var Signal = /*#__PURE__*/function () {
337
+ function Signal() {
338
+ this._subjects = new Map();
339
+ this._defaultSubject = new rxjs.Subject();
340
+ }
341
+ var _proto = Signal.prototype;
342
+ _proto.ping = function ping(value, id) {
343
+ if (id && this._subjects.has(id)) {
344
+ var _this$_subjects$get;
345
+ // If an ID is provided and exists, notify only that subscription.
346
+ (_this$_subjects$get = this._subjects.get(id)) == null ? void 0 : _this$_subjects$get.next(value);
347
+ } else {
348
+ // No ID provided, notify all default subscribers.
349
+ this._defaultSubject.next(value);
350
+ // Additionally, notify all subscriptions as a broadcast.
351
+ this._subjects.forEach(function (subject) {
352
+ return subject.next(value);
353
+ });
354
+ }
355
+ };
356
+ _proto.subscribe = function subscribe(callback, id) {
357
+ if (id) {
358
+ // If an ID is provided, subscribe to the specific ID.
359
+ if (!this._subjects.has(id)) {
360
+ this._subjects.set(id, new rxjs.Subject());
361
+ }
362
+ return this._subjects.get(id).subscribe(callback);
363
+ } else {
364
+ // No ID provided, subscribe to the default subject.
365
+ return this._defaultSubject.subscribe(callback);
366
+ }
367
+ }
368
+
369
+ // Optionally, implement unsubscribe logic to manage subscriptions.
370
+ ;
371
+ return Signal;
372
+ }();
337
373
 
338
374
  exports.Atom = Atom;
339
375
  exports.Signal = Signal;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { combineLatest, isObservable, BehaviorSubject, map, distinctUntilChanged } from 'rxjs';
1
+ import { combineLatest, isObservable, BehaviorSubject, map, distinctUntilChanged, Subject } from 'rxjs';
2
2
  import { useState, useEffect } from 'react';
3
3
 
4
4
  function _extends() {
@@ -239,15 +239,46 @@ function useSelectAtom(atom, key) {
239
239
  }
240
240
 
241
241
  const hydratedAtomsSet = new WeakSet();
242
- function hydrateAtoms(values) {
242
+ function hydrateAtoms(values, options) {
243
243
  for (const [atom, value] of values) {
244
- if (!hydratedAtomsSet.has(atom)) {
244
+ if (!hydratedAtomsSet.has(atom) || options != null && options.force) {
245
245
  hydratedAtomsSet.add(atom);
246
246
  atom._behavior$.next(value);
247
247
  }
248
248
  }
249
249
  }
250
250
 
251
- function Signal() {}
251
+ class Signal {
252
+ constructor() {
253
+ this._subjects = new Map();
254
+ this._defaultSubject = new Subject();
255
+ }
256
+ ping(value, id) {
257
+ if (id && this._subjects.has(id)) {
258
+ var _this$_subjects$get;
259
+ // If an ID is provided and exists, notify only that subscription.
260
+ (_this$_subjects$get = this._subjects.get(id)) == null ? void 0 : _this$_subjects$get.next(value);
261
+ } else {
262
+ // No ID provided, notify all default subscribers.
263
+ this._defaultSubject.next(value);
264
+ // Additionally, notify all subscriptions as a broadcast.
265
+ this._subjects.forEach(subject => subject.next(value));
266
+ }
267
+ }
268
+ subscribe(callback, id) {
269
+ if (id) {
270
+ // If an ID is provided, subscribe to the specific ID.
271
+ if (!this._subjects.has(id)) {
272
+ this._subjects.set(id, new Subject());
273
+ }
274
+ return this._subjects.get(id).subscribe(callback);
275
+ } else {
276
+ // No ID provided, subscribe to the default subject.
277
+ return this._defaultSubject.subscribe(callback);
278
+ }
279
+ }
280
+
281
+ // Optionally, implement unsubscribe logic to manage subscriptions.
282
+ }
252
283
 
253
284
  export { Atom, Signal, hydrateAtoms, useAtom, useSelectAtom };
@@ -1,3 +1,5 @@
1
1
  import { BaseAtom, NullableBaseAtom } from "./Atom";
2
- export declare function hydrateAtoms(values: readonly [NullableBaseAtom<any> | BaseAtom<any>, any][]): void;
2
+ export declare function hydrateAtoms(values: readonly [NullableBaseAtom<any> | BaseAtom<any>, any][], options?: {
3
+ force?: boolean;
4
+ }): void;
3
5
  //# sourceMappingURL=useHydrateAtoms.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useHydrateAtoms.d.ts","sourceRoot":"","sources":["../src/useHydrateAtoms.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAgB,MAAM,QAAQ,CAAC;AAIlE,wBAAgB,YAAY,CAC1B,MAAM,EAAE,SAAS,CAAC,gBAAgB,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,EAAE,QAQhE"}
1
+ {"version":3,"file":"useHydrateAtoms.d.ts","sourceRoot":"","sources":["../src/useHydrateAtoms.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAgB,MAAM,QAAQ,CAAC;AAIlE,wBAAgB,YAAY,CAC1B,MAAM,EAAE,SAAS,CAAC,gBAAgB,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,EAAE,EAC/D,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,QAQ9B"}
@@ -1 +1 @@
1
- {"version":3,"file":"useSelectAtom.d.ts","sourceRoot":"","sources":["../src/useSelectAtom.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAElE,wBAAgB,aAAa,CAC3B,CAAC,SAAS;KACP,GAAG,IAAI,CAAC,GAAG,CAAC;CACd,EACD,CAAC,SAAS,MAAM,CAAC,EACjB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EACR,IAAI,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAczE"}
1
+ {"version":3,"file":"useSelectAtom.d.ts","sourceRoot":"","sources":["../src/useSelectAtom.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAElE,wBAAgB,aAAa,CAC3B,CAAC,SACG;KACG,GAAG,IAAI,CAAC,GAAG,CAAC;CACd,EACL,CAAC,SAAS,MAAM,CAAC,EACjB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EACR,IAAI,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAczE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chem-rx",
3
- "version": "0.0.17",
3
+ "version": "0.0.19",
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/Signal.ts CHANGED
@@ -1,20 +1,38 @@
1
- import { Subject } from "rxjs";
1
+ import { Subject, Subscription } from "rxjs";
2
2
 
3
- export class BaseSignal<T = any> {
4
- _subject$: Subject<T>;
3
+ export class Signal<T = any> {
4
+ private _subjects: Map<string, Subject<T>>;
5
+ private _defaultSubject: Subject<T>;
5
6
 
6
7
  constructor() {
7
- // if it's just a value just use a regular behavior subject
8
- this._subject$ = new Subject<T>();
8
+ this._subjects = new Map();
9
+ this._defaultSubject = new Subject<T>();
9
10
  }
10
11
 
11
- ping(value: T) {
12
- this._subject$.next(value);
12
+ ping(value: T, id?: string) {
13
+ if (id && this._subjects.has(id)) {
14
+ // If an ID is provided and exists, notify only that subscription.
15
+ this._subjects.get(id)?.next(value);
16
+ } else {
17
+ // No ID provided, notify all default subscribers.
18
+ this._defaultSubject.next(value);
19
+ // Additionally, notify all subscriptions as a broadcast.
20
+ this._subjects.forEach((subject) => subject.next(value));
21
+ }
13
22
  }
14
23
 
15
- subscribe(...params: Parameters<Subject<T>["subscribe"]>) {
16
- return this._subject$.subscribe(...params);
24
+ subscribe(callback: (value: T) => void, id?: string): Subscription {
25
+ if (id) {
26
+ // If an ID is provided, subscribe to the specific ID.
27
+ if (!this._subjects.has(id)) {
28
+ this._subjects.set(id, new Subject<T>());
29
+ }
30
+ return this._subjects.get(id)!.subscribe(callback);
31
+ } else {
32
+ // No ID provided, subscribe to the default subject.
33
+ return this._defaultSubject.subscribe(callback);
34
+ }
17
35
  }
18
- }
19
36
 
20
- export function Signal<T>() {}
37
+ // Optionally, implement unsubscribe logic to manage subscriptions.
38
+ }
@@ -4,10 +4,11 @@ import { BaseAtom, NullableBaseAtom, ReadOnlyAtom } from "./Atom";
4
4
  const hydratedAtomsSet: WeakSet<ReadOnlyAtom<any>> = new WeakSet();
5
5
 
6
6
  export function hydrateAtoms(
7
- values: readonly [NullableBaseAtom<any> | BaseAtom<any>, any][]
7
+ values: readonly [NullableBaseAtom<any> | BaseAtom<any>, any][],
8
+ options?: { force?: boolean }
8
9
  ) {
9
10
  for (const [atom, value] of values) {
10
- if (!hydratedAtomsSet.has(atom)) {
11
+ if (!hydratedAtomsSet.has(atom) || options?.force) {
11
12
  hydratedAtomsSet.add(atom);
12
13
  atom._behavior$.next(value);
13
14
  }
@@ -2,9 +2,10 @@ import { useEffect, useState } from "react";
2
2
  import { BaseAtom, NullableBaseAtom, ReadOnlyAtom } from "./Atom";
3
3
 
4
4
  export function useSelectAtom<
5
- T extends {
6
- [key in K]: V;
7
- },
5
+ T extends
6
+ | {
7
+ [key in K]: V;
8
+ },
8
9
  K extends keyof T,
9
10
  V = T[K]
10
11
  >(atom: NullableBaseAtom<T> | BaseAtom<T> | ReadOnlyAtom<T>, key: K): T[K] {
@@ -1,4 +1,5 @@
1
1
  import { ArrayAtom, Atom, BaseAtom, ReadOnlyAtom } from "../src/Atom";
2
+ import { Signal } from "../src/Signal";
2
3
  import { BehaviorSubject, map } from "rxjs";
3
4
 
4
5
  test("Base Atom values test", () => {
@@ -171,6 +172,8 @@ test("Array Atom get index test", () => {
171
172
  atom.push("second");
172
173
  expect(atom.value().length).toBe(2);
173
174
  expect(atom.get(1)).toBe("second");
175
+
176
+ expect(atom.get(2)).toBe(undefined);
174
177
  });
175
178
 
176
179
  test("Test native pipe", () => {
@@ -415,6 +418,58 @@ test("Test select (nested objects)", () => {
415
418
  expect(stacySchool.get("graduation")).toBe(2014);
416
419
  });
417
420
 
421
+ test("Test parent value when updating child objects ", () => {
422
+ const students = Atom<{
423
+ [key: string]: {
424
+ nickname: string;
425
+ education: {
426
+ school: string;
427
+ graduation: number;
428
+ };
429
+ };
430
+ }>({
431
+ stacy: {
432
+ nickname: "stace",
433
+ education: {
434
+ school: "Penn",
435
+ graduation: 2014,
436
+ },
437
+ },
438
+ annie: {
439
+ nickname: "ann",
440
+ education: {
441
+ school: "Brown",
442
+ graduation: 2015,
443
+ },
444
+ },
445
+ prabhu: {
446
+ nickname: "prab",
447
+ education: {
448
+ school: "MIT",
449
+ graduation: 2016,
450
+ },
451
+ },
452
+ });
453
+ const stacy = students.select("stacy");
454
+ const stacySchool = stacy.select("education");
455
+
456
+ expect(stacy.get("nickname")).toBe("stace");
457
+ expect(students.get("stacy").nickname).toBe("stace");
458
+ expect(stacySchool.get("graduation")).toBe(2014);
459
+
460
+ students.set("stacy", {
461
+ nickname: "spacey",
462
+ education: {
463
+ ...students.get("stacy").education,
464
+ graduation: 2015,
465
+ },
466
+ });
467
+
468
+ expect(stacy.get("nickname")).toBe("spacey");
469
+ expect(students.get("stacy").nickname).toBe("spacey");
470
+ expect(stacySchool.get("graduation")).toBe(2015);
471
+ });
472
+
418
473
  test("Test nullable object", () => {
419
474
  const nestedData = Atom<{
420
475
  [key: string]: {
@@ -486,6 +541,83 @@ test("Test select nullable object", () => {
486
541
  expect(stacySchool.get("graduation")).toBe(2014);
487
542
  });
488
543
 
544
+ describe("SignalWithId", () => {
545
+ it("should broadcast messages to all subscribers when no ID is provided", (done) => {
546
+ const signal = new Signal<string>();
547
+ const mockCallback = jest.fn();
548
+
549
+ signal.subscribe(mockCallback);
550
+ signal.subscribe(mockCallback);
551
+
552
+ signal.ping("test message");
553
+
554
+ setImmediate(() => {
555
+ expect(mockCallback).toHaveBeenCalledTimes(2);
556
+ expect(mockCallback).toHaveBeenCalledWith("test message");
557
+ done();
558
+ });
559
+ });
560
+
561
+ it("should send messages only to subscribers with the specific ID", (done) => {
562
+ const signal = new Signal<string>();
563
+ const mockCallbackWithId = jest.fn();
564
+ const mockCallbackWithoutId = jest.fn();
565
+
566
+ signal.subscribe(mockCallbackWithId, "123");
567
+ signal.subscribe(mockCallbackWithoutId);
568
+
569
+ signal.ping("ID specific message", "123");
570
+
571
+ setImmediate(() => {
572
+ expect(mockCallbackWithId).toHaveBeenCalledTimes(1);
573
+ expect(mockCallbackWithId).toHaveBeenCalledWith("ID specific message");
574
+ expect(mockCallbackWithoutId).toHaveBeenCalledTimes(0);
575
+ done();
576
+ });
577
+ });
578
+
579
+ it("should not notify subscribers with different IDs", (done) => {
580
+ const signal = new Signal<string>();
581
+ const mockCallbackId123 = jest.fn();
582
+ const mockCallbackId456 = jest.fn();
583
+
584
+ signal.subscribe(mockCallbackId123, "123");
585
+ signal.subscribe(mockCallbackId456, "456");
586
+
587
+ signal.ping("Message for 123", "123");
588
+
589
+ setImmediate(() => {
590
+ expect(mockCallbackId123).toHaveBeenCalledTimes(1);
591
+ expect(mockCallbackId123).toHaveBeenCalledWith("Message for 123");
592
+ expect(mockCallbackId456).toHaveBeenCalledTimes(0);
593
+ done();
594
+ });
595
+ });
596
+
597
+ it("should notify all subscribers, including those with specific IDs, when pinging without an ID", (done) => {
598
+ const signal = new Signal<string>();
599
+ const mockCallback = jest.fn();
600
+ const mockCallbackWithId = jest.fn();
601
+
602
+ // Subscribe one callback without ID (for general broadcast)
603
+ signal.subscribe(mockCallback);
604
+ // Subscribe another callback with a specific ID
605
+ signal.subscribe(mockCallbackWithId, "123");
606
+
607
+ // Ping without specifying an ID should notify both subscribers
608
+ signal.ping("broadcast message");
609
+
610
+ setImmediate(() => {
611
+ // Both callbacks should be called once
612
+ expect(mockCallback).toHaveBeenCalledTimes(1);
613
+ expect(mockCallback).toHaveBeenCalledWith("broadcast message");
614
+ expect(mockCallbackWithId).toHaveBeenCalledTimes(1);
615
+ expect(mockCallbackWithId).toHaveBeenCalledWith("broadcast message");
616
+ done();
617
+ });
618
+ });
619
+ });
620
+
489
621
  /*
490
622
  * TODO:
491
623
  * - test react hooks