123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630 |
- /**
- * @license Angular v20.1.0
- * (c) 2010-2025 Google LLC. https://angular.io/
- * License: MIT
- */
- 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';
- import { setActiveConsumer, createComputed, SIGNAL, consumerDestroy, REACTIVE_NODE, isInNotificationPhase, consumerPollProducersForChange, consumerBeforeComputation, consumerAfterComputation } from './signal.mjs';
- import { untracked as untracked$1, createLinkedSignal, linkedSignalSetFn, linkedSignalUpdateFn } from './untracked.mjs';
- /**
- * An `OutputEmitterRef` is created by the `output()` function and can be
- * used to emit values to consumers of your directive or component.
- *
- * Consumers of your directive/component can bind to the output and
- * subscribe to changes via the bound event syntax. For example:
- *
- * ```html
- * <my-comp (valueChange)="processNewValue($event)" />
- * ```
- *
- * @publicAPI
- */
- class OutputEmitterRef {
- destroyed = false;
- listeners = null;
- errorHandler = inject(ErrorHandler, { optional: true });
- /** @internal */
- destroyRef = inject(DestroyRef);
- constructor() {
- // Clean-up all listeners and mark as destroyed upon destroy.
- this.destroyRef.onDestroy(() => {
- this.destroyed = true;
- this.listeners = null;
- });
- }
- subscribe(callback) {
- if (this.destroyed) {
- throw new RuntimeError(953 /* RuntimeErrorCode.OUTPUT_REF_DESTROYED */, ngDevMode &&
- 'Unexpected subscription to destroyed `OutputRef`. ' +
- 'The owning directive/component is destroyed.');
- }
- (this.listeners ??= []).push(callback);
- return {
- unsubscribe: () => {
- const idx = this.listeners?.indexOf(callback);
- if (idx !== undefined && idx !== -1) {
- this.listeners?.splice(idx, 1);
- }
- },
- };
- }
- /** Emits a new value to the output. */
- emit(value) {
- if (this.destroyed) {
- console.warn(formatRuntimeError(953 /* RuntimeErrorCode.OUTPUT_REF_DESTROYED */, ngDevMode &&
- 'Unexpected emit for destroyed `OutputRef`. ' +
- 'The owning directive/component is destroyed.'));
- return;
- }
- if (this.listeners === null) {
- return;
- }
- const previousConsumer = setActiveConsumer(null);
- try {
- for (const listenerFn of this.listeners) {
- try {
- listenerFn(value);
- }
- catch (err) {
- this.errorHandler?.handleError(err);
- }
- }
- }
- finally {
- setActiveConsumer(previousConsumer);
- }
- }
- }
- /** Gets the owning `DestroyRef` for the given output. */
- function getOutputDestroyRef(ref) {
- return ref.destroyRef;
- }
- /**
- * Execute an arbitrary function in a non-reactive (non-tracking) context. The executed function
- * can, optionally, return a value.
- */
- function untracked(nonReactiveReadsFn) {
- return untracked$1(nonReactiveReadsFn);
- }
- /**
- * Create a computed `Signal` which derives a reactive value from an expression.
- */
- function computed(computation, options) {
- const getter = createComputed(computation, options?.equal);
- if (ngDevMode) {
- getter.toString = () => `[Computed: ${getter()}]`;
- getter[SIGNAL].debugName = options?.debugName;
- }
- return getter;
- }
- class EffectRefImpl {
- [SIGNAL];
- constructor(node) {
- this[SIGNAL] = node;
- }
- destroy() {
- this[SIGNAL].destroy();
- }
- }
- /**
- * Registers an "effect" that will be scheduled & executed whenever the signals that it reads
- * changes.
- *
- * Angular has two different kinds of effect: component effects and root effects. Component effects
- * are created when `effect()` is called from a component, directive, or within a service of a
- * component/directive. Root effects are created when `effect()` is called from outside the
- * component tree, such as in a root service.
- *
- * The two effect types differ in their timing. Component effects run as a component lifecycle
- * event during Angular's synchronization (change detection) process, and can safely read input
- * signals or create/destroy views that depend on component state. Root effects run as microtasks
- * and have no connection to the component tree or change detection.
- *
- * `effect()` must be run in injection context, unless the `injector` option is manually specified.
- *
- * @publicApi 20.0
- */
- function effect(effectFn, options) {
- ngDevMode &&
- assertNotInReactiveContext(effect, 'Call `effect` outside of a reactive context. For example, schedule the ' +
- 'effect inside the component constructor.');
- if (ngDevMode && !options?.injector) {
- assertInInjectionContext(effect);
- }
- if (ngDevMode && options?.allowSignalWrites !== undefined) {
- console.warn(`The 'allowSignalWrites' flag is deprecated and no longer impacts effect() (writes are always allowed)`);
- }
- const injector = options?.injector ?? inject(Injector);
- let destroyRef = options?.manualCleanup !== true ? injector.get(DestroyRef) : null;
- let node;
- const viewContext = injector.get(ViewContext, null, { optional: true });
- const notifier = injector.get(ChangeDetectionScheduler);
- if (viewContext !== null) {
- // This effect was created in the context of a view, and will be associated with the view.
- node = createViewEffect(viewContext.view, notifier, effectFn);
- if (destroyRef instanceof NodeInjectorDestroyRef && destroyRef._lView === viewContext.view) {
- // The effect is being created in the same view as the `DestroyRef` references, so it will be
- // automatically destroyed without the need for an explicit `DestroyRef` registration.
- destroyRef = null;
- }
- }
- else {
- // This effect was created outside the context of a view, and will be scheduled independently.
- node = createRootEffect(effectFn, injector.get(EffectScheduler), notifier);
- }
- node.injector = injector;
- if (destroyRef !== null) {
- // If we need to register for cleanup, do that here.
- node.onDestroyFn = destroyRef.onDestroy(() => node.destroy());
- }
- const effectRef = new EffectRefImpl(node);
- if (ngDevMode) {
- node.debugName = options?.debugName ?? '';
- const prevInjectorProfilerContext = setInjectorProfilerContext({ injector, token: null });
- try {
- emitEffectCreatedEvent(effectRef);
- }
- finally {
- setInjectorProfilerContext(prevInjectorProfilerContext);
- }
- }
- return effectRef;
- }
- const BASE_EFFECT_NODE =
- /* @__PURE__ */ (() => ({
- ...REACTIVE_NODE,
- consumerIsAlwaysLive: true,
- consumerAllowSignalWrites: true,
- dirty: true,
- hasRun: false,
- cleanupFns: undefined,
- zone: null,
- kind: 'effect',
- onDestroyFn: noop,
- run() {
- this.dirty = false;
- if (ngDevMode && isInNotificationPhase()) {
- throw new Error(`Schedulers cannot synchronously execute watches while scheduling.`);
- }
- if (this.hasRun && !consumerPollProducersForChange(this)) {
- return;
- }
- this.hasRun = true;
- const registerCleanupFn = (cleanupFn) => (this.cleanupFns ??= []).push(cleanupFn);
- const prevNode = consumerBeforeComputation(this);
- // We clear `setIsRefreshingViews` so that `markForCheck()` within the body of an effect will
- // cause CD to reach the component in question.
- const prevRefreshingViews = setIsRefreshingViews(false);
- try {
- this.maybeCleanup();
- this.fn(registerCleanupFn);
- }
- finally {
- setIsRefreshingViews(prevRefreshingViews);
- consumerAfterComputation(this, prevNode);
- }
- },
- maybeCleanup() {
- if (!this.cleanupFns?.length) {
- return;
- }
- const prevConsumer = setActiveConsumer(null);
- try {
- // Attempt to run the cleanup functions. Regardless of failure or success, we consider
- // cleanup "completed" and clear the list for the next run of the effect. Note that an error
- // from the cleanup function will still crash the current run of the effect.
- while (this.cleanupFns.length) {
- this.cleanupFns.pop()();
- }
- }
- finally {
- this.cleanupFns = [];
- setActiveConsumer(prevConsumer);
- }
- },
- }))();
- const ROOT_EFFECT_NODE =
- /* @__PURE__ */ (() => ({
- ...BASE_EFFECT_NODE,
- consumerMarkedDirty() {
- this.scheduler.schedule(this);
- this.notifier.notify(12 /* NotificationSource.RootEffect */);
- },
- destroy() {
- consumerDestroy(this);
- this.onDestroyFn();
- this.maybeCleanup();
- this.scheduler.remove(this);
- },
- }))();
- const VIEW_EFFECT_NODE =
- /* @__PURE__ */ (() => ({
- ...BASE_EFFECT_NODE,
- consumerMarkedDirty() {
- this.view[FLAGS] |= 8192 /* LViewFlags.HasChildViewsToRefresh */;
- markAncestorsForTraversal(this.view);
- this.notifier.notify(13 /* NotificationSource.ViewEffect */);
- },
- destroy() {
- consumerDestroy(this);
- this.onDestroyFn();
- this.maybeCleanup();
- this.view[EFFECTS]?.delete(this);
- },
- }))();
- function createViewEffect(view, notifier, fn) {
- const node = Object.create(VIEW_EFFECT_NODE);
- node.view = view;
- node.zone = typeof Zone !== 'undefined' ? Zone.current : null;
- node.notifier = notifier;
- node.fn = fn;
- view[EFFECTS] ??= new Set();
- view[EFFECTS].add(node);
- node.consumerMarkedDirty(node);
- return node;
- }
- function createRootEffect(fn, scheduler, notifier) {
- const node = Object.create(ROOT_EFFECT_NODE);
- node.fn = fn;
- node.scheduler = scheduler;
- node.notifier = notifier;
- node.zone = typeof Zone !== 'undefined' ? Zone.current : null;
- node.scheduler.add(node);
- node.notifier.notify(12 /* NotificationSource.RootEffect */);
- return node;
- }
- const identityFn = (v) => v;
- function linkedSignal(optionsOrComputation, options) {
- if (typeof optionsOrComputation === 'function') {
- const getter = createLinkedSignal(optionsOrComputation, (identityFn), options?.equal);
- return upgradeLinkedSignalGetter(getter);
- }
- else {
- const getter = createLinkedSignal(optionsOrComputation.source, optionsOrComputation.computation, optionsOrComputation.equal);
- return upgradeLinkedSignalGetter(getter);
- }
- }
- function upgradeLinkedSignalGetter(getter) {
- if (ngDevMode) {
- getter.toString = () => `[LinkedSignal: ${getter()}]`;
- }
- const node = getter[SIGNAL];
- const upgradedGetter = getter;
- upgradedGetter.set = (newValue) => linkedSignalSetFn(node, newValue);
- upgradedGetter.update = (updateFn) => linkedSignalUpdateFn(node, updateFn);
- upgradedGetter.asReadonly = signalAsReadonlyFn.bind(getter);
- return upgradedGetter;
- }
- /**
- * Whether a `Resource.value()` should throw an error when the resource is in the error state.
- *
- * This internal flag is being used to gradually roll out this behavior.
- */
- const RESOURCE_VALUE_THROWS_ERRORS_DEFAULT = true;
- function resource(options) {
- if (ngDevMode && !options?.injector) {
- assertInInjectionContext(resource);
- }
- const oldNameForParams = options.request;
- const params = (options.params ?? oldNameForParams ?? (() => null));
- return new ResourceImpl(params, getLoader(options), options.defaultValue, options.equal ? wrapEqualityFn(options.equal) : undefined, options.injector ?? inject(Injector), RESOURCE_VALUE_THROWS_ERRORS_DEFAULT);
- }
- /**
- * Base class which implements `.value` as a `WritableSignal` by delegating `.set` and `.update`.
- */
- class BaseWritableResource {
- value;
- constructor(value) {
- this.value = value;
- this.value.set = this.set.bind(this);
- this.value.update = this.update.bind(this);
- this.value.asReadonly = signalAsReadonlyFn;
- }
- isError = computed(() => this.status() === 'error');
- update(updateFn) {
- this.set(updateFn(untracked(this.value)));
- }
- isLoading = computed(() => this.status() === 'loading' || this.status() === 'reloading');
- hasValue() {
- // Note: we specifically read `isError()` instead of `status()` here to avoid triggering
- // reactive consumers which read `hasValue()`. This way, if `hasValue()` is used inside of an
- // effect, it doesn't cause the effect to rerun on every status change.
- if (this.isError()) {
- return false;
- }
- return this.value() !== undefined;
- }
- asReadonly() {
- return this;
- }
- }
- /**
- * Implementation for `resource()` which uses a `linkedSignal` to manage the resource's state.
- */
- class ResourceImpl extends BaseWritableResource {
- loaderFn;
- equal;
- pendingTasks;
- /**
- * The current state of the resource. Status, value, and error are derived from this.
- */
- state;
- /**
- * Combines the current request with a reload counter which allows the resource to be reloaded on
- * imperative command.
- */
- extRequest;
- effectRef;
- pendingController;
- resolvePendingTask = undefined;
- destroyed = false;
- unregisterOnDestroy;
- constructor(request, loaderFn, defaultValue, equal, injector, throwErrorsFromValue = RESOURCE_VALUE_THROWS_ERRORS_DEFAULT) {
- super(
- // Feed a computed signal for the value to `BaseWritableResource`, which will upgrade it to a
- // `WritableSignal` that delegates to `ResourceImpl.set`.
- computed(() => {
- const streamValue = this.state().stream?.();
- if (!streamValue) {
- return defaultValue;
- }
- // Prevents `hasValue()` from throwing an error when a reload happened in the error state
- if (this.state().status === 'loading' && this.error()) {
- return defaultValue;
- }
- if (!isResolved(streamValue)) {
- if (throwErrorsFromValue) {
- throw new ResourceValueError(this.error());
- }
- else {
- return defaultValue;
- }
- }
- return streamValue.value;
- }, { equal }));
- this.loaderFn = loaderFn;
- this.equal = equal;
- // Extend `request()` to include a writable reload signal.
- this.extRequest = linkedSignal({
- source: request,
- computation: (request) => ({ request, reload: 0 }),
- });
- // The main resource state is managed in a `linkedSignal`, which allows the resource to change
- // state instantaneously when the request signal changes.
- this.state = linkedSignal({
- // Whenever the request changes,
- source: this.extRequest,
- // Compute the state of the resource given a change in status.
- computation: (extRequest, previous) => {
- const status = extRequest.request === undefined ? 'idle' : 'loading';
- if (!previous) {
- return {
- extRequest,
- status,
- previousStatus: 'idle',
- stream: undefined,
- };
- }
- else {
- return {
- extRequest,
- status,
- previousStatus: projectStatusOfState(previous.value),
- // If the request hasn't changed, keep the previous stream.
- stream: previous.value.extRequest.request === extRequest.request
- ? previous.value.stream
- : undefined,
- };
- }
- },
- });
- this.effectRef = effect(this.loadEffect.bind(this), {
- injector,
- manualCleanup: true,
- });
- this.pendingTasks = injector.get(PendingTasks);
- // Cancel any pending request when the resource itself is destroyed.
- this.unregisterOnDestroy = injector.get(DestroyRef).onDestroy(() => this.destroy());
- }
- status = computed(() => projectStatusOfState(this.state()));
- error = computed(() => {
- const stream = this.state().stream?.();
- return stream && !isResolved(stream) ? stream.error : undefined;
- });
- /**
- * Called either directly via `WritableResource.set` or via `.value.set()`.
- */
- set(value) {
- if (this.destroyed) {
- return;
- }
- const error = untracked(this.error);
- const state = untracked(this.state);
- if (!error) {
- const current = untracked(this.value);
- if (state.status === 'local' &&
- (this.equal ? this.equal(current, value) : current === value)) {
- return;
- }
- }
- // Enter Local state with the user-defined value.
- this.state.set({
- extRequest: state.extRequest,
- status: 'local',
- previousStatus: 'local',
- stream: signal({ value }),
- });
- // We're departing from whatever state the resource was in previously, so cancel any in-progress
- // loading operations.
- this.abortInProgressLoad();
- }
- reload() {
- // We don't want to restart in-progress loads.
- const { status } = untracked(this.state);
- if (status === 'idle' || status === 'loading') {
- return false;
- }
- // Increment the request reload to trigger the `state` linked signal to switch us to `Reload`
- this.extRequest.update(({ request, reload }) => ({ request, reload: reload + 1 }));
- return true;
- }
- destroy() {
- this.destroyed = true;
- this.unregisterOnDestroy();
- this.effectRef.destroy();
- this.abortInProgressLoad();
- // Destroyed resources enter Idle state.
- this.state.set({
- extRequest: { request: undefined, reload: 0 },
- status: 'idle',
- previousStatus: 'idle',
- stream: undefined,
- });
- }
- async loadEffect() {
- const extRequest = this.extRequest();
- // Capture the previous status before any state transitions. Note that this is `untracked` since
- // we do not want the effect to depend on the state of the resource, only on the request.
- const { status: currentStatus, previousStatus } = untracked(this.state);
- if (extRequest.request === undefined) {
- // Nothing to load (and we should already be in a non-loading state).
- return;
- }
- else if (currentStatus !== 'loading') {
- // We're not in a loading or reloading state, so this loading request is stale.
- return;
- }
- // Cancel any previous loading attempts.
- this.abortInProgressLoad();
- // Capturing _this_ load's pending task in a local variable is important here. We may attempt to
- // resolve it twice:
- //
- // 1. when the loading function promise resolves/rejects
- // 2. when cancelling the loading operation
- //
- // After the loading operation is cancelled, `this.resolvePendingTask` no longer represents this
- // particular task, but this `await` may eventually resolve/reject. Thus, when we cancel in
- // response to (1) below, we need to cancel the locally saved task.
- let resolvePendingTask = (this.resolvePendingTask =
- this.pendingTasks.add());
- const { signal: abortSignal } = (this.pendingController = new AbortController());
- try {
- // The actual loading is run through `untracked` - only the request side of `resource` is
- // reactive. This avoids any confusion with signals tracking or not tracking depending on
- // which side of the `await` they are.
- const stream = await untracked(() => {
- return this.loaderFn({
- params: extRequest.request,
- // TODO(alxhub): cleanup after g3 removal of `request` alias.
- request: extRequest.request,
- abortSignal,
- previous: {
- status: previousStatus,
- },
- });
- });
- // If this request has been aborted, or the current request no longer
- // matches this load, then we should ignore this resolution.
- if (abortSignal.aborted || untracked(this.extRequest) !== extRequest) {
- return;
- }
- this.state.set({
- extRequest,
- status: 'resolved',
- previousStatus: 'resolved',
- stream,
- });
- }
- catch (err) {
- if (abortSignal.aborted || untracked(this.extRequest) !== extRequest) {
- return;
- }
- this.state.set({
- extRequest,
- status: 'resolved',
- previousStatus: 'error',
- stream: signal({ error: encapsulateResourceError(err) }),
- });
- }
- finally {
- // Resolve the pending task now that the resource has a value.
- resolvePendingTask?.();
- resolvePendingTask = undefined;
- }
- }
- abortInProgressLoad() {
- untracked(() => this.pendingController?.abort());
- this.pendingController = undefined;
- // Once the load is aborted, we no longer want to block stability on its resolution.
- this.resolvePendingTask?.();
- this.resolvePendingTask = undefined;
- }
- }
- /**
- * Wraps an equality function to handle either value being `undefined`.
- */
- function wrapEqualityFn(equal) {
- return (a, b) => (a === undefined || b === undefined ? a === b : equal(a, b));
- }
- function getLoader(options) {
- if (isStreamingResourceOptions(options)) {
- return options.stream;
- }
- return async (params) => {
- try {
- return signal({ value: await options.loader(params) });
- }
- catch (err) {
- return signal({ error: encapsulateResourceError(err) });
- }
- };
- }
- function isStreamingResourceOptions(options) {
- return !!options.stream;
- }
- /**
- * Project from a state with `ResourceInternalStatus` to the user-facing `ResourceStatus`
- */
- function projectStatusOfState(state) {
- switch (state.status) {
- case 'loading':
- return state.extRequest.reload === 0 ? 'loading' : 'reloading';
- case 'resolved':
- return isResolved(state.stream()) ? 'resolved' : 'error';
- default:
- return state.status;
- }
- }
- function isResolved(state) {
- return state.error === undefined;
- }
- function encapsulateResourceError(error) {
- if (error instanceof Error) {
- return error;
- }
- return new ResourceWrappedError(error);
- }
- class ResourceValueError extends Error {
- constructor(error) {
- super(ngDevMode
- ? `Resource is currently in an error state (see Error.cause for details): ${error.message}`
- : error.message, { cause: error });
- }
- }
- class ResourceWrappedError extends Error {
- constructor(error) {
- super(ngDevMode
- ? `Resource returned an error that's not an Error instance: ${String(error)}. Check this error's .cause for the actual error.`
- : String(error), { cause: error });
- }
- }
- export { OutputEmitterRef, ResourceImpl, computed, effect, encapsulateResourceError, getOutputDestroyRef, linkedSignal, resource, untracked };
- //# sourceMappingURL=resource.mjs.map
|