rxjs-interop.mjs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. /**
  2. * @license Angular v20.1.0
  3. * (c) 2010-2025 Google LLC. https://angular.io/
  4. * License: MIT
  5. */
  6. import { Observable, ReplaySubject } from 'rxjs';
  7. import { takeUntil } from 'rxjs/operators';
  8. import { assertInInjectionContext, inject, DestroyRef, RuntimeError, Injector, assertNotInReactiveContext, signal, PendingTasks } from './root_effect_scheduler.mjs';
  9. import { getOutputDestroyRef, effect, untracked, computed, resource, encapsulateResourceError } from './resource.mjs';
  10. import './not_found.mjs';
  11. import './signal.mjs';
  12. import '@angular/core/primitives/di';
  13. import '@angular/core/primitives/signals';
  14. import './untracked.mjs';
  15. /**
  16. * Operator which completes the Observable when the calling context (component, directive, service,
  17. * etc) is destroyed.
  18. *
  19. * @param destroyRef optionally, the `DestroyRef` representing the current context. This can be
  20. * passed explicitly to use `takeUntilDestroyed` outside of an [injection
  21. * context](guide/di/dependency-injection-context). Otherwise, the current `DestroyRef` is injected.
  22. *
  23. * @publicApi 19.0
  24. */
  25. function takeUntilDestroyed(destroyRef) {
  26. if (!destroyRef) {
  27. ngDevMode && assertInInjectionContext(takeUntilDestroyed);
  28. destroyRef = inject(DestroyRef);
  29. }
  30. const destroyed$ = new Observable((subscriber) => {
  31. if (destroyRef.destroyed) {
  32. subscriber.next();
  33. return;
  34. }
  35. const unregisterFn = destroyRef.onDestroy(subscriber.next.bind(subscriber));
  36. return unregisterFn;
  37. });
  38. return (source) => {
  39. return source.pipe(takeUntil(destroyed$));
  40. };
  41. }
  42. /**
  43. * Implementation of `OutputRef` that emits values from
  44. * an RxJS observable source.
  45. *
  46. * @internal
  47. */
  48. class OutputFromObservableRef {
  49. source;
  50. destroyed = false;
  51. destroyRef = inject(DestroyRef);
  52. constructor(source) {
  53. this.source = source;
  54. this.destroyRef.onDestroy(() => {
  55. this.destroyed = true;
  56. });
  57. }
  58. subscribe(callbackFn) {
  59. if (this.destroyed) {
  60. throw new RuntimeError(953 /* ɵRuntimeErrorCode.OUTPUT_REF_DESTROYED */, ngDevMode &&
  61. 'Unexpected subscription to destroyed `OutputRef`. ' +
  62. 'The owning directive/component is destroyed.');
  63. }
  64. // Stop yielding more values when the directive/component is already destroyed.
  65. const subscription = this.source.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
  66. next: (value) => callbackFn(value),
  67. });
  68. return {
  69. unsubscribe: () => subscription.unsubscribe(),
  70. };
  71. }
  72. }
  73. /**
  74. * Declares an Angular output that is using an RxJS observable as a source
  75. * for events dispatched to parent subscribers.
  76. *
  77. * The behavior for an observable as source is defined as followed:
  78. * 1. New values are forwarded to the Angular output (next notifications).
  79. * 2. Errors notifications are not handled by Angular. You need to handle these manually.
  80. * For example by using `catchError`.
  81. * 3. Completion notifications stop the output from emitting new values.
  82. *
  83. * @usageNotes
  84. * Initialize an output in your directive by declaring a
  85. * class field and initializing it with the `outputFromObservable()` function.
  86. *
  87. * ```ts
  88. * @Directive({..})
  89. * export class MyDir {
  90. * nameChange$ = <some-observable>;
  91. * nameChange = outputFromObservable(this.nameChange$);
  92. * }
  93. * ```
  94. *
  95. * @publicApi 19.0
  96. */
  97. function outputFromObservable(observable, opts) {
  98. ngDevMode && assertInInjectionContext(outputFromObservable);
  99. return new OutputFromObservableRef(observable);
  100. }
  101. /**
  102. * Converts an Angular output declared via `output()` or `outputFromObservable()`
  103. * to an observable.
  104. *
  105. * You can subscribe to the output via `Observable.subscribe` then.
  106. *
  107. * @publicApi 19.0
  108. */
  109. function outputToObservable(ref) {
  110. const destroyRef = getOutputDestroyRef(ref);
  111. return new Observable((observer) => {
  112. // Complete the observable upon directive/component destroy.
  113. // Note: May be `undefined` if an `EventEmitter` is declared outside
  114. // of an injection context.
  115. const unregisterOnDestroy = destroyRef?.onDestroy(() => observer.complete());
  116. const subscription = ref.subscribe((v) => observer.next(v));
  117. return () => {
  118. subscription.unsubscribe();
  119. unregisterOnDestroy?.();
  120. };
  121. });
  122. }
  123. /**
  124. * Exposes the value of an Angular `Signal` as an RxJS `Observable`.
  125. *
  126. * The signal's value will be propagated into the `Observable`'s subscribers using an `effect`.
  127. *
  128. * `toObservable` must be called in an injection context unless an injector is provided via options.
  129. *
  130. * @publicApi 20.0
  131. */
  132. function toObservable(source, options) {
  133. if (ngDevMode && !options?.injector) {
  134. assertInInjectionContext(toObservable);
  135. }
  136. const injector = options?.injector ?? inject(Injector);
  137. const subject = new ReplaySubject(1);
  138. const watcher = effect(() => {
  139. let value;
  140. try {
  141. value = source();
  142. }
  143. catch (err) {
  144. untracked(() => subject.error(err));
  145. return;
  146. }
  147. untracked(() => subject.next(value));
  148. }, { injector, manualCleanup: true });
  149. injector.get(DestroyRef).onDestroy(() => {
  150. watcher.destroy();
  151. subject.complete();
  152. });
  153. return subject.asObservable();
  154. }
  155. /**
  156. * Get the current value of an `Observable` as a reactive `Signal`.
  157. *
  158. * `toSignal` returns a `Signal` which provides synchronous reactive access to values produced
  159. * by the given `Observable`, by subscribing to that `Observable`. The returned `Signal` will always
  160. * have the most recent value emitted by the subscription, and will throw an error if the
  161. * `Observable` errors.
  162. *
  163. * With `requireSync` set to `true`, `toSignal` will assert that the `Observable` produces a value
  164. * immediately upon subscription. No `initialValue` is needed in this case, and the returned signal
  165. * does not include an `undefined` type.
  166. *
  167. * By default, the subscription will be automatically cleaned up when the current [injection
  168. * context](guide/di/dependency-injection-context) is destroyed. For example, when `toSignal` is
  169. * called during the construction of a component, the subscription will be cleaned up when the
  170. * component is destroyed. If an injection context is not available, an explicit `Injector` can be
  171. * passed instead.
  172. *
  173. * If the subscription should persist until the `Observable` itself completes, the `manualCleanup`
  174. * option can be specified instead, which disables the automatic subscription teardown. No injection
  175. * context is needed in this configuration as well.
  176. */
  177. function toSignal(source, options) {
  178. typeof ngDevMode !== 'undefined' &&
  179. ngDevMode &&
  180. assertNotInReactiveContext(toSignal, 'Invoking `toSignal` causes new subscriptions every time. ' +
  181. 'Consider moving `toSignal` outside of the reactive context and read the signal value where needed.');
  182. const requiresCleanup = !options?.manualCleanup;
  183. if (ngDevMode && requiresCleanup && !options?.injector) {
  184. assertInInjectionContext(toSignal);
  185. }
  186. const cleanupRef = requiresCleanup
  187. ? (options?.injector?.get(DestroyRef) ?? inject(DestroyRef))
  188. : null;
  189. const equal = makeToSignalEqual(options?.equal);
  190. // Note: T is the Observable value type, and U is the initial value type. They don't have to be
  191. // the same - the returned signal gives values of type `T`.
  192. let state;
  193. if (options?.requireSync) {
  194. // Initially the signal is in a `NoValue` state.
  195. state = signal({ kind: 0 /* StateKind.NoValue */ }, { equal });
  196. }
  197. else {
  198. // If an initial value was passed, use it. Otherwise, use `undefined` as the initial value.
  199. state = signal({ kind: 1 /* StateKind.Value */, value: options?.initialValue }, { equal });
  200. }
  201. let destroyUnregisterFn;
  202. // Note: This code cannot run inside a reactive context (see assertion above). If we'd support
  203. // this, we would subscribe to the observable outside of the current reactive context, avoiding
  204. // that side-effect signal reads/writes are attribute to the current consumer. The current
  205. // consumer only needs to be notified when the `state` signal changes through the observable
  206. // subscription. Additional context (related to async pipe):
  207. // https://github.com/angular/angular/pull/50522.
  208. const sub = source.subscribe({
  209. next: (value) => state.set({ kind: 1 /* StateKind.Value */, value }),
  210. error: (error) => {
  211. state.set({ kind: 2 /* StateKind.Error */, error });
  212. destroyUnregisterFn?.();
  213. },
  214. complete: () => {
  215. destroyUnregisterFn?.();
  216. },
  217. // Completion of the Observable is meaningless to the signal. Signals don't have a concept of
  218. // "complete".
  219. });
  220. if (options?.requireSync && state().kind === 0 /* StateKind.NoValue */) {
  221. throw new RuntimeError(601 /* ɵRuntimeErrorCode.REQUIRE_SYNC_WITHOUT_SYNC_EMIT */, (typeof ngDevMode === 'undefined' || ngDevMode) &&
  222. '`toSignal()` called with `requireSync` but `Observable` did not emit synchronously.');
  223. }
  224. // Unsubscribe when the current context is destroyed, if requested.
  225. destroyUnregisterFn = cleanupRef?.onDestroy(sub.unsubscribe.bind(sub));
  226. // The actual returned signal is a `computed` of the `State` signal, which maps the various states
  227. // to either values or errors.
  228. return computed(() => {
  229. const current = state();
  230. switch (current.kind) {
  231. case 1 /* StateKind.Value */:
  232. return current.value;
  233. case 2 /* StateKind.Error */:
  234. throw current.error;
  235. case 0 /* StateKind.NoValue */:
  236. // This shouldn't really happen because the error is thrown on creation.
  237. throw new RuntimeError(601 /* ɵRuntimeErrorCode.REQUIRE_SYNC_WITHOUT_SYNC_EMIT */, (typeof ngDevMode === 'undefined' || ngDevMode) &&
  238. '`toSignal()` called with `requireSync` but `Observable` did not emit synchronously.');
  239. }
  240. }, { equal: options?.equal });
  241. }
  242. function makeToSignalEqual(userEquality = Object.is) {
  243. return (a, b) => a.kind === 1 /* StateKind.Value */ && b.kind === 1 /* StateKind.Value */ && userEquality(a.value, b.value);
  244. }
  245. /**
  246. * Operator which makes the application unstable until the observable emits, completes, errors, or is unsubscribed.
  247. *
  248. * Use this operator in observables whose subscriptions are important for rendering and should be included in SSR serialization.
  249. *
  250. * @param injector The `Injector` to use during creation. If this is not provided, the current injection context will be used instead (via `inject`).
  251. *
  252. * @developerPreview 20.0
  253. */
  254. function pendingUntilEvent(injector) {
  255. if (injector === undefined) {
  256. ngDevMode && assertInInjectionContext(pendingUntilEvent);
  257. injector = inject(Injector);
  258. }
  259. const taskService = injector.get(PendingTasks);
  260. return (sourceObservable) => {
  261. return new Observable((originalSubscriber) => {
  262. // create a new task on subscription
  263. const removeTask = taskService.add();
  264. let cleanedUp = false;
  265. function cleanupTask() {
  266. if (cleanedUp) {
  267. return;
  268. }
  269. removeTask();
  270. cleanedUp = true;
  271. }
  272. const innerSubscription = sourceObservable.subscribe({
  273. next: (v) => {
  274. originalSubscriber.next(v);
  275. cleanupTask();
  276. },
  277. complete: () => {
  278. originalSubscriber.complete();
  279. cleanupTask();
  280. },
  281. error: (e) => {
  282. originalSubscriber.error(e);
  283. cleanupTask();
  284. },
  285. });
  286. innerSubscription.add(() => {
  287. originalSubscriber.unsubscribe();
  288. cleanupTask();
  289. });
  290. return innerSubscription;
  291. });
  292. };
  293. }
  294. function rxResource(opts) {
  295. if (ngDevMode && !opts?.injector) {
  296. assertInInjectionContext(rxResource);
  297. }
  298. return resource({
  299. ...opts,
  300. loader: undefined,
  301. stream: (params) => {
  302. let sub;
  303. // Track the abort listener so it can be removed if the Observable completes (as a memory
  304. // optimization).
  305. const onAbort = () => sub.unsubscribe();
  306. params.abortSignal.addEventListener('abort', onAbort);
  307. // Start off stream as undefined.
  308. const stream = signal({ value: undefined });
  309. let resolve;
  310. const promise = new Promise((r) => (resolve = r));
  311. function send(value) {
  312. stream.set(value);
  313. resolve?.(stream);
  314. resolve = undefined;
  315. }
  316. // TODO(alxhub): remove after g3 updated to rename loader -> stream
  317. const streamFn = opts.stream ?? opts.loader;
  318. if (streamFn === undefined) {
  319. throw new RuntimeError(990 /* ɵRuntimeErrorCode.MUST_PROVIDE_STREAM_OPTION */, ngDevMode && `Must provide \`stream\` option.`);
  320. }
  321. sub = streamFn(params).subscribe({
  322. next: (value) => send({ value }),
  323. error: (error) => {
  324. send({ error: encapsulateResourceError(error) });
  325. params.abortSignal.removeEventListener('abort', onAbort);
  326. },
  327. complete: () => {
  328. if (resolve) {
  329. send({
  330. error: new RuntimeError(991 /* ɵRuntimeErrorCode.RESOURCE_COMPLETED_BEFORE_PRODUCING_VALUE */, ngDevMode && 'Resource completed before producing a value'),
  331. });
  332. }
  333. params.abortSignal.removeEventListener('abort', onAbort);
  334. },
  335. });
  336. return promise;
  337. },
  338. });
  339. }
  340. export { outputFromObservable, outputToObservable, pendingUntilEvent, rxResource, takeUntilDestroyed, toObservable, toSignal };
  341. //# sourceMappingURL=rxjs-interop.mjs.map