{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/devtools/server/actionDomainScope.ts"],"sourcesContent":["import type { IDevtoolsLogRecord, IDevtoolsSnapshotScope } from \"@nice-code/devtools-core\";\nimport type {\n  IDevtoolsActionEntry,\n  IDevtoolsObservableDomain,\n  TDevtoolsActionStatus,\n} from \"../core/ActionDevtools.types\";\nimport { ActionDevtoolsCore } from \"../core/ActionDevtoolsCore\";\n\nexport interface IActionDomainScopeOptions {\n  /** Max action entries to retain in the server-side collector. Default 200. */\n  maxEntries?: number;\n  /**\n   * Whether action **payload contents** (`input` / `output` / `error` / `abortReason` / progress\n   * values) leave this process — both in the `console` sink's log lines and in the snapshot streamed\n   * to an attached devtools window.\n   *\n   * **Default `false`: metrics and lifecycle metadata only.** A server scope can be observed over a\n   * socket (a Durable Object's dev endpoint, a relay), and a request body is the last thing that\n   * should ride that channel by accident. Action id, domain, status, timings, durations and error\n   * *shape* still stream — enough to answer \"what ran, how long, did it fail\". Turn it on\n   * deliberately, in dev, when you need to see the values.\n   *\n   * (Before this existed the console sink honoured a `logPayloads` flag while the snapshot streamed\n   * payloads regardless — the gap this closes.)\n   */\n  logPayloads?: boolean;\n}\n\n/**\n * Strip payload contents from an entry, keeping every metric and lifecycle fact. Deliberately\n * allow-list shaped: a field added to `IDevtoolsActionEntry` later is NOT silently published.\n */\nfunction redactEntry(entry: IDevtoolsActionEntry): IDevtoolsActionEntry {\n  return {\n    cuid: entry.cuid,\n    actionId: entry.actionId,\n    domain: entry.domain,\n    allDomains: entry.allDomains,\n    status: entry.status,\n    startTime: entry.startTime,\n    ...(entry.endTime != null ? { endTime: entry.endTime } : {}),\n    // The *shape* of the traffic survives; the values do not. Hashes are already content-free.\n    input: undefined,\n    ...(entry.inputHash != null ? { inputHash: entry.inputHash } : {}),\n    ...(entry.outputHash != null ? { outputHash: entry.outputHash } : {}),\n    // An error's presence and expectedness are lifecycle facts; its payload may carry user data.\n    ...(entry.error != null ? { error: \"[redacted]\" } : {}),\n    ...(entry.expected != null ? { expected: entry.expected } : {}),\n    ...(entry.abortReason != null ? { abortReason: \"[redacted]\" } : {}),\n    ...(entry.errorStack != null ? { errorStack: entry.errorStack } : {}),\n    ...(entry.callSite != null ? { callSite: entry.callSite } : {}),\n    ...(entry.parentCuid != null ? { parentCuid: entry.parentCuid } : {}),\n    ...(entry.reliability != null ? { reliability: entry.reliability } : {}),\n    // Progress *count* is a metric; each update's payload is not.\n    progressUpdates: [],\n    meta: entry.meta,\n  };\n}\n\n/**\n * The snapshot an action domain contributes to a server devtools host: the recent\n * server-side actions (same normalized entries the frontend panel renders), so a\n * devtools window can show what a backend actually ran. Cloneable data.\n */\nexport interface IActionServerScopeSnapshot {\n  kind: \"action\";\n  domain: string;\n  entries: readonly IDevtoolsActionEntry[];\n}\n\n/**\n * Plug an action domain into a server devtools host (devtools-revamp Wave D). Builds\n * on the same React-free `ActionDevtoolsCore` collector the frontend uses, so the\n * host can both stream the server's action list to a window (`relay` sink) and log\n * each action's lifecycle (`console` sink) — folding the old `ActionServerDevtools`\n * logger in as one sink of the unified host.\n *\n * Takes a **root** action domain — the same thing the frontend's `domains:` option takes. An action\n * whose root is not attached here simply never surfaces.\n *\n * ```ts\n * import { createServerDevtoolsHost } from \"@nice-code/devtools-core\";\n * import { actionDomainScope } from \"@nice-code/action/devtools/server\";\n *\n * createServerDevtoolsHost({ name: \"api\", sinks: [\"console\"] })\n *   .contribute(actionDomainScope(act_root))\n *   .start();\n * ```\n *\n * Most backends never write this: `createNiceServerDevtools({ domains: [act_root] })` builds it.\n */\nexport function actionDomainScope(\n  domain: IDevtoolsObservableDomain,\n  options: IActionDomainScopeOptions = {},\n): IDevtoolsSnapshotScope {\n  const logPayloads = options.logPayloads ?? false;\n  const core = new ActionDevtoolsCore({ maxEntries: options.maxEntries ?? 200 });\n  // Collect for the process lifetime — the console sink needs entries even with no\n  // window attached. The host is dev-gated, so the ≤maxEntries buffer is bounded.\n  core.attachToDomain(domain);\n\n  // The console sink's cursor: the last status we logged per cuid.\n  const loggedStatus = new Map<string, TDevtoolsActionStatus>();\n\n  return {\n    scope: `server:action:${domain.domain}`,\n    getSnapshot(): IActionServerScopeSnapshot {\n      const entries = core.getEntries();\n      // The gate applies HERE too, not just to the console sink: this snapshot is what crosses the\n      // wire to an attached window.\n      return {\n        kind: \"action\",\n        domain: domain.domain,\n        entries: logPayloads ? entries : entries.map(redactEntry),\n      };\n    },\n    subscribe(onChange): () => void {\n      return core.subscribe(() => onChange());\n    },\n    applyCommand(command): void {\n      const type = (command as { type?: string }).type;\n      if (type === \"clear\") core.clear();\n      else if (type === \"togglePaused\" || type === \"pause\") core.togglePaused();\n    },\n    logSince(): IDevtoolsLogRecord[] {\n      const records: IDevtoolsLogRecord[] = [];\n      const entries = core.getEntries();\n      const seen = new Set<string>();\n      // Oldest-first so log lines read in the order the actions progressed.\n      for (let i = entries.length - 1; i >= 0; i--) {\n        const entry = entries[i];\n        if (entry == null) continue;\n        seen.add(entry.cuid);\n        if (loggedStatus.get(entry.cuid) === entry.status) continue;\n        loggedStatus.set(entry.cuid, entry.status);\n        records.push(recordForEntry(entry, logPayloads));\n      }\n      // Drop cursors for entries that have since been evicted from the ring.\n      for (const cuid of [...loggedStatus.keys()]) {\n        if (!seen.has(cuid)) loggedStatus.delete(cuid);\n      }\n      return records;\n    },\n  };\n}\n\nconst PRETTY_PREFIX: Record<TDevtoolsActionStatus, string> = {\n  running: \"[nice-action] >\",\n  success: \"[nice-action] ✓\",\n  \"action-error\": \"[nice-action] ✗\",\n  failed: \"[nice-action] ✗\",\n  aborted: \"[nice-action] ~\",\n};\n\nfunction recordForEntry(entry: IDevtoolsActionEntry, logPayloads: boolean): IDevtoolsLogRecord {\n  const path = [...entry.allDomains, entry.actionId].join(\".\");\n  const duration = entry.endTime != null ? entry.endTime - entry.startTime : undefined;\n\n  const fields: Record<string, unknown> = { action: path, cuid: entry.cuid, status: entry.status };\n  if (duration != null) fields[\"duration\"] = `${duration}ms`;\n  if (entry.status === \"running\") {\n    if (logPayloads) fields[\"input\"] = entry.input;\n  } else if (entry.status === \"success\") {\n    if (logPayloads) fields[\"output\"] = entry.output;\n  } else if (entry.status === \"action-error\" || entry.status === \"failed\") {\n    fields[\"error\"] = entry.error;\n  } else if (entry.status === \"aborted\") {\n    if (entry.abortReason != null) fields[\"reason\"] = entry.abortReason;\n  }\n\n  const durationSuffix = duration != null ? `  ${duration}ms` : \"\";\n  const payloadSuffix = payloadLine(entry, logPayloads);\n  return {\n    event: `action-${entry.status}`,\n    label: path,\n    message: `${PRETTY_PREFIX[entry.status]} ${path}  cuid=${entry.cuid}${durationSuffix}${payloadSuffix}`,\n    fields,\n  };\n}\n\nfunction payloadLine(entry: IDevtoolsActionEntry, logPayloads: boolean): string {\n  if (entry.status === \"action-error\" || entry.status === \"failed\") {\n    return `  error=${safeStringify(entry.error)}`;\n  }\n  if (entry.status === \"aborted\" && entry.abortReason != null) {\n    return `  reason=${safeStringify(entry.abortReason)}`;\n  }\n  if (!logPayloads) return \"\";\n  if (entry.status === \"running\") return `  input=${safeStringify(entry.input)}`;\n  if (entry.status === \"success\") return `  output=${safeStringify(entry.output)}`;\n  return \"\";\n}\n\nfunction safeStringify(value: unknown): string {\n  if (value === undefined) return \"undefined\";\n  if (value === null) return \"null\";\n  if (typeof value === \"string\") return `\"${value}\"`;\n  try {\n    return JSON.stringify(value);\n  } catch {\n    return String(value);\n  }\n}\n"],"mappings":"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"}