resource.mjs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. /**
  2. * @license Angular v20.1.0
  3. * (c) 2010-2025 Google LLC. https://angular.io/
  4. * License: MIT
  5. */
  6. import { inject, ErrorHandler, DestroyRef, RuntimeError, formatRuntimeError, assertNotInReactiveContext, assertInInjectionContext, Injector, ViewContext, ChangeDetectionScheduler, EffectScheduler, setInjectorProfilerContext, emitEffectCreatedEvent, EFFECTS, NodeInjectorDestroyRef, FLAGS, markAncestorsForTraversal, noop, setIsRefreshingViews, signalAsReadonlyFn, PendingTasks, signal } from './root_effect_scheduler.mjs';
  7. import { setActiveConsumer, createComputed, SIGNAL, consumerDestroy, REACTIVE_NODE, isInNotificationPhase, consumerPollProducersForChange, consumerBeforeComputation, consumerAfterComputation } from './signal.mjs';
  8. import { untracked as untracked$1, createLinkedSignal, linkedSignalSetFn, linkedSignalUpdateFn } from './untracked.mjs';
  9. /**
  10. * An `OutputEmitterRef` is created by the `output()` function and can be
  11. * used to emit values to consumers of your directive or component.
  12. *
  13. * Consumers of your directive/component can bind to the output and
  14. * subscribe to changes via the bound event syntax. For example:
  15. *
  16. * ```html
  17. * <my-comp (valueChange)="processNewValue($event)" />
  18. * ```
  19. *
  20. * @publicAPI
  21. */
  22. class OutputEmitterRef {
  23. destroyed = false;
  24. listeners = null;
  25. errorHandler = inject(ErrorHandler, { optional: true });
  26. /** @internal */
  27. destroyRef = inject(DestroyRef);
  28. constructor() {
  29. // Clean-up all listeners and mark as destroyed upon destroy.
  30. this.destroyRef.onDestroy(() => {
  31. this.destroyed = true;
  32. this.listeners = null;
  33. });
  34. }
  35. subscribe(callback) {
  36. if (this.destroyed) {
  37. throw new RuntimeError(953 /* RuntimeErrorCode.OUTPUT_REF_DESTROYED */, ngDevMode &&
  38. 'Unexpected subscription to destroyed `OutputRef`. ' +
  39. 'The owning directive/component is destroyed.');
  40. }
  41. (this.listeners ??= []).push(callback);
  42. return {
  43. unsubscribe: () => {
  44. const idx = this.listeners?.indexOf(callback);
  45. if (idx !== undefined && idx !== -1) {
  46. this.listeners?.splice(idx, 1);
  47. }
  48. },
  49. };
  50. }
  51. /** Emits a new value to the output. */
  52. emit(value) {
  53. if (this.destroyed) {
  54. console.warn(formatRuntimeError(953 /* RuntimeErrorCode.OUTPUT_REF_DESTROYED */, ngDevMode &&
  55. 'Unexpected emit for destroyed `OutputRef`. ' +
  56. 'The owning directive/component is destroyed.'));
  57. return;
  58. }
  59. if (this.listeners === null) {
  60. return;
  61. }
  62. const previousConsumer = setActiveConsumer(null);
  63. try {
  64. for (const listenerFn of this.listeners) {
  65. try {
  66. listenerFn(value);
  67. }
  68. catch (err) {
  69. this.errorHandler?.handleError(err);
  70. }
  71. }
  72. }
  73. finally {
  74. setActiveConsumer(previousConsumer);
  75. }
  76. }
  77. }
  78. /** Gets the owning `DestroyRef` for the given output. */
  79. function getOutputDestroyRef(ref) {
  80. return ref.destroyRef;
  81. }
  82. /**
  83. * Execute an arbitrary function in a non-reactive (non-tracking) context. The executed function
  84. * can, optionally, return a value.
  85. */
  86. function untracked(nonReactiveReadsFn) {
  87. return untracked$1(nonReactiveReadsFn);
  88. }
  89. /**
  90. * Create a computed `Signal` which derives a reactive value from an expression.
  91. */
  92. function computed(computation, options) {
  93. const getter = createComputed(computation, options?.equal);
  94. if (ngDevMode) {
  95. getter.toString = () => `[Computed: ${getter()}]`;
  96. getter[SIGNAL].debugName = options?.debugName;
  97. }
  98. return getter;
  99. }
  100. class EffectRefImpl {
  101. [SIGNAL];
  102. constructor(node) {
  103. this[SIGNAL] = node;
  104. }
  105. destroy() {
  106. this[SIGNAL].destroy();
  107. }
  108. }
  109. /**
  110. * Registers an "effect" that will be scheduled & executed whenever the signals that it reads
  111. * changes.
  112. *
  113. * Angular has two different kinds of effect: component effects and root effects. Component effects
  114. * are created when `effect()` is called from a component, directive, or within a service of a
  115. * component/directive. Root effects are created when `effect()` is called from outside the
  116. * component tree, such as in a root service.
  117. *
  118. * The two effect types differ in their timing. Component effects run as a component lifecycle
  119. * event during Angular's synchronization (change detection) process, and can safely read input
  120. * signals or create/destroy views that depend on component state. Root effects run as microtasks
  121. * and have no connection to the component tree or change detection.
  122. *
  123. * `effect()` must be run in injection context, unless the `injector` option is manually specified.
  124. *
  125. * @publicApi 20.0
  126. */
  127. function effect(effectFn, options) {
  128. ngDevMode &&
  129. assertNotInReactiveContext(effect, 'Call `effect` outside of a reactive context. For example, schedule the ' +
  130. 'effect inside the component constructor.');
  131. if (ngDevMode && !options?.injector) {
  132. assertInInjectionContext(effect);
  133. }
  134. if (ngDevMode && options?.allowSignalWrites !== undefined) {
  135. console.warn(`The 'allowSignalWrites' flag is deprecated and no longer impacts effect() (writes are always allowed)`);
  136. }
  137. const injector = options?.injector ?? inject(Injector);
  138. let destroyRef = options?.manualCleanup !== true ? injector.get(DestroyRef) : null;
  139. let node;
  140. const viewContext = injector.get(ViewContext, null, { optional: true });
  141. const notifier = injector.get(ChangeDetectionScheduler);
  142. if (viewContext !== null) {
  143. // This effect was created in the context of a view, and will be associated with the view.
  144. node = createViewEffect(viewContext.view, notifier, effectFn);
  145. if (destroyRef instanceof NodeInjectorDestroyRef && destroyRef._lView === viewContext.view) {
  146. // The effect is being created in the same view as the `DestroyRef` references, so it will be
  147. // automatically destroyed without the need for an explicit `DestroyRef` registration.
  148. destroyRef = null;
  149. }
  150. }
  151. else {
  152. // This effect was created outside the context of a view, and will be scheduled independently.
  153. node = createRootEffect(effectFn, injector.get(EffectScheduler), notifier);
  154. }
  155. node.injector = injector;
  156. if (destroyRef !== null) {
  157. // If we need to register for cleanup, do that here.
  158. node.onDestroyFn = destroyRef.onDestroy(() => node.destroy());
  159. }
  160. const effectRef = new EffectRefImpl(node);
  161. if (ngDevMode) {
  162. node.debugName = options?.debugName ?? '';
  163. const prevInjectorProfilerContext = setInjectorProfilerContext({ injector, token: null });
  164. try {
  165. emitEffectCreatedEvent(effectRef);
  166. }
  167. finally {
  168. setInjectorProfilerContext(prevInjectorProfilerContext);
  169. }
  170. }
  171. return effectRef;
  172. }
  173. const BASE_EFFECT_NODE =
  174. /* @__PURE__ */ (() => ({
  175. ...REACTIVE_NODE,
  176. consumerIsAlwaysLive: true,
  177. consumerAllowSignalWrites: true,
  178. dirty: true,
  179. hasRun: false,
  180. cleanupFns: undefined,
  181. zone: null,
  182. kind: 'effect',
  183. onDestroyFn: noop,
  184. run() {
  185. this.dirty = false;
  186. if (ngDevMode && isInNotificationPhase()) {
  187. throw new Error(`Schedulers cannot synchronously execute watches while scheduling.`);
  188. }
  189. if (this.hasRun && !consumerPollProducersForChange(this)) {
  190. return;
  191. }
  192. this.hasRun = true;
  193. const registerCleanupFn = (cleanupFn) => (this.cleanupFns ??= []).push(cleanupFn);
  194. const prevNode = consumerBeforeComputation(this);
  195. // We clear `setIsRefreshingViews` so that `markForCheck()` within the body of an effect will
  196. // cause CD to reach the component in question.
  197. const prevRefreshingViews = setIsRefreshingViews(false);
  198. try {
  199. this.maybeCleanup();
  200. this.fn(registerCleanupFn);
  201. }
  202. finally {
  203. setIsRefreshingViews(prevRefreshingViews);
  204. consumerAfterComputation(this, prevNode);
  205. }
  206. },
  207. maybeCleanup() {
  208. if (!this.cleanupFns?.length) {
  209. return;
  210. }
  211. const prevConsumer = setActiveConsumer(null);
  212. try {
  213. // Attempt to run the cleanup functions. Regardless of failure or success, we consider
  214. // cleanup "completed" and clear the list for the next run of the effect. Note that an error
  215. // from the cleanup function will still crash the current run of the effect.
  216. while (this.cleanupFns.length) {
  217. this.cleanupFns.pop()();
  218. }
  219. }
  220. finally {
  221. this.cleanupFns = [];
  222. setActiveConsumer(prevConsumer);
  223. }
  224. },
  225. }))();
  226. const ROOT_EFFECT_NODE =
  227. /* @__PURE__ */ (() => ({
  228. ...BASE_EFFECT_NODE,
  229. consumerMarkedDirty() {
  230. this.scheduler.schedule(this);
  231. this.notifier.notify(12 /* NotificationSource.RootEffect */);
  232. },
  233. destroy() {
  234. consumerDestroy(this);
  235. this.onDestroyFn();
  236. this.maybeCleanup();
  237. this.scheduler.remove(this);
  238. },
  239. }))();
  240. const VIEW_EFFECT_NODE =
  241. /* @__PURE__ */ (() => ({
  242. ...BASE_EFFECT_NODE,
  243. consumerMarkedDirty() {
  244. this.view[FLAGS] |= 8192 /* LViewFlags.HasChildViewsToRefresh */;
  245. markAncestorsForTraversal(this.view);
  246. this.notifier.notify(13 /* NotificationSource.ViewEffect */);
  247. },
  248. destroy() {
  249. consumerDestroy(this);
  250. this.onDestroyFn();
  251. this.maybeCleanup();
  252. this.view[EFFECTS]?.delete(this);
  253. },
  254. }))();
  255. function createViewEffect(view, notifier, fn) {
  256. const node = Object.create(VIEW_EFFECT_NODE);
  257. node.view = view;
  258. node.zone = typeof Zone !== 'undefined' ? Zone.current : null;
  259. node.notifier = notifier;
  260. node.fn = fn;
  261. view[EFFECTS] ??= new Set();
  262. view[EFFECTS].add(node);
  263. node.consumerMarkedDirty(node);
  264. return node;
  265. }
  266. function createRootEffect(fn, scheduler, notifier) {
  267. const node = Object.create(ROOT_EFFECT_NODE);
  268. node.fn = fn;
  269. node.scheduler = scheduler;
  270. node.notifier = notifier;
  271. node.zone = typeof Zone !== 'undefined' ? Zone.current : null;
  272. node.scheduler.add(node);
  273. node.notifier.notify(12 /* NotificationSource.RootEffect */);
  274. return node;
  275. }
  276. const identityFn = (v) => v;
  277. function linkedSignal(optionsOrComputation, options) {
  278. if (typeof optionsOrComputation === 'function') {
  279. const getter = createLinkedSignal(optionsOrComputation, (identityFn), options?.equal);
  280. return upgradeLinkedSignalGetter(getter);
  281. }
  282. else {
  283. const getter = createLinkedSignal(optionsOrComputation.source, optionsOrComputation.computation, optionsOrComputation.equal);
  284. return upgradeLinkedSignalGetter(getter);
  285. }
  286. }
  287. function upgradeLinkedSignalGetter(getter) {
  288. if (ngDevMode) {
  289. getter.toString = () => `[LinkedSignal: ${getter()}]`;
  290. }
  291. const node = getter[SIGNAL];
  292. const upgradedGetter = getter;
  293. upgradedGetter.set = (newValue) => linkedSignalSetFn(node, newValue);
  294. upgradedGetter.update = (updateFn) => linkedSignalUpdateFn(node, updateFn);
  295. upgradedGetter.asReadonly = signalAsReadonlyFn.bind(getter);
  296. return upgradedGetter;
  297. }
  298. /**
  299. * Whether a `Resource.value()` should throw an error when the resource is in the error state.
  300. *
  301. * This internal flag is being used to gradually roll out this behavior.
  302. */
  303. const RESOURCE_VALUE_THROWS_ERRORS_DEFAULT = true;
  304. function resource(options) {
  305. if (ngDevMode && !options?.injector) {
  306. assertInInjectionContext(resource);
  307. }
  308. const oldNameForParams = options.request;
  309. const params = (options.params ?? oldNameForParams ?? (() => null));
  310. return new ResourceImpl(params, getLoader(options), options.defaultValue, options.equal ? wrapEqualityFn(options.equal) : undefined, options.injector ?? inject(Injector), RESOURCE_VALUE_THROWS_ERRORS_DEFAULT);
  311. }
  312. /**
  313. * Base class which implements `.value` as a `WritableSignal` by delegating `.set` and `.update`.
  314. */
  315. class BaseWritableResource {
  316. value;
  317. constructor(value) {
  318. this.value = value;
  319. this.value.set = this.set.bind(this);
  320. this.value.update = this.update.bind(this);
  321. this.value.asReadonly = signalAsReadonlyFn;
  322. }
  323. isError = computed(() => this.status() === 'error');
  324. update(updateFn) {
  325. this.set(updateFn(untracked(this.value)));
  326. }
  327. isLoading = computed(() => this.status() === 'loading' || this.status() === 'reloading');
  328. hasValue() {
  329. // Note: we specifically read `isError()` instead of `status()` here to avoid triggering
  330. // reactive consumers which read `hasValue()`. This way, if `hasValue()` is used inside of an
  331. // effect, it doesn't cause the effect to rerun on every status change.
  332. if (this.isError()) {
  333. return false;
  334. }
  335. return this.value() !== undefined;
  336. }
  337. asReadonly() {
  338. return this;
  339. }
  340. }
  341. /**
  342. * Implementation for `resource()` which uses a `linkedSignal` to manage the resource's state.
  343. */
  344. class ResourceImpl extends BaseWritableResource {
  345. loaderFn;
  346. equal;
  347. pendingTasks;
  348. /**
  349. * The current state of the resource. Status, value, and error are derived from this.
  350. */
  351. state;
  352. /**
  353. * Combines the current request with a reload counter which allows the resource to be reloaded on
  354. * imperative command.
  355. */
  356. extRequest;
  357. effectRef;
  358. pendingController;
  359. resolvePendingTask = undefined;
  360. destroyed = false;
  361. unregisterOnDestroy;
  362. constructor(request, loaderFn, defaultValue, equal, injector, throwErrorsFromValue = RESOURCE_VALUE_THROWS_ERRORS_DEFAULT) {
  363. super(
  364. // Feed a computed signal for the value to `BaseWritableResource`, which will upgrade it to a
  365. // `WritableSignal` that delegates to `ResourceImpl.set`.
  366. computed(() => {
  367. const streamValue = this.state().stream?.();
  368. if (!streamValue) {
  369. return defaultValue;
  370. }
  371. // Prevents `hasValue()` from throwing an error when a reload happened in the error state
  372. if (this.state().status === 'loading' && this.error()) {
  373. return defaultValue;
  374. }
  375. if (!isResolved(streamValue)) {
  376. if (throwErrorsFromValue) {
  377. throw new ResourceValueError(this.error());
  378. }
  379. else {
  380. return defaultValue;
  381. }
  382. }
  383. return streamValue.value;
  384. }, { equal }));
  385. this.loaderFn = loaderFn;
  386. this.equal = equal;
  387. // Extend `request()` to include a writable reload signal.
  388. this.extRequest = linkedSignal({
  389. source: request,
  390. computation: (request) => ({ request, reload: 0 }),
  391. });
  392. // The main resource state is managed in a `linkedSignal`, which allows the resource to change
  393. // state instantaneously when the request signal changes.
  394. this.state = linkedSignal({
  395. // Whenever the request changes,
  396. source: this.extRequest,
  397. // Compute the state of the resource given a change in status.
  398. computation: (extRequest, previous) => {
  399. const status = extRequest.request === undefined ? 'idle' : 'loading';
  400. if (!previous) {
  401. return {
  402. extRequest,
  403. status,
  404. previousStatus: 'idle',
  405. stream: undefined,
  406. };
  407. }
  408. else {
  409. return {
  410. extRequest,
  411. status,
  412. previousStatus: projectStatusOfState(previous.value),
  413. // If the request hasn't changed, keep the previous stream.
  414. stream: previous.value.extRequest.request === extRequest.request
  415. ? previous.value.stream
  416. : undefined,
  417. };
  418. }
  419. },
  420. });
  421. this.effectRef = effect(this.loadEffect.bind(this), {
  422. injector,
  423. manualCleanup: true,
  424. });
  425. this.pendingTasks = injector.get(PendingTasks);
  426. // Cancel any pending request when the resource itself is destroyed.
  427. this.unregisterOnDestroy = injector.get(DestroyRef).onDestroy(() => this.destroy());
  428. }
  429. status = computed(() => projectStatusOfState(this.state()));
  430. error = computed(() => {
  431. const stream = this.state().stream?.();
  432. return stream && !isResolved(stream) ? stream.error : undefined;
  433. });
  434. /**
  435. * Called either directly via `WritableResource.set` or via `.value.set()`.
  436. */
  437. set(value) {
  438. if (this.destroyed) {
  439. return;
  440. }
  441. const error = untracked(this.error);
  442. const state = untracked(this.state);
  443. if (!error) {
  444. const current = untracked(this.value);
  445. if (state.status === 'local' &&
  446. (this.equal ? this.equal(current, value) : current === value)) {
  447. return;
  448. }
  449. }
  450. // Enter Local state with the user-defined value.
  451. this.state.set({
  452. extRequest: state.extRequest,
  453. status: 'local',
  454. previousStatus: 'local',
  455. stream: signal({ value }),
  456. });
  457. // We're departing from whatever state the resource was in previously, so cancel any in-progress
  458. // loading operations.
  459. this.abortInProgressLoad();
  460. }
  461. reload() {
  462. // We don't want to restart in-progress loads.
  463. const { status } = untracked(this.state);
  464. if (status === 'idle' || status === 'loading') {
  465. return false;
  466. }
  467. // Increment the request reload to trigger the `state` linked signal to switch us to `Reload`
  468. this.extRequest.update(({ request, reload }) => ({ request, reload: reload + 1 }));
  469. return true;
  470. }
  471. destroy() {
  472. this.destroyed = true;
  473. this.unregisterOnDestroy();
  474. this.effectRef.destroy();
  475. this.abortInProgressLoad();
  476. // Destroyed resources enter Idle state.
  477. this.state.set({
  478. extRequest: { request: undefined, reload: 0 },
  479. status: 'idle',
  480. previousStatus: 'idle',
  481. stream: undefined,
  482. });
  483. }
  484. async loadEffect() {
  485. const extRequest = this.extRequest();
  486. // Capture the previous status before any state transitions. Note that this is `untracked` since
  487. // we do not want the effect to depend on the state of the resource, only on the request.
  488. const { status: currentStatus, previousStatus } = untracked(this.state);
  489. if (extRequest.request === undefined) {
  490. // Nothing to load (and we should already be in a non-loading state).
  491. return;
  492. }
  493. else if (currentStatus !== 'loading') {
  494. // We're not in a loading or reloading state, so this loading request is stale.
  495. return;
  496. }
  497. // Cancel any previous loading attempts.
  498. this.abortInProgressLoad();
  499. // Capturing _this_ load's pending task in a local variable is important here. We may attempt to
  500. // resolve it twice:
  501. //
  502. // 1. when the loading function promise resolves/rejects
  503. // 2. when cancelling the loading operation
  504. //
  505. // After the loading operation is cancelled, `this.resolvePendingTask` no longer represents this
  506. // particular task, but this `await` may eventually resolve/reject. Thus, when we cancel in
  507. // response to (1) below, we need to cancel the locally saved task.
  508. let resolvePendingTask = (this.resolvePendingTask =
  509. this.pendingTasks.add());
  510. const { signal: abortSignal } = (this.pendingController = new AbortController());
  511. try {
  512. // The actual loading is run through `untracked` - only the request side of `resource` is
  513. // reactive. This avoids any confusion with signals tracking or not tracking depending on
  514. // which side of the `await` they are.
  515. const stream = await untracked(() => {
  516. return this.loaderFn({
  517. params: extRequest.request,
  518. // TODO(alxhub): cleanup after g3 removal of `request` alias.
  519. request: extRequest.request,
  520. abortSignal,
  521. previous: {
  522. status: previousStatus,
  523. },
  524. });
  525. });
  526. // If this request has been aborted, or the current request no longer
  527. // matches this load, then we should ignore this resolution.
  528. if (abortSignal.aborted || untracked(this.extRequest) !== extRequest) {
  529. return;
  530. }
  531. this.state.set({
  532. extRequest,
  533. status: 'resolved',
  534. previousStatus: 'resolved',
  535. stream,
  536. });
  537. }
  538. catch (err) {
  539. if (abortSignal.aborted || untracked(this.extRequest) !== extRequest) {
  540. return;
  541. }
  542. this.state.set({
  543. extRequest,
  544. status: 'resolved',
  545. previousStatus: 'error',
  546. stream: signal({ error: encapsulateResourceError(err) }),
  547. });
  548. }
  549. finally {
  550. // Resolve the pending task now that the resource has a value.
  551. resolvePendingTask?.();
  552. resolvePendingTask = undefined;
  553. }
  554. }
  555. abortInProgressLoad() {
  556. untracked(() => this.pendingController?.abort());
  557. this.pendingController = undefined;
  558. // Once the load is aborted, we no longer want to block stability on its resolution.
  559. this.resolvePendingTask?.();
  560. this.resolvePendingTask = undefined;
  561. }
  562. }
  563. /**
  564. * Wraps an equality function to handle either value being `undefined`.
  565. */
  566. function wrapEqualityFn(equal) {
  567. return (a, b) => (a === undefined || b === undefined ? a === b : equal(a, b));
  568. }
  569. function getLoader(options) {
  570. if (isStreamingResourceOptions(options)) {
  571. return options.stream;
  572. }
  573. return async (params) => {
  574. try {
  575. return signal({ value: await options.loader(params) });
  576. }
  577. catch (err) {
  578. return signal({ error: encapsulateResourceError(err) });
  579. }
  580. };
  581. }
  582. function isStreamingResourceOptions(options) {
  583. return !!options.stream;
  584. }
  585. /**
  586. * Project from a state with `ResourceInternalStatus` to the user-facing `ResourceStatus`
  587. */
  588. function projectStatusOfState(state) {
  589. switch (state.status) {
  590. case 'loading':
  591. return state.extRequest.reload === 0 ? 'loading' : 'reloading';
  592. case 'resolved':
  593. return isResolved(state.stream()) ? 'resolved' : 'error';
  594. default:
  595. return state.status;
  596. }
  597. }
  598. function isResolved(state) {
  599. return state.error === undefined;
  600. }
  601. function encapsulateResourceError(error) {
  602. if (error instanceof Error) {
  603. return error;
  604. }
  605. return new ResourceWrappedError(error);
  606. }
  607. class ResourceValueError extends Error {
  608. constructor(error) {
  609. super(ngDevMode
  610. ? `Resource is currently in an error state (see Error.cause for details): ${error.message}`
  611. : error.message, { cause: error });
  612. }
  613. }
  614. class ResourceWrappedError extends Error {
  615. constructor(error) {
  616. super(ngDevMode
  617. ? `Resource returned an error that's not an Error instance: ${String(error)}. Check this error's .cause for the actual error.`
  618. : String(error), { cause: error });
  619. }
  620. }
  621. export { OutputEmitterRef, ResourceImpl, computed, effect, encapsulateResourceError, getOutputDestroyRef, linkedSignal, resource, untracked };
  622. //# sourceMappingURL=resource.mjs.map