UNPKG

@angular/core

Version:

Angular - the core framework

387 lines (380 loc) • 12 kB
/** * @license Angular v21.0.5 * (c) 2010-2025 Google LLC. https://angular.dev/ * License: MIT */ import { inject, ErrorHandler, DestroyRef, RuntimeError, formatRuntimeError, signalAsReadonlyFn, assertInInjectionContext, Injector, effect, PendingTasks, untracked, signal } from './_untracked-chunk.mjs'; import { setActiveConsumer, createComputed, SIGNAL } from './_effect-chunk.mjs'; import { createLinkedSignal, linkedSignalSetFn, linkedSignalUpdateFn } from './_linked_signal-chunk.mjs'; class OutputEmitterRef { destroyed = false; listeners = null; errorHandler = inject(ErrorHandler, { optional: true }); destroyRef = inject(DestroyRef); constructor() { this.destroyRef.onDestroy(() => { this.destroyed = true; this.listeners = null; }); } subscribe(callback) { if (this.destroyed) { throw new RuntimeError(953, 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); } } }; } emit(value) { if (this.destroyed) { console.warn(formatRuntimeError(953, 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); } } } function getOutputDestroyRef(ref) { return ref.destroyRef; } function computed(computation, options) { const getter = createComputed(computation, options?.equal); if (ngDevMode) { getter.toString = () => `[Computed: ${getter()}]`; getter[SIGNAL].debugName = options?.debugName; } return getter; } const identityFn = v => v; function linkedSignal(optionsOrComputation, options) { if (typeof optionsOrComputation === 'function') { const getter = createLinkedSignal(optionsOrComputation, identityFn, options?.equal); return upgradeLinkedSignalGetter(getter, options?.debugName); } else { const getter = createLinkedSignal(optionsOrComputation.source, optionsOrComputation.computation, optionsOrComputation.equal); return upgradeLinkedSignalGetter(getter, optionsOrComputation.debugName); } } function upgradeLinkedSignalGetter(getter, debugName) { if (ngDevMode) { getter.toString = () => `[LinkedSignal: ${getter()}]`; getter[SIGNAL].debugName = debugName; } 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; } 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.debugName, options.injector ?? inject(Injector)); } class BaseWritableResource { value; isLoading; constructor(value, debugName) { this.value = value; this.value.set = this.set.bind(this); this.value.update = this.update.bind(this); this.value.asReadonly = signalAsReadonlyFn; this.isLoading = computed(() => this.status() === 'loading' || this.status() === 'reloading', ngDevMode ? createDebugNameObject(debugName, 'isLoading') : undefined); } isError = computed(() => this.status() === 'error'); update(updateFn) { this.set(updateFn(untracked(this.value))); } isValueDefined = computed(() => { if (this.isError()) { return false; } return this.value() !== undefined; }); hasValue() { return this.isValueDefined(); } asReadonly() { return this; } } class ResourceImpl extends BaseWritableResource { loaderFn; equal; debugName; pendingTasks; state; extRequest; effectRef; pendingController; resolvePendingTask = undefined; destroyed = false; unregisterOnDestroy; status; error; constructor(request, loaderFn, defaultValue, equal, debugName, injector) { super(computed(() => { const streamValue = this.state().stream?.(); if (!streamValue) { return defaultValue; } if (this.state().status === 'loading' && this.error()) { return defaultValue; } if (!isResolved(streamValue)) { throw new ResourceValueError(this.error()); } return streamValue.value; }, { equal, ...(ngDevMode ? createDebugNameObject(debugName, 'value') : undefined) }), debugName); this.loaderFn = loaderFn; this.equal = equal; this.debugName = debugName; this.extRequest = linkedSignal({ source: request, computation: request => ({ request, reload: 0 }), ...(ngDevMode ? createDebugNameObject(debugName, 'extRequest') : undefined) }); this.state = linkedSignal({ source: this.extRequest, 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), stream: previous.value.extRequest.request === extRequest.request ? previous.value.stream : undefined }; } }, ...(ngDevMode ? createDebugNameObject(debugName, 'state') : undefined) }); this.effectRef = effect(this.loadEffect.bind(this), { injector, manualCleanup: true, ...(ngDevMode ? createDebugNameObject(debugName, 'loadEffect') : undefined) }); this.pendingTasks = injector.get(PendingTasks); this.unregisterOnDestroy = injector.get(DestroyRef).onDestroy(() => this.destroy()); this.status = computed(() => projectStatusOfState(this.state()), ngDevMode ? createDebugNameObject(debugName, 'status') : undefined); this.error = computed(() => { const stream = this.state().stream?.(); return stream && !isResolved(stream) ? stream.error : undefined; }, ngDevMode ? createDebugNameObject(debugName, 'error') : undefined); } 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; } } this.state.set({ extRequest: state.extRequest, status: 'local', previousStatus: 'local', stream: signal({ value }, ngDevMode ? createDebugNameObject(this.debugName, 'stream') : undefined) }); this.abortInProgressLoad(); } reload() { const { status } = untracked(this.state); if (status === 'idle' || status === 'loading') { return false; } this.extRequest.update(({ request, reload }) => ({ request, reload: reload + 1 })); return true; } destroy() { this.destroyed = true; this.unregisterOnDestroy(); this.effectRef.destroy(); this.abortInProgressLoad(); this.state.set({ extRequest: { request: undefined, reload: 0 }, status: 'idle', previousStatus: 'idle', stream: undefined }); } async loadEffect() { const extRequest = this.extRequest(); const { status: currentStatus, previousStatus } = untracked(this.state); if (extRequest.request === undefined) { return; } else if (currentStatus !== 'loading') { return; } this.abortInProgressLoad(); let resolvePendingTask = this.resolvePendingTask = this.pendingTasks.add(); const { signal: abortSignal } = this.pendingController = new AbortController(); try { const stream = await untracked(() => { return this.loaderFn({ params: extRequest.request, request: extRequest.request, abortSignal, previous: { status: previousStatus } }); }); 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) }, ngDevMode ? createDebugNameObject(this.debugName, 'stream') : undefined) }); } finally { resolvePendingTask?.(); resolvePendingTask = undefined; } } abortInProgressLoad() { untracked(() => this.pendingController?.abort()); this.pendingController = undefined; this.resolvePendingTask?.(); this.resolvePendingTask = 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) }, ngDevMode ? createDebugNameObject(options.debugName, 'stream') : undefined); } catch (err) { return signal({ error: encapsulateResourceError(err) }, ngDevMode ? createDebugNameObject(options.debugName, 'stream') : undefined); } }; } function isStreamingResourceOptions(options) { return !!options.stream; } 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 createDebugNameObject(resourceDebugName, internalSignalDebugName) { return { debugName: `Resource${resourceDebugName ? '#' + resourceDebugName : ''}.${internalSignalDebugName}` }; } function encapsulateResourceError(error) { if (isErrorLike(error)) { return error; } return new ResourceWrappedError(error); } function isErrorLike(error) { return error instanceof Error || typeof error === 'object' && typeof error.name === 'string' && typeof error.message === 'string'; } 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, encapsulateResourceError, getOutputDestroyRef, linkedSignal, resource }; //# sourceMappingURL=_resource-chunk.mjs.map