@angular/core
Version:
Angular - the core framework
387 lines (380 loc) • 12 kB
JavaScript
/**
* @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