@jscutlery/rx-computed 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # `@jscutlery/rx-computed`
2
+
3
+ # Installation
4
+
5
+ ```sh
6
+ yarn add @jscutlery/rx-computed
7
+
8
+ # or
9
+
10
+ npm install @jscutlery/rx-computed
11
+ ```
12
+
13
+ # `rxComputed`
14
+
15
+ ## Usage
16
+ ```typescript
17
+ import { rxComputed } from '@jscutlery/rx-computed';
18
+
19
+ @Component({
20
+ ...
21
+ template: `
22
+ <mc-keywords-input (keywordsChange)="keywords.set($event)"/>
23
+ <mc-sort-input (sortChange)="sort.set($event)"/>
24
+ <mc-recipes-list [recipes]="recipes()"/>
25
+ `
26
+ })
27
+ class MyCmp {
28
+ keywords = signal<string | undefined>(undefined);
29
+ sort = signal<'asc' | 'desc'>('desc');
30
+
31
+ recipes = rxComputed(() => repo.getRecipes(keywords(), sort()), {initialValue: []});
32
+ }
33
+ ```
34
+
35
+ ## Motivation
36
+
37
+ Synchronously computed signals in Angular are relatively straight forward. However, when dealing with an asynchronous source of data like an `Observable`, there is no primitive to derive a signal from it.
38
+
39
+ There are two common ways of dealing with this:
40
+
41
+ 1. Using `@angular/core/rxjs-interop` which requires us to explicitly define the dependencies of the computed signal:
42
+
43
+ ```typescript
44
+ // Signals
45
+ const keywords = signal(...);
46
+ const sort = signal(...);
47
+
48
+ // Signals => RxJS
49
+ const recipes$ = combineLatest({
50
+ keywords: toObservable(keywords),
51
+ sort: toObservable(sort)
52
+ }).pipe(
53
+ switchMap(({ keywords, sort }) => repo.getRecipes(keywords, sort))
54
+ );
55
+
56
+ // RxJS => Signals
57
+ const recipes = toSignal(recipes$, []);
58
+ ```
59
+
60
+ 2. Using `effet` explicitly _(which is not the recommended way of using it, cf. https://angular.io/guide/signals#when-not-to-use-effects)_:
61
+
62
+ ```typescript
63
+ const keywords = signal(...);
64
+ const sort = signal(...);
65
+
66
+ const recipes = signal(...);
67
+
68
+ effect((onCleanup) => {
69
+ const sub = repo.getRecipes(keywords(), sort())
70
+ .subscribe(_recipes => recipes.set(_recipes));
71
+
72
+ onCleanup(() => sub.unsubscribe());
73
+ });
74
+ ```
75
+
76
+ ## FAQ
77
+
78
+ ### What about errors?
79
+
80
+ Cf. [Managing RxJS Traffic with Signals and Suspensify](https://marmicode.io/blog/managing-rxjs-traffic-with-signals)
81
+
82
+ Cf. [@jscutlery/operators#suspensify](../operators/README.md#suspensify)
@@ -0,0 +1,2 @@
1
+ export { rxComputed } from './lib/rx-computed';
2
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi9wYWNrYWdlcy9yeC1jb21wdXRlZC9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLG1CQUFtQixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IHsgcnhDb21wdXRlZCB9IGZyb20gJy4vbGliL3J4LWNvbXB1dGVkJzsiXX0=
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Generated bundle index. Do not edit.
3
+ */
4
+ export * from './index';
5
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoianNjdXRsZXJ5LXJ4LWNvbXB1dGVkLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vcGFja2FnZXMvcngtY29tcHV0ZWQvc3JjL2pzY3V0bGVyeS1yeC1jb21wdXRlZC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7R0FFRztBQUVILGNBQWMsU0FBUyxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBHZW5lcmF0ZWQgYnVuZGxlIGluZGV4LiBEbyBub3QgZWRpdC5cbiAqL1xuXG5leHBvcnQgKiBmcm9tICcuL2luZGV4JztcbiJdfQ==
@@ -0,0 +1,22 @@
1
+ import { computed, effect, signal } from '@angular/core';
2
+ import { pending, suspensify } from '@jscutlery/operators';
3
+ export function rxComputed(fn, { initialValue } = {}) {
4
+ const sig = signal(pending);
5
+ effect((onCleanup) => {
6
+ const sub = fn()
7
+ .pipe(suspensify())
8
+ .subscribe((value) => sig.set(value));
9
+ onCleanup(() => sub.unsubscribe());
10
+ }, { allowSignalWrites: true });
11
+ return computed(() => {
12
+ const suspense = sig();
13
+ if (suspense.hasError) {
14
+ throw suspense.error;
15
+ }
16
+ if (suspense.hasValue) {
17
+ return suspense.value;
18
+ }
19
+ return initialValue;
20
+ });
21
+ }
22
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicngtY29tcHV0ZWQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi9wYWNrYWdlcy9yeC1jb21wdXRlZC9zcmMvbGliL3J4LWNvbXB1dGVkLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxRQUFRLEVBQUUsTUFBTSxFQUFFLE1BQU0sRUFBVSxNQUFNLGVBQWUsQ0FBQztBQUNqRSxPQUFPLEVBQUUsT0FBTyxFQUFZLFVBQVUsRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBR3JFLE1BQU0sVUFBVSxVQUFVLENBQ3hCLEVBQXVCLEVBQ3ZCLEVBQUUsWUFBWSxLQUF1QyxFQUFFO0lBRXZELE1BQU0sR0FBRyxHQUFHLE1BQU0sQ0FBOEIsT0FBTyxDQUFDLENBQUM7SUFFekQsTUFBTSxDQUNKLENBQUMsU0FBUyxFQUFFLEVBQUU7UUFDWixNQUFNLEdBQUcsR0FBRyxFQUFFLEVBQUU7YUFDYixJQUFJLENBQUMsVUFBVSxFQUFFLENBQUM7YUFDbEIsU0FBUyxDQUFDLENBQUMsS0FBSyxFQUFFLEVBQUUsQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUM7UUFDeEMsU0FBUyxDQUFDLEdBQUcsRUFBRSxDQUFDLEdBQUcsQ0FBQyxXQUFXLEVBQUUsQ0FBQyxDQUFDO0lBQ3JDLENBQUMsRUFDRCxFQUFFLGlCQUFpQixFQUFFLElBQUksRUFBRSxDQUM1QixDQUFDO0lBRUYsT0FBTyxRQUFRLENBQUMsR0FBRyxFQUFFO1FBQ25CLE1BQU0sUUFBUSxHQUFHLEdBQUcsRUFBRSxDQUFDO1FBQ3ZCLElBQUksUUFBUSxDQUFDLFFBQVEsRUFBRTtZQUNyQixNQUFNLFFBQVEsQ0FBQyxLQUFLLENBQUM7U0FDdEI7UUFFRCxJQUFJLFFBQVEsQ0FBQyxRQUFRLEVBQUU7WUFDckIsT0FBTyxRQUFRLENBQUMsS0FBSyxDQUFDO1NBQ3ZCO1FBRUQsT0FBTyxZQUE2QixDQUFDO0lBQ3ZDLENBQUMsQ0FBQyxDQUFDO0FBQ0wsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IGNvbXB1dGVkLCBlZmZlY3QsIHNpZ25hbCwgU2lnbmFsIH0gZnJvbSAnQGFuZ3VsYXIvY29yZSc7XG5pbXBvcnQgeyBwZW5kaW5nLCBTdXNwZW5zZSwgc3VzcGVuc2lmeSB9IGZyb20gJ0Bqc2N1dGxlcnkvb3BlcmF0b3JzJztcbmltcG9ydCB7IE9ic2VydmFibGUgfSBmcm9tICdyeGpzJztcblxuZXhwb3J0IGZ1bmN0aW9uIHJ4Q29tcHV0ZWQ8VCwgSU5JVElBTF9WQUxVRSA9IFQgfCBudWxsIHwgdW5kZWZpbmVkPihcbiAgZm46ICgpID0+IE9ic2VydmFibGU8VD4sXG4gIHsgaW5pdGlhbFZhbHVlIH06IHsgaW5pdGlhbFZhbHVlPzogSU5JVElBTF9WQUxVRSB9ID0ge31cbik6IFNpZ25hbDxUIHwgSU5JVElBTF9WQUxVRT4ge1xuICBjb25zdCBzaWcgPSBzaWduYWw8U3VzcGVuc2U8VCB8IElOSVRJQUxfVkFMVUU+PihwZW5kaW5nKTtcblxuICBlZmZlY3QoXG4gICAgKG9uQ2xlYW51cCkgPT4ge1xuICAgICAgY29uc3Qgc3ViID0gZm4oKVxuICAgICAgICAucGlwZShzdXNwZW5zaWZ5KCkpXG4gICAgICAgIC5zdWJzY3JpYmUoKHZhbHVlKSA9PiBzaWcuc2V0KHZhbHVlKSk7XG4gICAgICBvbkNsZWFudXAoKCkgPT4gc3ViLnVuc3Vic2NyaWJlKCkpO1xuICAgIH0sXG4gICAgeyBhbGxvd1NpZ25hbFdyaXRlczogdHJ1ZSB9XG4gICk7XG5cbiAgcmV0dXJuIGNvbXB1dGVkKCgpID0+IHtcbiAgICBjb25zdCBzdXNwZW5zZSA9IHNpZygpO1xuICAgIGlmIChzdXNwZW5zZS5oYXNFcnJvcikge1xuICAgICAgdGhyb3cgc3VzcGVuc2UuZXJyb3I7XG4gICAgfVxuXG4gICAgaWYgKHN1c3BlbnNlLmhhc1ZhbHVlKSB7XG4gICAgICByZXR1cm4gc3VzcGVuc2UudmFsdWU7XG4gICAgfVxuXG4gICAgcmV0dXJuIGluaXRpYWxWYWx1ZSBhcyBJTklUSUFMX1ZBTFVFO1xuICB9KTtcbn0iXX0=
@@ -0,0 +1,29 @@
1
+ import { signal, effect, computed } from '@angular/core';
2
+ import { pending, suspensify } from '@jscutlery/operators';
3
+
4
+ function rxComputed(fn, { initialValue } = {}) {
5
+ const sig = signal(pending);
6
+ effect((onCleanup) => {
7
+ const sub = fn()
8
+ .pipe(suspensify())
9
+ .subscribe((value) => sig.set(value));
10
+ onCleanup(() => sub.unsubscribe());
11
+ }, { allowSignalWrites: true });
12
+ return computed(() => {
13
+ const suspense = sig();
14
+ if (suspense.hasError) {
15
+ throw suspense.error;
16
+ }
17
+ if (suspense.hasValue) {
18
+ return suspense.value;
19
+ }
20
+ return initialValue;
21
+ });
22
+ }
23
+
24
+ /**
25
+ * Generated bundle index. Do not edit.
26
+ */
27
+
28
+ export { rxComputed };
29
+ //# sourceMappingURL=jscutlery-rx-computed.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jscutlery-rx-computed.mjs","sources":["../../../../packages/rx-computed/src/lib/rx-computed.ts","../../../../packages/rx-computed/src/jscutlery-rx-computed.ts"],"sourcesContent":["import { computed, effect, signal, Signal } from '@angular/core';\nimport { pending, Suspense, suspensify } from '@jscutlery/operators';\nimport { Observable } from 'rxjs';\n\nexport function rxComputed<T, INITIAL_VALUE = T | null | undefined>(\n fn: () => Observable<T>,\n { initialValue }: { initialValue?: INITIAL_VALUE } = {}\n): Signal<T | INITIAL_VALUE> {\n const sig = signal<Suspense<T | INITIAL_VALUE>>(pending);\n\n effect(\n (onCleanup) => {\n const sub = fn()\n .pipe(suspensify())\n .subscribe((value) => sig.set(value));\n onCleanup(() => sub.unsubscribe());\n },\n { allowSignalWrites: true }\n );\n\n return computed(() => {\n const suspense = sig();\n if (suspense.hasError) {\n throw suspense.error;\n }\n\n if (suspense.hasValue) {\n return suspense.value;\n }\n\n return initialValue as INITIAL_VALUE;\n });\n}","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;AAIM,SAAU,UAAU,CACxB,EAAuB,EACvB,EAAE,YAAY,KAAuC,EAAE,EAAA;AAEvD,IAAA,MAAM,GAAG,GAAG,MAAM,CAA8B,OAAO,CAAC,CAAC;AAEzD,IAAA,MAAM,CACJ,CAAC,SAAS,KAAI;QACZ,MAAM,GAAG,GAAG,EAAE,EAAE;aACb,IAAI,CAAC,UAAU,EAAE,CAAC;AAClB,aAAA,SAAS,CAAC,CAAC,KAAK,KAAK,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;QACxC,SAAS,CAAC,MAAM,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC;AACrC,KAAC,EACD,EAAE,iBAAiB,EAAE,IAAI,EAAE,CAC5B,CAAC;IAEF,OAAO,QAAQ,CAAC,MAAK;AACnB,QAAA,MAAM,QAAQ,GAAG,GAAG,EAAE,CAAC;QACvB,IAAI,QAAQ,CAAC,QAAQ,EAAE;YACrB,MAAM,QAAQ,CAAC,KAAK,CAAC;AACtB,SAAA;QAED,IAAI,QAAQ,CAAC,QAAQ,EAAE;YACrB,OAAO,QAAQ,CAAC,KAAK,CAAC;AACvB,SAAA;AAED,QAAA,OAAO,YAA6B,CAAC;AACvC,KAAC,CAAC,CAAC;AACL;;AChCA;;AAEG;;;;"}
package/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { rxComputed } from './lib/rx-computed';
@@ -0,0 +1,5 @@
1
+ import { Signal } from '@angular/core';
2
+ import { Observable } from 'rxjs';
3
+ export declare function rxComputed<T, INITIAL_VALUE = T | null | undefined>(fn: () => Observable<T>, { initialValue }?: {
4
+ initialValue?: INITIAL_VALUE;
5
+ }): Signal<T | INITIAL_VALUE>;
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@jscutlery/rx-computed",
3
+ "version": "0.1.0",
4
+ "peerDependencies": {
5
+ "@angular/common": "^16.0.0",
6
+ "@angular/core": "^16.0.0",
7
+ "rxjs": "7.8.1",
8
+ "@jscutlery/operators": "2.1.2"
9
+ },
10
+ "dependencies": {
11
+ "tslib": "^2.4.0"
12
+ },
13
+ "sideEffects": false,
14
+ "module": "fesm2022/jscutlery-rx-computed.mjs",
15
+ "typings": "index.d.ts",
16
+ "exports": {
17
+ "./package.json": {
18
+ "default": "./package.json"
19
+ },
20
+ ".": {
21
+ "types": "./index.d.ts",
22
+ "esm2022": "./esm2022/jscutlery-rx-computed.mjs",
23
+ "esm": "./esm2022/jscutlery-rx-computed.mjs",
24
+ "default": "./fesm2022/jscutlery-rx-computed.mjs"
25
+ }
26
+ }
27
+ }