rxjs-interop.mjs 14 KB

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