@jscutlery/rx-computed 0.1.0 → 0.2.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 CHANGED
@@ -13,6 +13,7 @@ npm install @jscutlery/rx-computed
13
13
  # `rxComputed`
14
14
 
15
15
  ## Usage
16
+
16
17
  ```typescript
17
18
  import { rxComputed } from '@jscutlery/rx-computed';
18
19
 
@@ -32,9 +33,25 @@ class MyCmp {
32
33
  }
33
34
  ```
34
35
 
36
+ ### Custom Injector
37
+
38
+ {::comment}TODO: Link Injection Context documentation when it is live. https://github.com/angular/angular/pull/49782 {:/comment}
39
+ `rxComputed` utilizes `effect()` underneath so it is **required** to be invoked within an Injection Context. However, a custom `Injector`
40
+ can be passed in and `rxComputed` will invoke within that Injector context.
41
+
42
+ ```ts
43
+ class MyCmp {
44
+ injector = inject(Injector);
45
+
46
+ ngOnInit() {
47
+ const value = rxComputed(() => of(16), { injector: this.injector });
48
+ }
49
+ }
50
+ ```
51
+
35
52
  ## Motivation
36
53
 
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.
54
+ 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
55
 
39
56
  There are two common ways of dealing with this:
40
57
 
@@ -79,4 +96,4 @@ effect((onCleanup) => {
79
96
 
80
97
  Cf. [Managing RxJS Traffic with Signals and Suspensify](https://marmicode.io/blog/managing-rxjs-traffic-with-signals)
81
98
 
82
- Cf. [@jscutlery/operators#suspensify](../operators/README.md#suspensify)
99
+ Cf. [@jscutlery/operators#suspensify](../operators/README.md#suspensify)
@@ -1,13 +1,16 @@
1
- import { computed, effect, signal } from '@angular/core';
1
+ import { assertInInjectionContext, computed, effect, inject, Injector, signal, } from '@angular/core';
2
2
  import { pending, suspensify } from '@jscutlery/operators';
3
- export function rxComputed(fn, { initialValue } = {}) {
3
+ export function rxComputed(fn, { initialValue, injector, } = {}) {
4
+ if (!injector)
5
+ assertInInjectionContext(rxComputed);
6
+ injector ??= inject(Injector);
4
7
  const sig = signal(pending);
5
8
  effect((onCleanup) => {
6
9
  const sub = fn()
7
10
  .pipe(suspensify())
8
11
  .subscribe((value) => sig.set(value));
9
12
  onCleanup(() => sub.unsubscribe());
10
- }, { allowSignalWrites: true });
13
+ }, { allowSignalWrites: true, injector });
11
14
  return computed(() => {
12
15
  const suspense = sig();
13
16
  if (suspense.hasError) {
@@ -19,4 +22,4 @@ export function rxComputed(fn, { initialValue } = {}) {
19
22
  return initialValue;
20
23
  });
21
24
  }
22
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicngtY29tcHV0ZWQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi9wYWNrYWdlcy9yeC1jb21wdXRlZC9zcmMvbGliL3J4LWNvbXB1dGVkLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxRQUFRLEVBQUUsTUFBTSxFQUFFLE1BQU0sRUFBVSxNQUFNLGVBQWUsQ0FBQztBQUNqRSxPQUFPLEVBQUUsT0FBTyxFQUFZLFVBQVUsRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBR3JFLE1BQU0sVUFBVSxVQUFVLENBQ3hCLEVBQXVCLEVBQ3ZCLEVBQUUsWUFBWSxLQUF1QyxFQUFFO0lBRXZELE1BQU0sR0FBRyxHQUFHLE1BQU0sQ0FBOEIsT0FBTyxDQUFDLENBQUM7SUFFekQsTUFBTSxDQUNKLENBQUMsU0FBUyxFQUFFLEVBQUU7UUFDWixNQUFNLEdBQUcsR0FBRyxFQUFFLEVBQUU7YUFDYixJQUFJLENBQUMsVUFBVSxFQUFFLENBQUM7YUFDbEIsU0FBUyxDQUFDLENBQUMsS0FBSyxFQUFFLEVBQUUsQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUM7UUFDeEMsU0FBUyxDQUFDLEdBQUcsRUFBRSxDQUFDLEdBQUcsQ0FBQyxXQUFXLEVBQUUsQ0FBQyxDQUFDO0lBQ3JDLENBQUMsRUFDRCxFQUFFLGlCQUFpQixFQUFFLElBQUksRUFBRSxDQUM1QixDQUFDO0lBRUYsT0FBTyxRQUFRLENBQUMsR0FBRyxFQUFFO1FBQ25CLE1BQU0sUUFBUSxHQUFHLEdBQUcsRUFBRSxDQUFDO1FBQ3ZCLElBQUksUUFBUSxDQUFDLFFBQVEsRUFBRTtZQUNyQixNQUFNLFFBQVEsQ0FBQyxLQUFLLENBQUM7U0FDdEI7UUFFRCxJQUFJLFFBQVEsQ0FBQyxRQUFRLEVBQUU7WUFDckIsT0FBTyxRQUFRLENBQUMsS0FBSyxDQUFDO1NBQ3ZCO1FBRUQsT0FBTyxZQUE2QixDQUFDO0lBQ3ZDLENBQUMsQ0FBQyxDQUFDO0FBQ0wsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IGNvbXB1dGVkLCBlZmZlY3QsIHNpZ25hbCwgU2lnbmFsIH0gZnJvbSAnQGFuZ3VsYXIvY29yZSc7XG5pbXBvcnQgeyBwZW5kaW5nLCBTdXNwZW5zZSwgc3VzcGVuc2lmeSB9IGZyb20gJ0Bqc2N1dGxlcnkvb3BlcmF0b3JzJztcbmltcG9ydCB7IE9ic2VydmFibGUgfSBmcm9tICdyeGpzJztcblxuZXhwb3J0IGZ1bmN0aW9uIHJ4Q29tcHV0ZWQ8VCwgSU5JVElBTF9WQUxVRSA9IFQgfCBudWxsIHwgdW5kZWZpbmVkPihcbiAgZm46ICgpID0+IE9ic2VydmFibGU8VD4sXG4gIHsgaW5pdGlhbFZhbHVlIH06IHsgaW5pdGlhbFZhbHVlPzogSU5JVElBTF9WQUxVRSB9ID0ge31cbik6IFNpZ25hbDxUIHwgSU5JVElBTF9WQUxVRT4ge1xuICBjb25zdCBzaWcgPSBzaWduYWw8U3VzcGVuc2U8VCB8IElOSVRJQUxfVkFMVUU+PihwZW5kaW5nKTtcblxuICBlZmZlY3QoXG4gICAgKG9uQ2xlYW51cCkgPT4ge1xuICAgICAgY29uc3Qgc3ViID0gZm4oKVxuICAgICAgICAucGlwZShzdXNwZW5zaWZ5KCkpXG4gICAgICAgIC5zdWJzY3JpYmUoKHZhbHVlKSA9PiBzaWcuc2V0KHZhbHVlKSk7XG4gICAgICBvbkNsZWFudXAoKCkgPT4gc3ViLnVuc3Vic2NyaWJlKCkpO1xuICAgIH0sXG4gICAgeyBhbGxvd1NpZ25hbFdyaXRlczogdHJ1ZSB9XG4gICk7XG5cbiAgcmV0dXJuIGNvbXB1dGVkKCgpID0+IHtcbiAgICBjb25zdCBzdXNwZW5zZSA9IHNpZygpO1xuICAgIGlmIChzdXNwZW5zZS5oYXNFcnJvcikge1xuICAgICAgdGhyb3cgc3VzcGVuc2UuZXJyb3I7XG4gICAgfVxuXG4gICAgaWYgKHN1c3BlbnNlLmhhc1ZhbHVlKSB7XG4gICAgICByZXR1cm4gc3VzcGVuc2UudmFsdWU7XG4gICAgfVxuXG4gICAgcmV0dXJuIGluaXRpYWxWYWx1ZSBhcyBJTklUSUFMX1ZBTFVFO1xuICB9KTtcbn0iXX0=
25
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicngtY29tcHV0ZWQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi9wYWNrYWdlcy9yeC1jb21wdXRlZC9zcmMvbGliL3J4LWNvbXB1dGVkLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFDTCx3QkFBd0IsRUFDeEIsUUFBUSxFQUNSLE1BQU0sRUFDTixNQUFNLEVBQ04sUUFBUSxFQUNSLE1BQU0sR0FFUCxNQUFNLGVBQWUsQ0FBQztBQUN2QixPQUFPLEVBQUUsT0FBTyxFQUFZLFVBQVUsRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBR3JFLE1BQU0sVUFBVSxVQUFVLENBQ3hCLEVBQXVCLEVBQ3ZCLEVBQ0UsWUFBWSxFQUNaLFFBQVEsTUFDaUQsRUFBRTtJQUU3RCxJQUFJLENBQUMsUUFBUTtRQUFFLHdCQUF3QixDQUFDLFVBQVUsQ0FBQyxDQUFDO0lBQ3BELFFBQVEsS0FBSyxNQUFNLENBQUMsUUFBUSxDQUFDLENBQUM7SUFFOUIsTUFBTSxHQUFHLEdBQUcsTUFBTSxDQUE4QixPQUFPLENBQUMsQ0FBQztJQUV6RCxNQUFNLENBQ0osQ0FBQyxTQUFTLEVBQUUsRUFBRTtRQUNaLE1BQU0sR0FBRyxHQUFHLEVBQUUsRUFBRTthQUNiLElBQUksQ0FBQyxVQUFVLEVBQUUsQ0FBQzthQUNsQixTQUFTLENBQUMsQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQztRQUN4QyxTQUFTLENBQUMsR0FBRyxFQUFFLENBQUMsR0FBRyxDQUFDLFdBQVcsRUFBRSxDQUFDLENBQUM7SUFDckMsQ0FBQyxFQUNELEVBQUUsaUJBQWlCLEVBQUUsSUFBSSxFQUFFLFFBQVEsRUFBRSxDQUN0QyxDQUFDO0lBRUYsT0FBTyxRQUFRLENBQUMsR0FBRyxFQUFFO1FBQ25CLE1BQU0sUUFBUSxHQUFHLEdBQUcsRUFBRSxDQUFDO1FBQ3ZCLElBQUksUUFBUSxDQUFDLFFBQVEsRUFBRTtZQUNyQixNQUFNLFFBQVEsQ0FBQyxLQUFLLENBQUM7U0FDdEI7UUFFRCxJQUFJLFFBQVEsQ0FBQyxRQUFRLEVBQUU7WUFDckIsT0FBTyxRQUFRLENBQUMsS0FBSyxDQUFDO1NBQ3ZCO1FBRUQsT0FBTyxZQUE2QixDQUFDO0lBQ3ZDLENBQUMsQ0FBQyxDQUFDO0FBQ0wsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7XG4gIGFzc2VydEluSW5qZWN0aW9uQ29udGV4dCxcbiAgY29tcHV0ZWQsXG4gIGVmZmVjdCxcbiAgaW5qZWN0LFxuICBJbmplY3RvcixcbiAgc2lnbmFsLFxuICBTaWduYWwsXG59IGZyb20gJ0Bhbmd1bGFyL2NvcmUnO1xuaW1wb3J0IHsgcGVuZGluZywgU3VzcGVuc2UsIHN1c3BlbnNpZnkgfSBmcm9tICdAanNjdXRsZXJ5L29wZXJhdG9ycyc7XG5pbXBvcnQgeyBPYnNlcnZhYmxlIH0gZnJvbSAncnhqcyc7XG5cbmV4cG9ydCBmdW5jdGlvbiByeENvbXB1dGVkPFQsIElOSVRJQUxfVkFMVUUgPSBUIHwgbnVsbCB8IHVuZGVmaW5lZD4oXG4gIGZuOiAoKSA9PiBPYnNlcnZhYmxlPFQ+LFxuICB7XG4gICAgaW5pdGlhbFZhbHVlLFxuICAgIGluamVjdG9yLFxuICB9OiB7IGluaXRpYWxWYWx1ZT86IElOSVRJQUxfVkFMVUU7IGluamVjdG9yPzogSW5qZWN0b3IgfSA9IHt9XG4pOiBTaWduYWw8VCB8IElOSVRJQUxfVkFMVUU+IHtcbiAgaWYgKCFpbmplY3RvcikgYXNzZXJ0SW5JbmplY3Rpb25Db250ZXh0KHJ4Q29tcHV0ZWQpO1xuICBpbmplY3RvciA/Pz0gaW5qZWN0KEluamVjdG9yKTtcblxuICBjb25zdCBzaWcgPSBzaWduYWw8U3VzcGVuc2U8VCB8IElOSVRJQUxfVkFMVUU+PihwZW5kaW5nKTtcblxuICBlZmZlY3QoXG4gICAgKG9uQ2xlYW51cCkgPT4ge1xuICAgICAgY29uc3Qgc3ViID0gZm4oKVxuICAgICAgICAucGlwZShzdXNwZW5zaWZ5KCkpXG4gICAgICAgIC5zdWJzY3JpYmUoKHZhbHVlKSA9PiBzaWcuc2V0KHZhbHVlKSk7XG4gICAgICBvbkNsZWFudXAoKCkgPT4gc3ViLnVuc3Vic2NyaWJlKCkpO1xuICAgIH0sXG4gICAgeyBhbGxvd1NpZ25hbFdyaXRlczogdHJ1ZSwgaW5qZWN0b3IgfVxuICApO1xuXG4gIHJldHVybiBjb21wdXRlZCgoKSA9PiB7XG4gICAgY29uc3Qgc3VzcGVuc2UgPSBzaWcoKTtcbiAgICBpZiAoc3VzcGVuc2UuaGFzRXJyb3IpIHtcbiAgICAgIHRocm93IHN1c3BlbnNlLmVycm9yO1xuICAgIH1cblxuICAgIGlmIChzdXNwZW5zZS5oYXNWYWx1ZSkge1xuICAgICAgcmV0dXJuIHN1c3BlbnNlLnZhbHVlO1xuICAgIH1cblxuICAgIHJldHVybiBpbml0aWFsVmFsdWUgYXMgSU5JVElBTF9WQUxVRTtcbiAgfSk7XG59XG4iXX0=
@@ -1,14 +1,17 @@
1
- import { signal, effect, computed } from '@angular/core';
1
+ import { assertInInjectionContext, inject, Injector, signal, effect, computed } from '@angular/core';
2
2
  import { pending, suspensify } from '@jscutlery/operators';
3
3
 
4
- function rxComputed(fn, { initialValue } = {}) {
4
+ function rxComputed(fn, { initialValue, injector, } = {}) {
5
+ if (!injector)
6
+ assertInInjectionContext(rxComputed);
7
+ injector ??= inject(Injector);
5
8
  const sig = signal(pending);
6
9
  effect((onCleanup) => {
7
10
  const sub = fn()
8
11
  .pipe(suspensify())
9
12
  .subscribe((value) => sig.set(value));
10
13
  onCleanup(() => sub.unsubscribe());
11
- }, { allowSignalWrites: true });
14
+ }, { allowSignalWrites: true, injector });
12
15
  return computed(() => {
13
16
  const suspense = sig();
14
17
  if (suspense.hasError) {
@@ -1 +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;;;;"}
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 {\n assertInInjectionContext,\n computed,\n effect,\n inject,\n Injector,\n signal,\n Signal,\n} 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 {\n initialValue,\n injector,\n }: { initialValue?: INITIAL_VALUE; injector?: Injector } = {}\n): Signal<T | INITIAL_VALUE> {\n if (!injector) assertInInjectionContext(rxComputed);\n injector ??= inject(Injector);\n\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, injector }\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","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;AAYM,SAAU,UAAU,CACxB,EAAuB,EACvB,EACE,YAAY,EACZ,QAAQ,GAAA,GACiD,EAAE,EAAA;AAE7D,IAAA,IAAI,CAAC,QAAQ;QAAE,wBAAwB,CAAC,UAAU,CAAC,CAAC;AACpD,IAAA,QAAQ,KAAK,MAAM,CAAC,QAAQ,CAAC,CAAC;AAE9B,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;KACpC,EACD,EAAE,iBAAiB,EAAE,IAAI,EAAE,QAAQ,EAAE,CACtC,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;;AC9CA;;AAEG;;;;"}
@@ -1,5 +1,6 @@
1
- import { Signal } from '@angular/core';
1
+ import { Injector, Signal } from '@angular/core';
2
2
  import { Observable } from 'rxjs';
3
- export declare function rxComputed<T, INITIAL_VALUE = T | null | undefined>(fn: () => Observable<T>, { initialValue }?: {
3
+ export declare function rxComputed<T, INITIAL_VALUE = T | null | undefined>(fn: () => Observable<T>, { initialValue, injector, }?: {
4
4
  initialValue?: INITIAL_VALUE;
5
+ injector?: Injector;
5
6
  }): Signal<T | INITIAL_VALUE>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jscutlery/rx-computed",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "peerDependencies": {
5
5
  "@angular/common": "^16.0.0",
6
6
  "@angular/core": "^16.0.0",