{"version":3,"file":"action-DcUMl6uX.cjs","names":[],"sources":["../../src/action/define.ts","../../src/action/index.ts"],"sourcesContent":["// --- defineAction(): typed server action definition (plan §9.2) ---\n//\n// Provides a typed wrapper for server actions with:\n// - optional input schema validation (without requiring Zod)\n// - AbortSignal propagation from the request\n// - idempotency key support\n// - concurrency modes (latest, queue, parallel)\n//\n// Legacy exported async functions continue to work: `defineAction()` is an\n// opt-in upgrade path, not a replacement.\n\nimport type { ActionFailure } from \"../errors.js\";\n\n// Capture AbortController at module load time — tests may temporarily remove\n// it from the global scope.\nconst AbortControllerCtor = globalThis.AbortController;\n\n/** Concurrency mode for actions called multiple times in rapid succession. */\nexport type ActionConcurrencyMode = \"latest\" | \"queue\" | \"parallel\";\n\n/** Options for defining a typed server action. */\nexport interface DefineActionOptions<TInput> {\n  /**\n   * Optional input validator. Can be a Zod schema, a plain function, or any\n   * object with a `.parse()` method. If validation fails, the action returns\n   * a 400 ActionFailure with the validation error.\n   */\n  input?: ActionInputValidator<TInput>;\n  /** Concurrency mode when the same action is called multiple times. */\n  concurrency?: ActionConcurrencyMode;\n  /** Whether the action is idempotent (safe to retry). */\n  idempotent?: boolean;\n  /** Tags to invalidate from the cache after a successful action (§9.4). */\n  invalidateTags?: string[];\n  /** Paths to invalidate from the cache after a successful action (§9.4). */\n  invalidatePaths?: string[];\n}\n\n/** A validator that has a `.parse()` method (Zod-compatible) or is a function. */\nexport interface ActionInputValidator<T> {\n  parse(input: unknown): T;\n}\n\n/** Context passed to a defined action. */\nexport interface ActionContext {\n  /** The original Web Request. */\n  request: Request;\n  /** AbortSignal from the request — aborts if the client disconnects. */\n  signal: AbortSignal;\n  /** Idempotency key from the request header, if present. */\n  idempotencyKey?: string;\n  /** Route params (for page-scoped actions). */\n  params: Record<string, string | string[]>;\n  /** Per-request locals (populated by middleware). */\n  locals: Record<string, unknown>;\n}\n\n/** A defined action function. */\nexport type DefinedActionFn<TInput, TOutput> = (\n  input: TInput,\n  ctx: ActionContext,\n) => Promise<TOutput | ActionFailure<TOutput>>;\n\n/** The return type of defineAction(): a callable with metadata. */\nexport interface DefinedAction<TInput, TOutput> {\n  (input: TInput, ctx: ActionContext): Promise<TOutput | ActionFailure<TOutput>>;\n  /** Metadata for the action (used by the runtime/manifest). */\n  __nixAction: {\n    name: string;\n    concurrency: ActionConcurrencyMode;\n    idempotent: boolean;\n    invalidateTags: readonly string[];\n    invalidatePaths: readonly string[];\n  };\n}\n\n/**\n * Defines a typed server action with validation, abort support, and cache\n * invalidation metadata.\n *\n * ```ts\n * import { defineAction, fail } from \"@deijose/nix-js-kit/action\";\n *\n * export const submitContact = defineAction({\n *   input: { parse: (v) => v as { name: string; email: string } },\n *   invalidateTags: [\"contacts\"],\n * }, async (input, ctx) => {\n *   if (!input.email.includes(\"@\")) return fail(400, { email: \"Invalid\" });\n *   await saveContact(input);\n *   return { success: true };\n * });\n * ```\n *\n * Legacy exported async functions (without `defineAction`) continue to work\n * as before — this is an opt-in upgrade.\n */\nexport function defineAction<TInput = unknown, TOutput = unknown>(\n  options: DefineActionOptions<TInput>,\n  handler: DefinedActionFn<TInput, TOutput>,\n): DefinedAction<TInput, TOutput> {\n  const concurrency = options.concurrency ?? \"latest\";\n  const idempotent = options.idempotent ?? false;\n  const invalidateTags = options.invalidateTags ?? [];\n  const invalidatePaths = options.invalidatePaths ?? [];\n\n  // Track in-flight calls for concurrency control.\n  let latestController: InstanceType<typeof AbortControllerCtor> | null = null;\n  const queue: Array<() => void> = [];\n  let running = 0;\n\n  async function run(\n    input: TInput,\n    ctx: ActionContext,\n  ): Promise<TOutput | ActionFailure<TOutput>> {\n    // Validate input if a validator is configured.\n    if (options.input) {\n      try {\n        const validated = options.input.parse(input);\n        input = validated;\n      } catch (err) {\n        const message = err instanceof Error ? err.message : String(err);\n        const { fail } = await import(\"../errors.js\");\n        return fail(400, { validation: message }) as ActionFailure<TOutput>;\n      }\n    }\n\n    return handler(input, ctx);\n  }\n\n  const fn = async (input: TInput, ctx: ActionContext): Promise<TOutput | ActionFailure<TOutput>> => {\n    if (concurrency === \"latest\") {\n      // Cancel any previous in-flight call.\n      if (latestController) latestController.abort();\n      latestController = new AbortControllerCtor();\n      // Combine the request signal with our cancellation signal.\n      const combinedSignal = combineSignals(ctx.signal, latestController.signal);\n      return run(input, { ...ctx, signal: combinedSignal });\n    }\n\n    if (concurrency === \"queue\") {\n      // Wait for previous calls to finish.\n      if (running > 0) {\n        await new Promise<void>((resolve) => queue.push(resolve));\n      }\n      running++;\n      try {\n        return await run(input, ctx);\n      } finally {\n        running--;\n        const next = queue.shift();\n        if (next) next();\n      }\n    }\n\n    // parallel: just run it.\n    return run(input, ctx);\n  };\n\n  // Attach metadata.\n  (fn as DefinedAction<TInput, TOutput>).__nixAction = {\n    name: handler.name || \"anonymous\",\n    concurrency,\n    idempotent,\n    invalidateTags,\n    invalidatePaths,\n  };\n\n  return fn as DefinedAction<TInput, TOutput>;\n}\n\n/** Combines two AbortSignals into one that aborts when either does. */\nfunction combineSignals(...signals: (AbortSignal | undefined)[]): AbortSignal {\n  const controller = new AbortControllerCtor();\n  for (const signal of signals) {\n    if (!signal) continue;\n    if (signal.aborted) {\n      controller.abort();\n      break;\n    }\n    signal.addEventListener(\"abort\", () => controller.abort(), { once: true });\n  }\n  return controller.signal;\n}\n","/**\n * Client-side helpers for invoking server actions.\n *\n * Server actions are defined in `page.action.ts` files next to `page.ts`.\n * They export async functions that run on the server. On the client, call them\n * by name using `callAction` or the higher-level `nixJsAction` helper:\n *\n * ```ts\n * import { callAction } from \"@deijose/nix-js-kit/action\";\n *\n * const result = await callAction(\"submitContact\", { name: \"Ada\" }, { page: \"/contact\" });\n * ```\n *\n * ```ts\n * import { nixJsAction } from \"@deijose/nix-js-kit/action\";\n *\n * const contact = nixJsAction(\"submitContact\", { page: \"/contact\" });\n * await contact.submit({ name: \"Ada\" });\n * console.log(contact.data.value, contact.error.value, contact.pending.value);\n * ```\n */\n\nimport { signal } from \"@deijose/nix-js\";\nimport { ActionFailure, RedirectResponse } from \"../errors.js\";\n\ninterface ActionFailurePayload {\n  __nix_js_action_failure?: boolean;\n  status?: number;\n  data?: unknown;\n}\n\ninterface RedirectPayload {\n  __nix_js_action_redirect?: boolean;\n  status?: number;\n  location?: string;\n}\n\nfunction isActionFailurePayload(value: unknown): value is ActionFailurePayload & { __nix_js_action_failure: true } {\n  return typeof value === \"object\" && value !== null && (value as Record<string, unknown>).__nix_js_action_failure === true;\n}\n\nfunction isRedirectPayload(value: unknown): value is RedirectPayload & { __nix_js_action_redirect: true } {\n  return typeof value === \"object\" && value !== null && (value as Record<string, unknown>).__nix_js_action_redirect === true;\n}\n\nexport interface ActionRequest {\n  name: string;\n  page?: string;\n  args: unknown[];\n}\n\nexport interface CallActionOptions {\n  /** Page URL path that scopes the action, e.g. `/contact`. */\n  page?: string;\n}\n\n/**\n * Call a server action by name.\n *\n * The request is sent as a POST to `/__nix-js/actions` with the action name,\n * optional page scope, and serialized arguments. The server executes the\n * matching exported function from the scanned `page.action.ts` modules and\n * returns its JSON result.\n */\nexport async function callAction<T = unknown>(\n  name: string,\n  args: unknown = [],\n  options: CallActionOptions = {},\n): Promise<T | ActionFailure<T> | RedirectResponse> {\n  const argsArray = Array.isArray(args) ? args : [args];\n  const res = await fetch(\"/__nix-js/actions\", {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n      Accept: \"application/json\",\n    },\n    body: JSON.stringify({ name, page: options.page, args: argsArray } as ActionRequest),\n  });\n\n  const text = await res.text();\n  if (!res.ok) {\n    let payload: unknown;\n    try {\n      payload = JSON.parse(text);\n    } catch {\n      // not JSON, treat as plain error\n    }\n    if (isActionFailurePayload(payload) && payload.status !== undefined) {\n      return new ActionFailure(payload.status, payload.data as T);\n    }\n    throw new Error(`Action \"${name}\" failed: ${text}`);\n  }\n\n  const payload: unknown = JSON.parse(text);\n  if (isRedirectPayload(payload) && payload.status !== undefined && payload.location !== undefined) {\n    return new RedirectResponse(payload.status, payload.location);\n  }\n\n  return payload as T;\n}\n\nexport interface NixJsAction<TInput = unknown, TOutput = unknown> {\n  /** Submit the action with the given input. */\n  submit(input: TInput): Promise<TOutput | ActionFailure<TOutput> | RedirectResponse>;\n  /** Signal that is true while the action is running. */\n  pending: { value: boolean };\n  /** Signal with the last successful result, action failure, redirect, or null. */\n  data: { value: TOutput | ActionFailure<TOutput> | RedirectResponse | null };\n  /** Signal with the last error, or null. */\n  error: { value: Error | null };\n}\n\n/**\n * Create a reactive handle for a server action.\n *\n * Returns a `submit` function and signals for `pending`, `data`, and `error`.\n * Useful for wiring actions to forms and islands without manual signal boilerplate.\n */\nexport function nixJsAction<TInput = unknown, TOutput = unknown>(\n  name: string,\n  options: CallActionOptions = {},\n): NixJsAction<TInput, TOutput> {\n  const pending = signal(false);\n  const error = signal<Error | null>(null);\n  const data = signal<TOutput | ActionFailure<TOutput> | RedirectResponse | null>(null);\n\n  async function submit(input: TInput): Promise<TOutput | ActionFailure<TOutput> | RedirectResponse> {\n    pending.value = true;\n    error.value = null;\n    try {\n      const result = await callAction<TOutput>(name, input, options);\n      data.value = result;\n      return result;\n    } catch (err) {\n      error.value = err instanceof Error ? err : new Error(String(err));\n      throw err;\n    } finally {\n      pending.value = false;\n    }\n  }\n\n  return {\n    submit,\n    pending,\n    data,\n    error,\n  };\n}\n\n// Server-side: defineAction() for typed actions with validation/abort/cache.\nexport {\n  defineAction,\n  type DefineActionOptions,\n  type DefinedAction,\n  type DefinedActionFn,\n  type ActionContext,\n  type ActionInputValidator,\n  type ActionConcurrencyMode,\n} from \"./define.js\";\n"],"mappings":"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"}