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 +155 -70
- package/dist/Signal.d.ts +6 -6
- package/dist/Signal.d.ts.map +1 -1
- package/dist/index.cjs.js +39 -3
- package/dist/index.iife.js +39 -3
- package/dist/index.js +35 -4
- package/dist/useHydrateAtoms.d.ts +3 -1
- package/dist/useHydrateAtoms.d.ts.map +1 -1
- package/dist/useSelectAtom.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Signal.ts +29 -11
- package/src/useHydrateAtoms.ts +3 -2
- package/src/useSelectAtom.ts +4 -3
- package/tests/atom.test.ts +132 -0
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
|
-
|
|
20
|
-
|
|
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
|
-
`
|
|
26
|
-
`ObjectAtom` depending on the input.
|
|
57
|
+
`BaseAtom` offers simple helpers to access and modify your data.
|
|
27
58
|
|
|
28
59
|
```
|
|
29
|
-
//
|
|
30
|
-
number$.
|
|
31
|
-
number$.value()
|
|
32
|
-
|
|
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
|
-
//
|
|
38
|
-
array$.get(
|
|
39
|
-
|
|
74
|
+
array$.value() // ['hello', 'world', '!']
|
|
75
|
+
array$.get(2) // '!'
|
|
76
|
+
array$.get(3) // undefined
|
|
77
|
+
```
|
|
40
78
|
|
|
41
|
-
|
|
42
|
-
object$.set('world', 'hi')
|
|
43
|
-
object$.value()
|
|
44
|
-
// {'hello': 'world', 'world': 'hi'}
|
|
79
|
+
## Composability & ReadOnlyAtom's
|
|
45
80
|
|
|
46
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
88
|
+
This can be especially useful for working with different parts of nested Array and Object atoms.
|
|
54
89
|
|
|
55
|
-
|
|
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
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
328
|
+
#### `force` hydrateAtoms
|
|
268
329
|
|
|
269
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
2
|
-
export declare class
|
|
3
|
-
|
|
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(
|
|
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
|
package/dist/Signal.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Signal.d.ts","sourceRoot":"","sources":["../src/Signal.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
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
|
|
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;
|
package/dist/index.iife.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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][]
|
|
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,
|
|
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,
|
|
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
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
|
|
4
|
-
|
|
3
|
+
export class Signal<T = any> {
|
|
4
|
+
private _subjects: Map<string, Subject<T>>;
|
|
5
|
+
private _defaultSubject: Subject<T>;
|
|
5
6
|
|
|
6
7
|
constructor() {
|
|
7
|
-
|
|
8
|
-
this.
|
|
8
|
+
this._subjects = new Map();
|
|
9
|
+
this._defaultSubject = new Subject<T>();
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
ping(value: T) {
|
|
12
|
-
this.
|
|
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(
|
|
16
|
-
|
|
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
|
-
|
|
37
|
+
// Optionally, implement unsubscribe logic to manage subscriptions.
|
|
38
|
+
}
|
package/src/useHydrateAtoms.ts
CHANGED
|
@@ -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
|
}
|
package/src/useSelectAtom.ts
CHANGED
|
@@ -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
|
-
|
|
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] {
|
package/tests/atom.test.ts
CHANGED
|
@@ -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
|