/**
 * @license Angular v21.1.4
 * (c) 2010-2026 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
