{"version":3,"sources":["../../src/protocol/derivative-status.ts"],"sourcesContent":["/**\n * Derivative status: the lifecycle of the question behind a derived scope,\n * as the party that READS the answer sees it.\n *\n * @remarks\n * A builder registers a question with {@link registerQuestion} and follows it\n * with {@link waitForQuestion}, both of which need a write session. The app\n * that only consumes the answer holds no write entry at all — a consent flow\n * grants it a bare read on the derived scope — so it cannot open one, and\n * `GET /v1/data/<derivedScope>` answers 404 for all three of \"computing right\n * now\", \"failed but retrying\" and \"failed for good\".\n *\n * `GET /v1/derivatives/status?derivedScope=<scope>` is that reader's view.\n * Authorization is the data read's (a live grant covering the derived scope,\n * or the owner), nothing is served and nothing is charged, and the view is\n * deliberately narrow: lifecycle, a coarse {@link DerivativeErrorCode} and\n * the next retry. The question text, the source scopes, the question id, the\n * registrar and the server's raw error string stay owner-only.\n *\n * Requires `personal-server-ts` with the status route; an older Personal\n * Server answers 404 for the route itself.\n *\n * @category Protocol\n */\n\nimport { z } from \"zod\";\nimport { buildWeb3SignedHeader } from \"../auth/web3-signed-builder\";\nimport {\n  DerivativeQuestionRejectedError,\n  DerivativeQuestionTimeoutError,\n  WriteRequestError,\n  WriteTransportError,\n  type PersonalServerWriteError,\n} from \"../errors\";\nimport {\n  DerivativeErrorCodeSchema,\n  personalServerErrorFromQuestionResponse,\n  QuestionStatusSchema,\n  type QuestionStatus,\n} from \"./derivative-questions\";\nimport {\n  resolveWriteSigner,\n  type ResolveWriteSignerOptions,\n  type WriteSignerSource,\n} from \"./write-signer\";\n\nexport {\n  DERIVATIVE_ERROR_CODES,\n  DerivativeErrorCodeSchema,\n  type DerivativeErrorCode,\n} from \"./derivative-questions\";\n\n/** The reader-facing status route. */\nexport const DERIVATIVE_STATUS_PATH = \"/v1/derivatives/status\";\n\n/** How long {@link waitForDerivativeStatus} polls before giving up. */\nexport const DEFAULT_DERIVATIVE_STATUS_TIMEOUT_MS = 120_000;\n\n/**\n * How long {@link waitForDerivativeStatus} waits between polls when the\n * server names no retry time of its own.\n */\nexport const DEFAULT_DERIVATIVE_STATUS_POLL_INTERVAL_MS = 2_000;\n\nconst nullable = <T extends z.ZodTypeAny>(schema: T) =>\n  schema\n    .nullish()\n    .transform((value: z.infer<T> | null | undefined) => value ?? null);\n\n/**\n * The status of the derived scope, not of one registration: when several\n * questions write the same scope, the server reports the most optimistic\n * true state, because serving data is registration-agnostic.\n */\nexport const DerivativeStatusSchema = z.object({\n  derivedScope: z.string().min(1),\n  status: QuestionStatusSchema,\n  /** When the last compute finished, or `null` if none ever has. */\n  lastComputedAt: nullable(z.string()),\n  /** Local version of the derived record the last compute wrote. */\n  derivedVersion: nullable(z.number()),\n  derivedCollectedAt: nullable(z.string()),\n  /** The failure class; `null` unless `status` is `failed`. */\n  errorCode: nullable(DerivativeErrorCodeSchema),\n  /**\n   * Seconds until the Personal Server's next automatic retry, or `null` when\n   * none is pending or running — the terminal signature. Poll on this cadence\n   * rather than guessing one.\n   */\n  retryAfterSeconds: nullable(z.number()),\n});\n\n/** @see {@link DerivativeStatusSchema} */\nexport type DerivativeStatus = z.infer<typeof DerivativeStatusSchema>;\n\n/** What {@link getDerivativeStatus} needs to sign and send one read. */\nexport interface GetDerivativeStatusParams extends ResolveWriteSignerOptions {\n  /** Personal Server origin, e.g. `https://ps.example.com`. */\n  personalServerUrl: string;\n  /** The derived scope whose question to observe. */\n  derivedScope: string;\n  /**\n   * A grant covering the derived scope, sent as the signed `grantId` claim.\n   * Omit only when the signer is the Personal Server's owner, who is\n   * authorized without one.\n   */\n  grantId?: string;\n  /** Reader key: a viem `LocalAccount`, `WalletClient`, or `{ signMessage }`. */\n  signer: WriteSignerSource;\n  /** Web3Signed audience; defaults to `personalServerUrl`. */\n  audience?: string;\n  /** `fetch` to use; defaults to `globalThis.fetch`. */\n  fetch?: typeof fetch;\n  /** Extra request headers. */\n  headers?: HeadersInit;\n  /** Aborts the request in flight. */\n  signal?: AbortSignal;\n}\n\n/** What {@link waitForDerivativeStatus} polls with. */\nexport interface WaitForDerivativeStatusParams extends GetDerivativeStatusParams {\n  /** Give up after this long (default 120s). */\n  timeoutMs?: number;\n  /**\n   * Wait between polls when the server names no retry time (default 2s).\n   * A `retryAfterSeconds` from the server replaces this outright, longer or\n   * shorter: it is when the next compute actually happens.\n   */\n  pollIntervalMs?: number;\n  /**\n   * Aborts the wait, and the request in flight with it: the signal is passed\n   * to every poll, so an abort during a stalled request does not sit until\n   * the transport gives up.\n   */\n  signal?: AbortSignal;\n}\n\n/**\n * The request target for a status read: the query carries the derived scope,\n * the signed `uri` does not.\n *\n * @remarks\n * Like every Web3Signed read (data, lineage), the Personal Server verifies\n * the signature over the PATH; per-scope authorization is enforced live\n * against the caller's grant on each request, so the query needs no\n * signature to be safe. Only the write path signs path AND query, where a\n * parameter decides what is written.\n */\nexport function derivativeStatusTarget(derivedScope: string): string {\n  return `${DERIVATIVE_STATUS_PATH}?derivedScope=${encodeURIComponent(derivedScope)}`;\n}\n\nfunction normalizeBaseUrl(url: string): string {\n  return url.replace(/\\/+$/, \"\");\n}\n\nfunction resolveFetch(fetchFn: typeof fetch | undefined): typeof fetch {\n  const resolved = fetchFn ?? globalThis.fetch;\n  if (resolved === undefined) {\n    throw new WriteRequestError(\"No fetch implementation available\");\n  }\n  return resolved;\n}\n\nfunction requireDerivedScope(derivedScope: string): string {\n  if (typeof derivedScope !== \"string\" || derivedScope.length === 0) {\n    // The server answers 400 DERIVATIVE_DERIVED_SCOPE_REQUIRED; refuse before\n    // signing rather than spend a signature on a request that cannot pass.\n    throw new WriteRequestError(\"derivedScope is required\");\n  }\n  return derivedScope;\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n  if (ms <= 0) return Promise.resolve();\n  return new Promise((resolve, reject) => {\n    const timer = setTimeout(() => {\n      signal?.removeEventListener(\"abort\", onAbort);\n      resolve();\n    }, ms);\n    const onAbort = () => {\n      clearTimeout(timer);\n      reject(abortError(signal));\n    };\n    signal?.addEventListener(\"abort\", onAbort, { once: true });\n  });\n}\n\nfunction timeoutError(\n  latest: DerivativeStatus,\n  timeoutMs: number,\n): DerivativeQuestionTimeoutError {\n  return new DerivativeQuestionTimeoutError(\n    `Derived scope ${latest.derivedScope} was still ${latest.status} after ${timeoutMs}ms`,\n    {\n      derivedScope: latest.derivedScope,\n      status: latest.status,\n      errorCode: latest.errorCode,\n      retryAfterSeconds: latest.retryAfterSeconds,\n      timeoutMs,\n    },\n  );\n}\n\nfunction abortError(signal?: AbortSignal): Error {\n  const reason = signal?.reason;\n  return reason instanceof Error\n    ? reason\n    : new WriteRequestError(\"Derivative status wait was aborted\");\n}\n\n/**\n * Is this a state the reader can act on?\n *\n * @remarks\n * `ready` means the derived scope has an answer to read. A `failed` status\n * is settled only when no retry is pending: with `retryAfterSeconds` set the\n * Personal Server will compute again on its own, so the answer may still\n * arrive. `pending` and `stale` are always in flight.\n */\nexport function isDerivativeStatusSettled(status: DerivativeStatus): boolean {\n  if (status.status === \"ready\") return true;\n  return status.status === \"failed\" && status.retryAfterSeconds === null;\n}\n\n/**\n * Read the lifecycle of the question behind a derived scope.\n *\n * @remarks\n * Sends `GET /v1/derivatives/status?derivedScope=<scope>` with a Web3Signed\n * `Authorization` header carrying `grantId`, the same authentication a data\n * read uses. Nothing is charged: the route authorizes, it does not serve\n * data, so a priced grant raises no 402 here.\n *\n * @returns The status of the derived scope. When several registrations write\n *   it, the most optimistic true state answers — `ready`, then `stale`, then\n *   `pending`, then `failed` — because a duplicate that never wrote anything\n *   must not report away an answer the scope has.\n * @throws {DerivativeQuestionNotFoundError} 404: the caller may read the\n *   scope but no question stands behind it (and, on an older Personal\n *   Server, the route itself is unknown).\n * @throws {WriteForbiddenError} 403: the grant does not cover the derived\n *   scope. The check runs before any store lookup, so a caller cannot probe\n *   which scopes have questions.\n * @throws {DerivativeQuestionRejectedError} On any other non-2xx answer or\n *   an unparseable body.\n * @throws {WriteTransportError} When `fetch` itself failed.\n * @throws {WriteRequestError} On a missing `derivedScope` or no `fetch`.\n *\n * @example\n * ```typescript\n * const status = await getDerivativeStatus({\n *   personalServerUrl: \"https://ps.example.com\",\n *   derivedScope: \"coach.weekly\",\n *   grantId,\n *   signer,\n * });\n * if (status.status === \"ready\") {\n *   const record = await readPersonalServerData({ ... });\n * } else if (status.retryAfterSeconds !== null) {\n *   // Computing or retrying: come back then.\n * }\n * ```\n */\nexport async function getDerivativeStatus(\n  params: GetDerivativeStatusParams,\n): Promise<DerivativeStatus> {\n  const derivedScope = requireDerivedScope(params.derivedScope);\n  const fetchFn = resolveFetch(params.fetch);\n  const baseUrl = normalizeBaseUrl(params.personalServerUrl);\n  const signer = resolveWriteSigner(params.signer, { account: params.account });\n  const headers = new Headers(params.headers);\n  headers.set(\n    \"Authorization\",\n    await buildWeb3SignedHeader({\n      signMessage: signer.signMessage,\n      aud: params.audience ?? baseUrl,\n      method: \"GET\",\n      uri: DERIVATIVE_STATUS_PATH,\n      grantId: params.grantId,\n    }),\n  );\n\n  let response: Response;\n  try {\n    response = await fetchFn(\n      `${baseUrl}${derivativeStatusTarget(derivedScope)}`,\n      {\n        method: \"GET\",\n        headers,\n        ...(params.signal ? { signal: params.signal } : {}),\n      },\n    );\n  } catch (err) {\n    throw new WriteTransportError(\n      `Derivative status read failed: ${err instanceof Error ? err.message : String(err)}`,\n      1,\n      err,\n    );\n  }\n  if (!response.ok) {\n    throw (await personalServerErrorFromQuestionResponse(\n      response,\n    )) as PersonalServerWriteError;\n  }\n  let body: unknown;\n  try {\n    body = await response.json();\n  } catch (err) {\n    throw new DerivativeQuestionRejectedError(\n      \"Derivative status response is not JSON\",\n      response.status,\n      null,\n      { cause: err instanceof Error ? err.message : String(err) },\n    );\n  }\n  const parsed = DerivativeStatusSchema.safeParse(body);\n  if (!parsed.success) {\n    throw new DerivativeQuestionRejectedError(\n      \"Derivative status response is not a status view\",\n      response.status,\n      null,\n      { issues: parsed.error.issues },\n    );\n  }\n  return parsed.data;\n}\n\n/**\n * Poll {@link getDerivativeStatus} until the derived scope has an answer or\n * has stopped trying to get one.\n *\n * @remarks\n * Returns as soon as {@link isDerivativeStatusSettled} holds: `ready`, or\n * `failed` with no retry pending. A failure the server will retry is not a\n * result, so the wait continues through it — on the server's own cadence,\n * because `retryAfterSeconds` is when the next compute actually happens and\n * polling faster only spends requests. A failed status is returned, not\n * thrown: the reader branches on `errorCode`.\n *\n * @returns The settled status.\n * @throws {DerivativeQuestionTimeoutError} When the budget ran out first.\n *   The question keeps computing on the server; call again.\n *\n * @example\n * ```typescript\n * const status = await waitForDerivativeStatus({\n *   personalServerUrl,\n *   derivedScope: \"coach.weekly\",\n *   grantId,\n *   signer,\n *   timeoutMs: 60_000,\n * });\n * if (status.status !== \"ready\") console.log(status.errorCode);\n * ```\n */\nexport async function waitForDerivativeStatus(\n  params: WaitForDerivativeStatusParams,\n): Promise<DerivativeStatus> {\n  const timeoutMs = Math.max(\n    0,\n    params.timeoutMs ?? DEFAULT_DERIVATIVE_STATUS_TIMEOUT_MS,\n  );\n  const pollIntervalMs = Math.max(\n    0,\n    params.pollIntervalMs ?? DEFAULT_DERIVATIVE_STATUS_POLL_INTERVAL_MS,\n  );\n  const deadline = Date.now() + timeoutMs;\n  for (;;) {\n    if (params.signal?.aborted) throw abortError(params.signal);\n    const latest = await getDerivativeStatus(params);\n    if (isDerivativeStatusSettled(latest)) return latest;\n    const remaining = deadline - Date.now();\n    if (remaining <= 0) throw timeoutError(latest, timeoutMs);\n    // The server's scheduled retry decides the cadence outright, in both\n    // directions: a caller's longer pollIntervalMs would sit past a retry\n    // that has already produced an answer, and a shorter one would ask\n    // before anything can have changed. pollIntervalMs is the cadence for\n    // the case where the server named none.\n    const waitMs =\n      latest.retryAfterSeconds === null\n        ? pollIntervalMs\n        : latest.retryAfterSeconds * 1000;\n    if (waitMs > remaining) {\n      // The budget cannot cover the next cadence, so the poll after this\n      // sleep would land before anything could have changed. Give up now\n      // instead of spending a request to say the same thing.\n      throw timeoutError(latest, timeoutMs);\n    }\n    await sleep(waitMs, params.signal);\n  }\n}\n\n/** Statuses that mean a compute is in flight. @see {@link QuestionStatus} */\nexport type PendingDerivativeStatus = Extract<\n  QuestionStatus,\n  \"pending\" | \"stale\"\n>;\n"],"mappings":"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","names":["import_derivative_questions"]}