// fallow-ignore-file unused-file
// Async substrate for solidify.md §1 (headless primitives) + §4 (suspense-native
// async). Will be consumed by `createComboboxItem` / `createSelectItem`
// async-iterable paths once §1 lands.

import { action, createMemo, onCleanup, refresh } from "solid-js";
import { readOption } from "../maybe-accessor.jsx";

// Stage 03 — async-compound shape spike.
//
// Validates the four axes from the stage stub against a minimal data
// primitive that's small enough to test in isolation but exercises every
// concern that the live Combobox-async port will hit:
//
//   S — `commit` exposed as an `action()`-typed setter (visible in the
//       type, no "transparent upgrade").
//   U — resource-owning memo gated by `lazy: true` + `unobserved` teardown,
//       which lets us hang an AbortController off the *one* signal that
//       represents subscription liveness.
//   W — `items()` is async (`createMemo` returning a Promise). Reads
//       outside `<Loading>` throw `ASYNC_OUTSIDE_LOADING_BOUNDARY`. The
//       primitive does NOT bake in a wrapper-internal `<Loading>`.
//   Stage 02 error contract — fetcher throws/rejects propagate through
//       the graph to `<Errored>`. The primitive does not catch.
//
// `createProjection` (axis T) is intentionally not exercised here; for
// scalar selection there's nothing to project. The retro carries this
// forward as deferred.
//
// Companion doc: work-left/primitive-template.md §2.4.

export function createAsyncSource(options) {
  const query = () => readOption(options.query);

  // `lazy: true` defers the first fetch until something reads `items()`.
  // Combined with `unobserved`, the resource lifecycle is:
  //
  //   first read → fetch starts (controller A live).
  //   query flip → memo re-runs → onCleanup aborts A → fetch starts B.
  //   all readers go away → unobserved → abort B + null out.
  //   reader returns → memo recomputes → fresh controller C.
  //
  // The AbortController is held in a closure-local that the unobserved
  // callback can reach. Per RFC 01, `unobserved` is per-signal/memo —
  // hanging it off the resource-owning memo gives compound-level
  // teardown semantics for free.
  let activeController = null;
  // Snapshot bookkeeping — `lastError` is the most recent fetch rejection
  // (cleared whenever a new fetch starts). Reading `items()` outside a
  // `<Loading>` boundary throws a runtime sentinel that's not a real
  // rejection; this flag lets `snapshot()` distinguish "fetch in flight"
  // from "fetch rejected" without inspecting Solid-internal error types.
  let lastError;
  let lastSettledItems;
  const items = createMemo(() => {
    const q = query();
    activeController?.abort();
    const ctrl = new AbortController();
    activeController = ctrl;

    // Fires when the memo re-evaluates (dependency change) or
    // when its owner disposes. Belt-and-braces with `unobserved`:
    // onCleanup covers re-runs and dispose, unobserved covers
    // "all subscribers gone" without re-evaluation.
    onCleanup(() => {
      if (activeController === ctrl) activeController = null;
      ctrl.abort();
    });
    lastError = undefined;
    const p = options.fetch(q, ctrl.signal);
    p.then(v => {
      if (activeController === ctrl) lastSettledItems = v;
    }, e => {
      if (activeController === ctrl) lastError = e;
    });
    return p;
  }, {
    lazy: true,
    name: "async-source.items",
    unobserved: () => {
      activeController?.abort();
      activeController = null;
    }
  });
  // Cast: the memo's compute returns Promise<readonly V[]>, but Solid 2
  // async-memos surface the resolved value at the read site (suspending
  // in <Loading> until ready). The runtime type is the unwrapped form;
  // the declared compute type is the promise. This matches RFC 05 —
  // `createMemo(() => fetchUser())` reads as `User`, not `Promise<User>`.
  //
  // Cast target is `SourceAccessor` (= `Refreshable<Accessor<…>>`), not a
  // bare `Accessor` — `createMemo` returns the refreshable brand and we
  // keep it so `refresh(items)` below type-checks against the live memo.

  // `action()` wraps an async generator. The returned function is a
  // regular `(value: V) => Promise<void>` — visible in the type — that
  // runs inside a transition. Aborts cascade through the same controller
  // pattern as the fetch path; consumers can branch on `signal.aborted`
  // inside their commit fn. Beta.28 routes generator rejections to the
  // returned promise without the local signals patch older betas required.
  const commitControllers = new Set();
  onCleanup(() => {
    for (const controller of commitControllers) controller.abort();
    commitControllers.clear();
  });
  const commit = options.commit ? action(async function* (value) {
    const ctrl = new AbortController();
    commitControllers.add(ctrl);
    try {
      await options.commit(value, ctrl.signal);
      yield;
    } finally {
      commitControllers.delete(ctrl);
    }
  }) : async _value => undefined;
  const refreshItems = () => {
    // `refresh()` re-runs the memo even when `query` is unchanged.
    // `items` keeps its `Refreshable` brand from `createMemo`, so this
    // needs no cast.
    void refresh(items);
  };
  const api = {
    query,
    items,
    commit,
    refresh: refreshItems
  };
  // Custom snapshot override — async compounds use a `{ status, items,
  // error }` projection per template §2.5. Reading `items()` outside a
  // `<Loading>` boundary throws (a Promise for pending, an Error for
  // rejection); we catch both and surface them as data so observability
  // tools can call `snapshot(c)` from anywhere.
  //
  // Defined non-enumerable: `snapshot` is metadata, not part of the
  // compound's primary surface (`Object.keys(c)` should still report only
  // the four functional keys).
  const snapshotFn = () => {
    const out = {
      query: query()
    };
    if (lastError !== undefined) {
      out.status = "error";
      out.error = lastError;
    } else if (lastSettledItems !== undefined) {
      out.status = "settled";
      out.items = lastSettledItems;
    } else {
      out.status = "pending";
    }
    return out;
  };
  Object.defineProperty(api, "snapshot", {
    value: snapshotFn,
    enumerable: false,
    configurable: true,
    writable: true
  });
  return api;
}
//# sourceMappingURL=create-async-source.jsx.map
