@superatomai/sdk-node 0.0.12-s → 0.0.13-s
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +942 -942
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -0
- package/dist/index.mjs.map +1 -1
- package/dist/userResponse/scripts/script-bootstrap.js.map +1 -1
- package/dist/userResponse/scripts/script-bootstrap.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/userResponse/scripts/script-bootstrap.ts","../../../src/userResponse/scripts/script-ipc.ts"],"sourcesContent":["/**\n * script-bootstrap.ts — Child-side entrypoint for subprocess script execution.\n *\n * Runs as a separate process spawned by ScriptRunner via tsx. Reads the\n * target user-script path from argv[2], listens for INIT on stdin, builds\n * the ctx object, dynamic-imports the user script's `getData` export,\n * and reports the result (or error) back via stdout.\n *\n * See backend/docs/SCRIPT-FLOW-IMPLEMENTATION.md § IPC Bridge.\n */\n\nimport { pathToFileURL } from 'url';\nimport {\n\tencodeMessage,\n\tLineSplitter,\n\tshortId,\n\ttype ChildToParentMessage,\n\ttype ParentToChildMessage,\n\ttype ExecutedQuery,\n\ttype QueryResultMessage,\n\ttype QueryErrorMessage,\n} from './script-ipc';\n\nconst SCRIPT_ARG = process.argv[2];\nif (!SCRIPT_ARG) {\n\tsendError('Bootstrap was invoked without a script path.', 'compile');\n\tprocess.exit(1);\n}\n\n/**\n * Pending queries await a QUERY_RESULT or QUERY_ERROR from the parent.\n * Keyed by the short uuid we generated when sending the QUERY message.\n */\n/**\n * One pending entry per in-flight ctx.query or ctx.runTool. The resolve\n * payload differs by call kind ({data, count, metadata} for query,\n * {result} for runTool) — each caller's resolve handler unwraps the shape\n * it expects, so the map uses `any` rather than a discriminated union to\n * keep the call sites simple.\n */\nconst pending = new Map<string, {\n\tresolve: (value: any) => void;\n\treject: (err: Error) => void;\n\ttoolId: string;\n\tsql: string;\n\tstartedAt: number;\n}>();\n\n/** Completed queries — reported to the parent as part of RESULT */\nconst executedQueries: ExecutedQuery[] = [];\n\n/** Resolvers for the INIT handshake — getData waits for this */\nlet initResolve: ((init: { params: Record<string, any>; now: Date }) => void) | null = null;\nconst initPromise = new Promise<{ params: Record<string, any>; now: Date }>((resolve) => {\n\tinitResolve = resolve;\n});\n\n/**\n * Source name lookup is not available in the child (the parent holds the\n * ExternalTool list). We use toolId as sourceName and let the parent fill\n * it in when building the final ScriptResult, if needed.\n */\nfunction buildCtx(init: { params: Record<string, any>; now: Date }) {\n\treturn {\n\t\tnow: init.now,\n\n\t\tasync query(toolId: string, sql: string): Promise<{ data: any[]; count: number }> {\n\t\t\tconst id = shortId();\n\t\t\tconst startedAt = Date.now();\n\t\t\tconst promise = new Promise<{ data: any[]; count: number; metadata?: any }>((resolve, reject) => {\n\t\t\t\tpending.set(id, { resolve, reject, toolId, sql, startedAt });\n\t\t\t});\n\n\t\t\tsend({ type: 'query', id, toolId, sql });\n\n\t\t\tconst result = await promise;\n\t\t\tconst executionTimeMs = Date.now() - startedAt;\n\n\t\t\texecutedQueries.push({\n\t\t\t\tsourceId: toolId,\n\t\t\t\tsourceName: toolId, // parent overwrites with the real name\n\t\t\t\tsql,\n\t\t\t\tdata: result.data,\n\t\t\t\tcount: result.count,\n\t\t\t\ttotalCount: result.metadata?.totalCount,\n\t\t\t\texecutionTimeMs,\n\t\t\t});\n\n\t\t\treturn { data: result.data, count: result.count };\n\t\t},\n\n\t\t/**\n\t\t * Call a tool with structured params (typically a direct tool whose\n\t\t * fn(params) does not expect a SQL string). Returns whatever the tool\n\t\t * returned — shape is up to the tool. The parent verifies toolId\n\t\t * against the authorized externalTools list before executing.\n\t\t *\n\t\t * Use this when you need a pre-built business-logic tool (e.g.,\n\t\t * \"Calculate Shelf Life\") that takes structured input rather than SQL.\n\t\t * For SQL source tools, keep using ctx.query.\n\t\t *\n\t\t * runTool results do NOT auto-flow into executedQueries. If you want\n\t\t * the result visualised, register it via ctx.emit() after extracting\n\t\t * the array you want to chart.\n\t\t */\n\t\tasync runTool<T = any>(toolId: string, params: Record<string, any> = {}): Promise<T> {\n\t\t\tconst id = shortId();\n\t\t\tconst promise = new Promise<any>((resolveResult, rejectResult) => {\n\t\t\t\tpending.set(id, {\n\t\t\t\t\tresolve: (payload: any) => resolveResult(payload?.result ?? payload),\n\t\t\t\t\treject: rejectResult,\n\t\t\t\t\ttoolId,\n\t\t\t\t\tsql: `-- runTool: ${toolId}`,\n\t\t\t\t\tstartedAt: Date.now(),\n\t\t\t\t});\n\t\t\t});\n\t\t\tsend({ type: 'run_tool', id, toolId, params });\n\t\t\treturn promise;\n\t\t},\n\n\t\t/**\n\t\t * Register a computed dataset. Flows into executedQueries alongside\n\t\t * real SQL query results so component generation can render post-SQL\n\t\t * transforms (weighted averages, clustering, joins) as charts.\n\t\t */\n\t\temit(name: string, rows: any[], meta?: { description?: string }): void {\n\t\t\texecutedQueries.push({\n\t\t\t\tsourceId: `computed:${name}`,\n\t\t\t\tsourceName: meta?.description || name,\n\t\t\t\tsql: `-- computed dataset: ${name}`,\n\t\t\t\tdata: Array.isArray(rows) ? rows : [],\n\t\t\t\tcount: Array.isArray(rows) ? rows.length : 0,\n\t\t\t\texecutionTimeMs: 0,\n\t\t\t\tvirtual: true,\n\t\t\t});\n\t\t},\n\n\t\t/**\n\t\t * Forward a short progress message to the parent's StreamBuffer so the UI\n\t\t * continues to see per-script feedback during execution.\n\t\t */\n\t\tlog(chunk: string): void {\n\t\t\tsend({ type: 'stream', chunk });\n\t\t},\n\t};\n}\n\nfunction send(msg: ChildToParentMessage): void {\n\tprocess.stdout.write(encodeMessage(msg));\n}\n\nfunction sendError(message: string, phase: 'compile' | 'runtime', stack?: string): void {\n\tsend({ type: 'error', message, stack, phase });\n}\n\n/**\n * Reject every in-flight ctx.query / ctx.runTool with the same reason. Used when\n * the IPC channel goes away (stdin end/error) so a pending query fails fast\n * instead of hanging until the parent's wall-clock SIGKILL. Idempotent — a\n * second call finds the map already drained.\n */\nfunction rejectAllPending(reason: string): void {\n\tfor (const [id, entry] of Array.from(pending.entries())) {\n\t\tpending.delete(id);\n\t\tentry.reject(new Error(reason));\n\t}\n}\n\n// ============================================\n// stdin reader — parse NDJSON messages from the parent\n// ============================================\n\nconst splitter = new LineSplitter();\n\nprocess.stdin.setEncoding('utf-8');\nprocess.stdin.on('data', (chunk: string) => {\n\tlet lines: string[];\n\ttry {\n\t\tlines = splitter.push(chunk);\n\t} catch (err) {\n\t\t// Oversized parent message (#9) — abort cleanly rather than buffer to OOM.\n\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\trejectAllPending(msg);\n\t\tsendError(msg, 'runtime');\n\t\tprocess.exit(1);\n\t}\n\tfor (const line of lines) {\n\t\tlet msg: ParentToChildMessage;\n\t\ttry {\n\t\t\tmsg = JSON.parse(line);\n\t\t} catch {\n\t\t\tcontinue; // ignore malformed input\n\t\t}\n\t\thandleMessage(msg);\n\t}\n});\n\nprocess.stdin.on('end', () => {\n\t// Parent closed our stdin — no further query replies can arrive. Reject any\n\t// in-flight queries now so getData fails fast (and we report ERROR) instead\n\t// of awaiting a reply that will never come, until the parent's wall-clock kill.\n\trejectAllPending('Parent closed the IPC channel (stdin end) before a query completed.');\n});\n\nprocess.stdin.on('error', (err: Error) => {\n\trejectAllPending(`IPC channel error before a query completed: ${err.message}`);\n});\n\nfunction handleMessage(msg: ParentToChildMessage): void {\n\tswitch (msg.type) {\n\t\tcase 'init': {\n\t\t\tif (initResolve) {\n\t\t\t\tconst now = new Date(msg.now || new Date().toISOString());\n\t\t\t\tconst params = restoreParamTypes(msg.params || {}, msg.paramSchema);\n\t\t\t\tinitResolve({ params, now });\n\t\t\t\tinitResolve = null;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase 'query_result': {\n\t\t\tconst entry = pending.get(msg.id);\n\t\t\tif (entry) {\n\t\t\t\tpending.delete(msg.id);\n\t\t\t\tentry.resolve({ data: msg.data, count: msg.count, metadata: msg.metadata });\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase 'run_tool_result': {\n\t\t\tconst entry = pending.get(msg.id);\n\t\t\tif (entry) {\n\t\t\t\tpending.delete(msg.id);\n\t\t\t\t// runTool's resolve unwraps payload.result — wrap here so the\n\t\t\t\t// same pending entry shape works for both query and runTool.\n\t\t\t\tentry.resolve({ result: msg.result });\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase 'query_error': {\n\t\t\tconst entry = pending.get(msg.id);\n\t\t\tif (entry) {\n\t\t\t\tpending.delete(msg.id);\n\t\t\t\tentry.reject(new Error(msg.error));\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\n// ============================================\n// Main — import the user script and run getData\n// ============================================\n\n(async () => {\n\tlet getData: (ctx: any, params: any) => Promise<any>;\n\n\ttry {\n\t\t// Dynamic import — tsx transpiles the user .ts file on the fly\n\t\tconst mod = await import(pathToFileURL(SCRIPT_ARG).href);\n\t\tgetData = mod.getData ?? mod.default;\n\t\tif (typeof getData !== 'function') {\n\t\t\tthrow new Error(\n\t\t\t\t`Script at ${SCRIPT_ARG} does not export getData — expected \\`export async function getData(ctx, params)\\``,\n\t\t\t);\n\t\t}\n\t} catch (err) {\n\t\tconst e = err as Error;\n\t\tsendError(e.message || String(err), 'compile', e.stack);\n\t\tprocess.exit(1);\n\t\treturn;\n\t}\n\n\tconst init = await initPromise;\n\tconst ctx = buildCtx(init);\n\tconst startedAt = Date.now();\n\n\ttry {\n\t\tconst raw = await getData(ctx, init.params);\n\t\tconst executionTimeMs = Date.now() - startedAt;\n\n\t\tconst data = normalizeResultData(raw);\n\t\tconst count = typeof raw?.count === 'number' ? raw.count : data.length;\n\t\tconst sampleRows = data.slice(0, 5);\n\n\t\tconst result: ChildToParentMessage = {\n\t\t\ttype: 'result',\n\t\t\tdata,\n\t\t\tcount,\n\t\t\texecutedQueries,\n\t\t\texecutionTimeMs,\n\t\t\tsampleRows,\n\t\t};\n\t\tsend(result);\n\t\t// Give stdout a tick to flush before we exit\n\t\tprocess.stdout.once('drain', () => process.exit(0));\n\t\tsetImmediate(() => process.exit(0));\n\t} catch (err) {\n\t\tconst e = err as Error;\n\t\tsendError(e.message || String(err), 'runtime', e.stack);\n\t\tprocess.exit(1);\n\t}\n})();\n\n/**\n * Normalize whatever getData returned into an array. Mirrors the old\n * ScriptRunner semantics: direct array, { data: [] }, or { data: {...} }\n * wrapped as single-element array.\n */\nfunction normalizeResultData(raw: any): any[] {\n\tif (!raw) return [];\n\tif (Array.isArray(raw)) return raw;\n\tif (Array.isArray(raw.data)) return raw.data;\n\tif (raw.data && typeof raw.data === 'object') return [raw.data];\n\treturn [];\n}\n\n/**\n * Restore typed values that JSON stripped during IPC transport. Date and\n * date_range params arrive as ISO strings; re-wrap them so scripts can call\n * `.toISOString()` and other Date methods the same way they would in-process.\n */\nfunction restoreParamTypes(\n\tparams: Record<string, any>,\n\tschema?: Record<string, 'string' | 'number' | 'date' | 'date_range' | 'enum' | 'boolean'>,\n): Record<string, any> {\n\tif (!schema) return params;\n\tconst out = { ...params };\n\tfor (const [name, type] of Object.entries(schema)) {\n\t\tconst v = out[name];\n\t\tif (v === undefined || v === null) continue;\n\t\tif (type === 'date' && typeof v === 'string') {\n\t\t\tconst d = new Date(v);\n\t\t\tif (!isNaN(d.getTime())) out[name] = d;\n\t\t} else if (type === 'date_range' && typeof v === 'object') {\n\t\t\tconst from = v.from;\n\t\t\tconst to = v.to;\n\t\t\tout[name] = {\n\t\t\t\tfrom: typeof from === 'string' ? new Date(from) : from,\n\t\t\t\tto: typeof to === 'string' ? new Date(to) : to,\n\t\t\t};\n\t\t}\n\t}\n\treturn out;\n}\n","/**\n * script-ipc.ts — Protocol definitions for parent/child IPC.\n *\n * Transport: newline-delimited JSON over the child's stdin/stdout.\n * stderr is passed through for uncaught exceptions and compile errors.\n *\n * See backend/docs/SCRIPT-FLOW-IMPLEMENTATION.md § IPC Bridge.\n */\n\n// ============================================\n// Parent → Child\n// ============================================\n\nexport interface InitMessage {\n\ttype: 'init';\n\t/**\n\t * Params passed to getData(). Date/date_range values cross the IPC boundary\n\t * as ISO strings and are restored to Date objects in the child via paramSchema.\n\t */\n\tparams: Record<string, any>;\n\t/** Frozen timestamp for ctx.now — ISO string */\n\tnow: string;\n\t/**\n\t * Maps each parameter name to its declared type so the child can restore\n\t * Date objects (lost during JSON serialization).\n\t */\n\tparamSchema?: Record<string, 'string' | 'number' | 'date' | 'date_range' | 'enum' | 'boolean'>;\n}\n\nexport interface QueryResultMessage {\n\ttype: 'query_result';\n\t/** Matches the `id` from the child's QUERY message */\n\tid: string;\n\tdata: any[];\n\tcount: number;\n\tmetadata?: {\n\t\ttotalCount?: number;\n\t\texecutionTimeMs?: number;\n\t};\n}\n\nexport interface QueryErrorMessage {\n\ttype: 'query_error';\n\tid: string;\n\terror: string;\n}\n\n/**\n * Response to a `run_tool` request — carries whatever the direct-tool's fn()\n * returned. Shape is arbitrary; the script is expected to know what its chosen\n * tool yields and unwrap accordingly.\n */\nexport interface RunToolResultMessage {\n\ttype: 'run_tool_result';\n\tid: string;\n\tresult: any;\n}\n\nexport type ParentToChildMessage =\n\t| InitMessage\n\t| QueryResultMessage\n\t| QueryErrorMessage\n\t| RunToolResultMessage;\n\n// ============================================\n// Child → Parent\n// ============================================\n\nexport interface QueryMessage {\n\ttype: 'query';\n\t/** Short uuid — parent uses this to correlate its response */\n\tid: string;\n\ttoolId: string;\n\tsql: string;\n}\n\n/**\n * Run an arbitrary tool with structured params. Used by ctx.runTool() to call\n * direct tools (and any tool whose `fn(params)` doesn't expect a SQL string).\n * Reply is RunToolResultMessage or QueryErrorMessage.\n */\nexport interface RunToolMessage {\n\ttype: 'run_tool';\n\tid: string;\n\ttoolId: string;\n\tparams: Record<string, any>;\n}\n\nexport interface StreamMessage {\n\ttype: 'stream';\n\tchunk: string;\n}\n\nexport interface ExecutedQuery {\n\tsourceId: string;\n\tsourceName: string;\n\tsql: string;\n\tdata: any[];\n\tcount: number;\n\ttotalCount?: number;\n\texecutionTimeMs: number;\n\t/**\n\t * When the script registered a computed dataset via ctx.emit(), this is\n\t * marked virtual so component generation can distinguish it from a real SQL query.\n\t */\n\tvirtual?: boolean;\n}\n\nexport interface ResultMessage {\n\ttype: 'result';\n\tdata: any[];\n\tcount: number;\n\texecutedQueries: ExecutedQuery[];\n\texecutionTimeMs: number;\n\t/** First 5 rows of the final `data` — lets the parent judge intent-match cheaply */\n\tsampleRows: any[];\n}\n\nexport interface ErrorMessage {\n\ttype: 'error';\n\tmessage: string;\n\tstack?: string;\n\t/** \"compile\" → tsx couldn't parse; \"runtime\" → thrown during getData */\n\tphase?: 'compile' | 'runtime';\n}\n\nexport type ChildToParentMessage =\n\t| QueryMessage\n\t| RunToolMessage\n\t| StreamMessage\n\t| ResultMessage\n\t| ErrorMessage;\n\n// ============================================\n// Utilities\n// ============================================\n\n/**\n * Encode a message for transport: JSON + newline.\n */\nexport function encodeMessage(msg: ParentToChildMessage | ChildToParentMessage): string {\n\treturn JSON.stringify(msg) + '\\n';\n}\n\n/**\n * Thrown when a single NDJSON line grows past the byte ceiling without a\n * newline — a runaway writer or a pathologically large message. Callers abort\n * (kill the child / exit) instead of buffering toward OOM. See #9 in\n * backend/docs/SCRIPT-FLOW-SCALING-ISSUES.md.\n */\nexport class LineOverflowError extends Error {\n\tconstructor(public readonly buffered: number, public readonly limit: number) {\n\t\tsuper(`IPC message exceeded ${limit} bytes without a newline (${buffered} buffered) — aborting to avoid unbounded memory.`);\n\t\tthis.name = 'LineOverflowError';\n\t}\n}\n\n/** Default per-message ceiling — generous enough for large result sets, low\n * enough to catch a runaway/infinite writer. Override via SCRIPT_MAX_IPC_BYTES. */\nconst DEFAULT_MAX_IPC_BYTES = (() => {\n\tconst envVal = Number(process.env.SCRIPT_MAX_IPC_BYTES);\n\treturn Number.isFinite(envVal) && envVal > 0 ? Math.floor(envVal) : 64 * 1024 * 1024;\n})();\n\n/**\n * Split a stream of data chunks on newlines and emit each complete JSON line.\n * Partial lines are buffered until the next chunk arrives, up to `maxBytes` —\n * past which `push` throws `LineOverflowError` rather than grow unbounded.\n */\nexport class LineSplitter {\n\tprivate buffer = '';\n\n\tconstructor(private readonly maxBytes: number = DEFAULT_MAX_IPC_BYTES) {}\n\n\tpush(chunk: string): string[] {\n\t\tthis.buffer += chunk;\n\t\tconst lines: string[] = [];\n\t\tlet idx: number;\n\t\twhile ((idx = this.buffer.indexOf('\\n')) !== -1) {\n\t\t\tconst line = this.buffer.slice(0, idx);\n\t\t\tthis.buffer = this.buffer.slice(idx + 1);\n\t\t\tif (line.length > 0) lines.push(line);\n\t\t}\n\t\t// A partial line past the ceiling means no newline is coming (or the\n\t\t// message is pathologically large). Drop the buffer and signal abort.\n\t\tif (this.buffer.length > this.maxBytes) {\n\t\t\tconst buffered = this.buffer.length;\n\t\t\tthis.buffer = '';\n\t\t\tthrow new LineOverflowError(buffered, this.maxBytes);\n\t\t}\n\t\treturn lines;\n\t}\n\n\t/** Flush any remaining partial data (useful on stream close) */\n\tflush(): string | null {\n\t\tif (this.buffer.length === 0) return null;\n\t\tconst rest = this.buffer;\n\t\tthis.buffer = '';\n\t\treturn rest;\n\t}\n}\n\n/**\n * Generate a short correlation id for in-flight queries.\n * Collision within a single execution is astronomically unlikely.\n */\nexport function shortId(): string {\n\treturn Math.random().toString(36).slice(2, 10);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/userResponse/scripts/script-bootstrap.ts","../../../src/userResponse/scripts/script-ipc.ts"],"sourcesContent":["/**\r\n * script-bootstrap.ts — Child-side entrypoint for subprocess script execution.\r\n *\r\n * Runs as a separate process spawned by ScriptRunner via tsx. Reads the\r\n * target user-script path from argv[2], listens for INIT on stdin, builds\r\n * the ctx object, dynamic-imports the user script's `getData` export,\r\n * and reports the result (or error) back via stdout.\r\n *\r\n * See backend/docs/SCRIPT-FLOW-IMPLEMENTATION.md § IPC Bridge.\r\n */\r\n\r\nimport { pathToFileURL } from 'url';\r\nimport {\r\n\tencodeMessage,\r\n\tLineSplitter,\r\n\tshortId,\r\n\ttype ChildToParentMessage,\r\n\ttype ParentToChildMessage,\r\n\ttype ExecutedQuery,\r\n\ttype QueryResultMessage,\r\n\ttype QueryErrorMessage,\r\n} from './script-ipc';\r\n\r\nconst SCRIPT_ARG = process.argv[2];\r\nif (!SCRIPT_ARG) {\r\n\tsendError('Bootstrap was invoked without a script path.', 'compile');\r\n\tprocess.exit(1);\r\n}\r\n\r\n/**\r\n * Pending queries await a QUERY_RESULT or QUERY_ERROR from the parent.\r\n * Keyed by the short uuid we generated when sending the QUERY message.\r\n */\r\n/**\r\n * One pending entry per in-flight ctx.query or ctx.runTool. The resolve\r\n * payload differs by call kind ({data, count, metadata} for query,\r\n * {result} for runTool) — each caller's resolve handler unwraps the shape\r\n * it expects, so the map uses `any` rather than a discriminated union to\r\n * keep the call sites simple.\r\n */\r\nconst pending = new Map<string, {\r\n\tresolve: (value: any) => void;\r\n\treject: (err: Error) => void;\r\n\ttoolId: string;\r\n\tsql: string;\r\n\tstartedAt: number;\r\n}>();\r\n\r\n/** Completed queries — reported to the parent as part of RESULT */\r\nconst executedQueries: ExecutedQuery[] = [];\r\n\r\n/** Resolvers for the INIT handshake — getData waits for this */\r\nlet initResolve: ((init: { params: Record<string, any>; now: Date }) => void) | null = null;\r\nconst initPromise = new Promise<{ params: Record<string, any>; now: Date }>((resolve) => {\r\n\tinitResolve = resolve;\r\n});\r\n\r\n/**\r\n * Source name lookup is not available in the child (the parent holds the\r\n * ExternalTool list). We use toolId as sourceName and let the parent fill\r\n * it in when building the final ScriptResult, if needed.\r\n */\r\nfunction buildCtx(init: { params: Record<string, any>; now: Date }) {\r\n\treturn {\r\n\t\tnow: init.now,\r\n\r\n\t\tasync query(toolId: string, sql: string): Promise<{ data: any[]; count: number }> {\r\n\t\t\tconst id = shortId();\r\n\t\t\tconst startedAt = Date.now();\r\n\t\t\tconst promise = new Promise<{ data: any[]; count: number; metadata?: any }>((resolve, reject) => {\r\n\t\t\t\tpending.set(id, { resolve, reject, toolId, sql, startedAt });\r\n\t\t\t});\r\n\r\n\t\t\tsend({ type: 'query', id, toolId, sql });\r\n\r\n\t\t\tconst result = await promise;\r\n\t\t\tconst executionTimeMs = Date.now() - startedAt;\r\n\r\n\t\t\texecutedQueries.push({\r\n\t\t\t\tsourceId: toolId,\r\n\t\t\t\tsourceName: toolId, // parent overwrites with the real name\r\n\t\t\t\tsql,\r\n\t\t\t\tdata: result.data,\r\n\t\t\t\tcount: result.count,\r\n\t\t\t\ttotalCount: result.metadata?.totalCount,\r\n\t\t\t\texecutionTimeMs,\r\n\t\t\t});\r\n\r\n\t\t\treturn { data: result.data, count: result.count };\r\n\t\t},\r\n\r\n\t\t/**\r\n\t\t * Call a tool with structured params (typically a direct tool whose\r\n\t\t * fn(params) does not expect a SQL string). Returns whatever the tool\r\n\t\t * returned — shape is up to the tool. The parent verifies toolId\r\n\t\t * against the authorized externalTools list before executing.\r\n\t\t *\r\n\t\t * Use this when you need a pre-built business-logic tool (e.g.,\r\n\t\t * \"Calculate Shelf Life\") that takes structured input rather than SQL.\r\n\t\t * For SQL source tools, keep using ctx.query.\r\n\t\t *\r\n\t\t * runTool results do NOT auto-flow into executedQueries. If you want\r\n\t\t * the result visualised, register it via ctx.emit() after extracting\r\n\t\t * the array you want to chart.\r\n\t\t */\r\n\t\tasync runTool<T = any>(toolId: string, params: Record<string, any> = {}): Promise<T> {\r\n\t\t\tconst id = shortId();\r\n\t\t\tconst promise = new Promise<any>((resolveResult, rejectResult) => {\r\n\t\t\t\tpending.set(id, {\r\n\t\t\t\t\tresolve: (payload: any) => resolveResult(payload?.result ?? payload),\r\n\t\t\t\t\treject: rejectResult,\r\n\t\t\t\t\ttoolId,\r\n\t\t\t\t\tsql: `-- runTool: ${toolId}`,\r\n\t\t\t\t\tstartedAt: Date.now(),\r\n\t\t\t\t});\r\n\t\t\t});\r\n\t\t\tsend({ type: 'run_tool', id, toolId, params });\r\n\t\t\treturn promise;\r\n\t\t},\r\n\r\n\t\t/**\r\n\t\t * Register a computed dataset. Flows into executedQueries alongside\r\n\t\t * real SQL query results so component generation can render post-SQL\r\n\t\t * transforms (weighted averages, clustering, joins) as charts.\r\n\t\t */\r\n\t\temit(name: string, rows: any[], meta?: { description?: string }): void {\r\n\t\t\texecutedQueries.push({\r\n\t\t\t\tsourceId: `computed:${name}`,\r\n\t\t\t\tsourceName: meta?.description || name,\r\n\t\t\t\tsql: `-- computed dataset: ${name}`,\r\n\t\t\t\tdata: Array.isArray(rows) ? rows : [],\r\n\t\t\t\tcount: Array.isArray(rows) ? rows.length : 0,\r\n\t\t\t\texecutionTimeMs: 0,\r\n\t\t\t\tvirtual: true,\r\n\t\t\t});\r\n\t\t},\r\n\r\n\t\t/**\r\n\t\t * Forward a short progress message to the parent's StreamBuffer so the UI\r\n\t\t * continues to see per-script feedback during execution.\r\n\t\t */\r\n\t\tlog(chunk: string): void {\r\n\t\t\tsend({ type: 'stream', chunk });\r\n\t\t},\r\n\t};\r\n}\r\n\r\nfunction send(msg: ChildToParentMessage): void {\r\n\tprocess.stdout.write(encodeMessage(msg));\r\n}\r\n\r\nfunction sendError(message: string, phase: 'compile' | 'runtime', stack?: string): void {\r\n\tsend({ type: 'error', message, stack, phase });\r\n}\r\n\r\n/**\r\n * Reject every in-flight ctx.query / ctx.runTool with the same reason. Used when\r\n * the IPC channel goes away (stdin end/error) so a pending query fails fast\r\n * instead of hanging until the parent's wall-clock SIGKILL. Idempotent — a\r\n * second call finds the map already drained.\r\n */\r\nfunction rejectAllPending(reason: string): void {\r\n\tfor (const [id, entry] of Array.from(pending.entries())) {\r\n\t\tpending.delete(id);\r\n\t\tentry.reject(new Error(reason));\r\n\t}\r\n}\r\n\r\n// ============================================\r\n// stdin reader — parse NDJSON messages from the parent\r\n// ============================================\r\n\r\nconst splitter = new LineSplitter();\r\n\r\nprocess.stdin.setEncoding('utf-8');\r\nprocess.stdin.on('data', (chunk: string) => {\r\n\tlet lines: string[];\r\n\ttry {\r\n\t\tlines = splitter.push(chunk);\r\n\t} catch (err) {\r\n\t\t// Oversized parent message (#9) — abort cleanly rather than buffer to OOM.\r\n\t\tconst msg = err instanceof Error ? err.message : String(err);\r\n\t\trejectAllPending(msg);\r\n\t\tsendError(msg, 'runtime');\r\n\t\tprocess.exit(1);\r\n\t}\r\n\tfor (const line of lines) {\r\n\t\tlet msg: ParentToChildMessage;\r\n\t\ttry {\r\n\t\t\tmsg = JSON.parse(line);\r\n\t\t} catch {\r\n\t\t\tcontinue; // ignore malformed input\r\n\t\t}\r\n\t\thandleMessage(msg);\r\n\t}\r\n});\r\n\r\nprocess.stdin.on('end', () => {\r\n\t// Parent closed our stdin — no further query replies can arrive. Reject any\r\n\t// in-flight queries now so getData fails fast (and we report ERROR) instead\r\n\t// of awaiting a reply that will never come, until the parent's wall-clock kill.\r\n\trejectAllPending('Parent closed the IPC channel (stdin end) before a query completed.');\r\n});\r\n\r\nprocess.stdin.on('error', (err: Error) => {\r\n\trejectAllPending(`IPC channel error before a query completed: ${err.message}`);\r\n});\r\n\r\nfunction handleMessage(msg: ParentToChildMessage): void {\r\n\tswitch (msg.type) {\r\n\t\tcase 'init': {\r\n\t\t\tif (initResolve) {\r\n\t\t\t\tconst now = new Date(msg.now || new Date().toISOString());\r\n\t\t\t\tconst params = restoreParamTypes(msg.params || {}, msg.paramSchema);\r\n\t\t\t\tinitResolve({ params, now });\r\n\t\t\t\tinitResolve = null;\r\n\t\t\t}\r\n\t\t\tbreak;\r\n\t\t}\r\n\t\tcase 'query_result': {\r\n\t\t\tconst entry = pending.get(msg.id);\r\n\t\t\tif (entry) {\r\n\t\t\t\tpending.delete(msg.id);\r\n\t\t\t\tentry.resolve({ data: msg.data, count: msg.count, metadata: msg.metadata });\r\n\t\t\t}\r\n\t\t\tbreak;\r\n\t\t}\r\n\t\tcase 'run_tool_result': {\r\n\t\t\tconst entry = pending.get(msg.id);\r\n\t\t\tif (entry) {\r\n\t\t\t\tpending.delete(msg.id);\r\n\t\t\t\t// runTool's resolve unwraps payload.result — wrap here so the\r\n\t\t\t\t// same pending entry shape works for both query and runTool.\r\n\t\t\t\tentry.resolve({ result: msg.result });\r\n\t\t\t}\r\n\t\t\tbreak;\r\n\t\t}\r\n\t\tcase 'query_error': {\r\n\t\t\tconst entry = pending.get(msg.id);\r\n\t\t\tif (entry) {\r\n\t\t\t\tpending.delete(msg.id);\r\n\t\t\t\tentry.reject(new Error(msg.error));\r\n\t\t\t}\r\n\t\t\tbreak;\r\n\t\t}\r\n\t}\r\n}\r\n\r\n// ============================================\r\n// Main — import the user script and run getData\r\n// ============================================\r\n\r\n(async () => {\r\n\tlet getData: (ctx: any, params: any) => Promise<any>;\r\n\r\n\ttry {\r\n\t\t// Dynamic import — tsx transpiles the user .ts file on the fly\r\n\t\tconst mod = await import(pathToFileURL(SCRIPT_ARG).href);\r\n\t\tgetData = mod.getData ?? mod.default;\r\n\t\tif (typeof getData !== 'function') {\r\n\t\t\tthrow new Error(\r\n\t\t\t\t`Script at ${SCRIPT_ARG} does not export getData — expected \\`export async function getData(ctx, params)\\``,\r\n\t\t\t);\r\n\t\t}\r\n\t} catch (err) {\r\n\t\tconst e = err as Error;\r\n\t\tsendError(e.message || String(err), 'compile', e.stack);\r\n\t\tprocess.exit(1);\r\n\t\treturn;\r\n\t}\r\n\r\n\tconst init = await initPromise;\r\n\tconst ctx = buildCtx(init);\r\n\tconst startedAt = Date.now();\r\n\r\n\ttry {\r\n\t\tconst raw = await getData(ctx, init.params);\r\n\t\tconst executionTimeMs = Date.now() - startedAt;\r\n\r\n\t\tconst data = normalizeResultData(raw);\r\n\t\tconst count = typeof raw?.count === 'number' ? raw.count : data.length;\r\n\t\tconst sampleRows = data.slice(0, 5);\r\n\r\n\t\tconst result: ChildToParentMessage = {\r\n\t\t\ttype: 'result',\r\n\t\t\tdata,\r\n\t\t\tcount,\r\n\t\t\texecutedQueries,\r\n\t\t\texecutionTimeMs,\r\n\t\t\tsampleRows,\r\n\t\t};\r\n\t\tsend(result);\r\n\t\t// Give stdout a tick to flush before we exit\r\n\t\tprocess.stdout.once('drain', () => process.exit(0));\r\n\t\tsetImmediate(() => process.exit(0));\r\n\t} catch (err) {\r\n\t\tconst e = err as Error;\r\n\t\tsendError(e.message || String(err), 'runtime', e.stack);\r\n\t\tprocess.exit(1);\r\n\t}\r\n})();\r\n\r\n/**\r\n * Normalize whatever getData returned into an array. Mirrors the old\r\n * ScriptRunner semantics: direct array, { data: [] }, or { data: {...} }\r\n * wrapped as single-element array.\r\n */\r\nfunction normalizeResultData(raw: any): any[] {\r\n\tif (!raw) return [];\r\n\tif (Array.isArray(raw)) return raw;\r\n\tif (Array.isArray(raw.data)) return raw.data;\r\n\tif (raw.data && typeof raw.data === 'object') return [raw.data];\r\n\treturn [];\r\n}\r\n\r\n/**\r\n * Restore typed values that JSON stripped during IPC transport. Date and\r\n * date_range params arrive as ISO strings; re-wrap them so scripts can call\r\n * `.toISOString()` and other Date methods the same way they would in-process.\r\n */\r\nfunction restoreParamTypes(\r\n\tparams: Record<string, any>,\r\n\tschema?: Record<string, 'string' | 'number' | 'date' | 'date_range' | 'enum' | 'boolean'>,\r\n): Record<string, any> {\r\n\tif (!schema) return params;\r\n\tconst out = { ...params };\r\n\tfor (const [name, type] of Object.entries(schema)) {\r\n\t\tconst v = out[name];\r\n\t\tif (v === undefined || v === null) continue;\r\n\t\tif (type === 'date' && typeof v === 'string') {\r\n\t\t\tconst d = new Date(v);\r\n\t\t\tif (!isNaN(d.getTime())) out[name] = d;\r\n\t\t} else if (type === 'date_range' && typeof v === 'object') {\r\n\t\t\tconst from = v.from;\r\n\t\t\tconst to = v.to;\r\n\t\t\tout[name] = {\r\n\t\t\t\tfrom: typeof from === 'string' ? new Date(from) : from,\r\n\t\t\t\tto: typeof to === 'string' ? new Date(to) : to,\r\n\t\t\t};\r\n\t\t}\r\n\t}\r\n\treturn out;\r\n}\r\n","/**\r\n * script-ipc.ts — Protocol definitions for parent/child IPC.\r\n *\r\n * Transport: newline-delimited JSON over the child's stdin/stdout.\r\n * stderr is passed through for uncaught exceptions and compile errors.\r\n *\r\n * See backend/docs/SCRIPT-FLOW-IMPLEMENTATION.md § IPC Bridge.\r\n */\r\n\r\n// ============================================\r\n// Parent → Child\r\n// ============================================\r\n\r\nexport interface InitMessage {\r\n\ttype: 'init';\r\n\t/**\r\n\t * Params passed to getData(). Date/date_range values cross the IPC boundary\r\n\t * as ISO strings and are restored to Date objects in the child via paramSchema.\r\n\t */\r\n\tparams: Record<string, any>;\r\n\t/** Frozen timestamp for ctx.now — ISO string */\r\n\tnow: string;\r\n\t/**\r\n\t * Maps each parameter name to its declared type so the child can restore\r\n\t * Date objects (lost during JSON serialization).\r\n\t */\r\n\tparamSchema?: Record<string, 'string' | 'number' | 'date' | 'date_range' | 'enum' | 'boolean'>;\r\n}\r\n\r\nexport interface QueryResultMessage {\r\n\ttype: 'query_result';\r\n\t/** Matches the `id` from the child's QUERY message */\r\n\tid: string;\r\n\tdata: any[];\r\n\tcount: number;\r\n\tmetadata?: {\r\n\t\ttotalCount?: number;\r\n\t\texecutionTimeMs?: number;\r\n\t};\r\n}\r\n\r\nexport interface QueryErrorMessage {\r\n\ttype: 'query_error';\r\n\tid: string;\r\n\terror: string;\r\n}\r\n\r\n/**\r\n * Response to a `run_tool` request — carries whatever the direct-tool's fn()\r\n * returned. Shape is arbitrary; the script is expected to know what its chosen\r\n * tool yields and unwrap accordingly.\r\n */\r\nexport interface RunToolResultMessage {\r\n\ttype: 'run_tool_result';\r\n\tid: string;\r\n\tresult: any;\r\n}\r\n\r\nexport type ParentToChildMessage =\r\n\t| InitMessage\r\n\t| QueryResultMessage\r\n\t| QueryErrorMessage\r\n\t| RunToolResultMessage;\r\n\r\n// ============================================\r\n// Child → Parent\r\n// ============================================\r\n\r\nexport interface QueryMessage {\r\n\ttype: 'query';\r\n\t/** Short uuid — parent uses this to correlate its response */\r\n\tid: string;\r\n\ttoolId: string;\r\n\tsql: string;\r\n}\r\n\r\n/**\r\n * Run an arbitrary tool with structured params. Used by ctx.runTool() to call\r\n * direct tools (and any tool whose `fn(params)` doesn't expect a SQL string).\r\n * Reply is RunToolResultMessage or QueryErrorMessage.\r\n */\r\nexport interface RunToolMessage {\r\n\ttype: 'run_tool';\r\n\tid: string;\r\n\ttoolId: string;\r\n\tparams: Record<string, any>;\r\n}\r\n\r\nexport interface StreamMessage {\r\n\ttype: 'stream';\r\n\tchunk: string;\r\n}\r\n\r\nexport interface ExecutedQuery {\r\n\tsourceId: string;\r\n\tsourceName: string;\r\n\tsql: string;\r\n\tdata: any[];\r\n\tcount: number;\r\n\ttotalCount?: number;\r\n\texecutionTimeMs: number;\r\n\t/**\r\n\t * When the script registered a computed dataset via ctx.emit(), this is\r\n\t * marked virtual so component generation can distinguish it from a real SQL query.\r\n\t */\r\n\tvirtual?: boolean;\r\n}\r\n\r\nexport interface ResultMessage {\r\n\ttype: 'result';\r\n\tdata: any[];\r\n\tcount: number;\r\n\texecutedQueries: ExecutedQuery[];\r\n\texecutionTimeMs: number;\r\n\t/** First 5 rows of the final `data` — lets the parent judge intent-match cheaply */\r\n\tsampleRows: any[];\r\n}\r\n\r\nexport interface ErrorMessage {\r\n\ttype: 'error';\r\n\tmessage: string;\r\n\tstack?: string;\r\n\t/** \"compile\" → tsx couldn't parse; \"runtime\" → thrown during getData */\r\n\tphase?: 'compile' | 'runtime';\r\n}\r\n\r\nexport type ChildToParentMessage =\r\n\t| QueryMessage\r\n\t| RunToolMessage\r\n\t| StreamMessage\r\n\t| ResultMessage\r\n\t| ErrorMessage;\r\n\r\n// ============================================\r\n// Utilities\r\n// ============================================\r\n\r\n/**\r\n * Encode a message for transport: JSON + newline.\r\n */\r\nexport function encodeMessage(msg: ParentToChildMessage | ChildToParentMessage): string {\r\n\treturn JSON.stringify(msg) + '\\n';\r\n}\r\n\r\n/**\r\n * Thrown when a single NDJSON line grows past the byte ceiling without a\r\n * newline — a runaway writer or a pathologically large message. Callers abort\r\n * (kill the child / exit) instead of buffering toward OOM. See #9 in\r\n * backend/docs/SCRIPT-FLOW-SCALING-ISSUES.md.\r\n */\r\nexport class LineOverflowError extends Error {\r\n\tconstructor(public readonly buffered: number, public readonly limit: number) {\r\n\t\tsuper(`IPC message exceeded ${limit} bytes without a newline (${buffered} buffered) — aborting to avoid unbounded memory.`);\r\n\t\tthis.name = 'LineOverflowError';\r\n\t}\r\n}\r\n\r\n/** Default per-message ceiling — generous enough for large result sets, low\r\n * enough to catch a runaway/infinite writer. Override via SCRIPT_MAX_IPC_BYTES. */\r\nconst DEFAULT_MAX_IPC_BYTES = (() => {\r\n\tconst envVal = Number(process.env.SCRIPT_MAX_IPC_BYTES);\r\n\treturn Number.isFinite(envVal) && envVal > 0 ? Math.floor(envVal) : 64 * 1024 * 1024;\r\n})();\r\n\r\n/**\r\n * Split a stream of data chunks on newlines and emit each complete JSON line.\r\n * Partial lines are buffered until the next chunk arrives, up to `maxBytes` —\r\n * past which `push` throws `LineOverflowError` rather than grow unbounded.\r\n */\r\nexport class LineSplitter {\r\n\tprivate buffer = '';\r\n\r\n\tconstructor(private readonly maxBytes: number = DEFAULT_MAX_IPC_BYTES) {}\r\n\r\n\tpush(chunk: string): string[] {\r\n\t\tthis.buffer += chunk;\r\n\t\tconst lines: string[] = [];\r\n\t\tlet idx: number;\r\n\t\twhile ((idx = this.buffer.indexOf('\\n')) !== -1) {\r\n\t\t\tconst line = this.buffer.slice(0, idx);\r\n\t\t\tthis.buffer = this.buffer.slice(idx + 1);\r\n\t\t\tif (line.length > 0) lines.push(line);\r\n\t\t}\r\n\t\t// A partial line past the ceiling means no newline is coming (or the\r\n\t\t// message is pathologically large). Drop the buffer and signal abort.\r\n\t\tif (this.buffer.length > this.maxBytes) {\r\n\t\t\tconst buffered = this.buffer.length;\r\n\t\t\tthis.buffer = '';\r\n\t\t\tthrow new LineOverflowError(buffered, this.maxBytes);\r\n\t\t}\r\n\t\treturn lines;\r\n\t}\r\n\r\n\t/** Flush any remaining partial data (useful on stream close) */\r\n\tflush(): string | null {\r\n\t\tif (this.buffer.length === 0) return null;\r\n\t\tconst rest = this.buffer;\r\n\t\tthis.buffer = '';\r\n\t\treturn rest;\r\n\t}\r\n}\r\n\r\n/**\r\n * Generate a short correlation id for in-flight queries.\r\n * Collision within a single execution is astronomically unlikely.\r\n */\r\nexport function shortId(): string {\r\n\treturn Math.random().toString(36).slice(2, 10);\r\n}\r\n"],"mappings":";;;AAWA,iBAA8B;;;ACiIvB,SAAS,cAAc,KAA0D;AACvF,SAAO,KAAK,UAAU,GAAG,IAAI;AAC9B;AAQO,IAAM,oBAAN,cAAgC,MAAM;AAAA,EAC5C,YAA4B,UAAkC,OAAe;AAC5E,UAAM,wBAAwB,KAAK,6BAA6B,QAAQ,uDAAkD;AAD/F;AAAkC;AAE7D,SAAK,OAAO;AAAA,EACb;AACD;AAIA,IAAM,yBAAyB,MAAM;AACpC,QAAM,SAAS,OAAO,QAAQ,IAAI,oBAAoB;AACtD,SAAO,OAAO,SAAS,MAAM,KAAK,SAAS,IAAI,KAAK,MAAM,MAAM,IAAI,KAAK,OAAO;AACjF,GAAG;AAOI,IAAM,eAAN,MAAmB;AAAA,EAGzB,YAA6B,WAAmB,uBAAuB;AAA1C;AAF7B,SAAQ,SAAS;AAAA,EAEuD;AAAA,EAExE,KAAK,OAAyB;AAC7B,SAAK,UAAU;AACf,UAAM,QAAkB,CAAC;AACzB,QAAI;AACJ,YAAQ,MAAM,KAAK,OAAO,QAAQ,IAAI,OAAO,IAAI;AAChD,YAAM,OAAO,KAAK,OAAO,MAAM,GAAG,GAAG;AACrC,WAAK,SAAS,KAAK,OAAO,MAAM,MAAM,CAAC;AACvC,UAAI,KAAK,SAAS,EAAG,OAAM,KAAK,IAAI;AAAA,IACrC;AAGA,QAAI,KAAK,OAAO,SAAS,KAAK,UAAU;AACvC,YAAM,WAAW,KAAK,OAAO;AAC7B,WAAK,SAAS;AACd,YAAM,IAAI,kBAAkB,UAAU,KAAK,QAAQ;AAAA,IACpD;AACA,WAAO;AAAA,EACR;AAAA;AAAA,EAGA,QAAuB;AACtB,QAAI,KAAK,OAAO,WAAW,EAAG,QAAO;AACrC,UAAM,OAAO,KAAK;AAClB,SAAK,SAAS;AACd,WAAO;AAAA,EACR;AACD;AAMO,SAAS,UAAkB;AACjC,SAAO,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,EAAE;AAC9C;;;ADzLA,IAAM,aAAa,QAAQ,KAAK,CAAC;AACjC,IAAI,CAAC,YAAY;AAChB,YAAU,gDAAgD,SAAS;AACnE,UAAQ,KAAK,CAAC;AACf;AAaA,IAAM,UAAU,oBAAI,IAMjB;AAGH,IAAM,kBAAmC,CAAC;AAG1C,IAAI,cAAmF;AACvF,IAAM,cAAc,IAAI,QAAoD,CAAC,YAAY;AACxF,gBAAc;AACf,CAAC;AAOD,SAAS,SAAS,MAAkD;AACnE,SAAO;AAAA,IACN,KAAK,KAAK;AAAA,IAEV,MAAM,MAAM,QAAgB,KAAsD;AACjF,YAAM,KAAK,QAAQ;AACnB,YAAM,YAAY,KAAK,IAAI;AAC3B,YAAM,UAAU,IAAI,QAAwD,CAAC,SAAS,WAAW;AAChG,gBAAQ,IAAI,IAAI,EAAE,SAAS,QAAQ,QAAQ,KAAK,UAAU,CAAC;AAAA,MAC5D,CAAC;AAED,WAAK,EAAE,MAAM,SAAS,IAAI,QAAQ,IAAI,CAAC;AAEvC,YAAM,SAAS,MAAM;AACrB,YAAM,kBAAkB,KAAK,IAAI,IAAI;AAErC,sBAAgB,KAAK;AAAA,QACpB,UAAU;AAAA,QACV,YAAY;AAAA;AAAA,QACZ;AAAA,QACA,MAAM,OAAO;AAAA,QACb,OAAO,OAAO;AAAA,QACd,YAAY,OAAO,UAAU;AAAA,QAC7B;AAAA,MACD,CAAC;AAED,aAAO,EAAE,MAAM,OAAO,MAAM,OAAO,OAAO,MAAM;AAAA,IACjD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAgBA,MAAM,QAAiB,QAAgB,SAA8B,CAAC,GAAe;AACpF,YAAM,KAAK,QAAQ;AACnB,YAAM,UAAU,IAAI,QAAa,CAAC,eAAe,iBAAiB;AACjE,gBAAQ,IAAI,IAAI;AAAA,UACf,SAAS,CAAC,YAAiB,cAAc,SAAS,UAAU,OAAO;AAAA,UACnE,QAAQ;AAAA,UACR;AAAA,UACA,KAAK,eAAe,MAAM;AAAA,UAC1B,WAAW,KAAK,IAAI;AAAA,QACrB,CAAC;AAAA,MACF,CAAC;AACD,WAAK,EAAE,MAAM,YAAY,IAAI,QAAQ,OAAO,CAAC;AAC7C,aAAO;AAAA,IACR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOA,KAAK,MAAc,MAAa,MAAuC;AACtE,sBAAgB,KAAK;AAAA,QACpB,UAAU,YAAY,IAAI;AAAA,QAC1B,YAAY,MAAM,eAAe;AAAA,QACjC,KAAK,wBAAwB,IAAI;AAAA,QACjC,MAAM,MAAM,QAAQ,IAAI,IAAI,OAAO,CAAC;AAAA,QACpC,OAAO,MAAM,QAAQ,IAAI,IAAI,KAAK,SAAS;AAAA,QAC3C,iBAAiB;AAAA,QACjB,SAAS;AAAA,MACV,CAAC;AAAA,IACF;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,IAAI,OAAqB;AACxB,WAAK,EAAE,MAAM,UAAU,MAAM,CAAC;AAAA,IAC/B;AAAA,EACD;AACD;AAEA,SAAS,KAAK,KAAiC;AAC9C,UAAQ,OAAO,MAAM,cAAc,GAAG,CAAC;AACxC;AAEA,SAAS,UAAU,SAAiB,OAA8B,OAAsB;AACvF,OAAK,EAAE,MAAM,SAAS,SAAS,OAAO,MAAM,CAAC;AAC9C;AAQA,SAAS,iBAAiB,QAAsB;AAC/C,aAAW,CAAC,IAAI,KAAK,KAAK,MAAM,KAAK,QAAQ,QAAQ,CAAC,GAAG;AACxD,YAAQ,OAAO,EAAE;AACjB,UAAM,OAAO,IAAI,MAAM,MAAM,CAAC;AAAA,EAC/B;AACD;AAMA,IAAM,WAAW,IAAI,aAAa;AAElC,QAAQ,MAAM,YAAY,OAAO;AACjC,QAAQ,MAAM,GAAG,QAAQ,CAAC,UAAkB;AAC3C,MAAI;AACJ,MAAI;AACH,YAAQ,SAAS,KAAK,KAAK;AAAA,EAC5B,SAAS,KAAK;AAEb,UAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,qBAAiB,GAAG;AACpB,cAAU,KAAK,SAAS;AACxB,YAAQ,KAAK,CAAC;AAAA,EACf;AACA,aAAW,QAAQ,OAAO;AACzB,QAAI;AACJ,QAAI;AACH,YAAM,KAAK,MAAM,IAAI;AAAA,IACtB,QAAQ;AACP;AAAA,IACD;AACA,kBAAc,GAAG;AAAA,EAClB;AACD,CAAC;AAED,QAAQ,MAAM,GAAG,OAAO,MAAM;AAI7B,mBAAiB,qEAAqE;AACvF,CAAC;AAED,QAAQ,MAAM,GAAG,SAAS,CAAC,QAAe;AACzC,mBAAiB,+CAA+C,IAAI,OAAO,EAAE;AAC9E,CAAC;AAED,SAAS,cAAc,KAAiC;AACvD,UAAQ,IAAI,MAAM;AAAA,IACjB,KAAK,QAAQ;AACZ,UAAI,aAAa;AAChB,cAAM,MAAM,IAAI,KAAK,IAAI,QAAO,oBAAI,KAAK,GAAE,YAAY,CAAC;AACxD,cAAM,SAAS,kBAAkB,IAAI,UAAU,CAAC,GAAG,IAAI,WAAW;AAClE,oBAAY,EAAE,QAAQ,IAAI,CAAC;AAC3B,sBAAc;AAAA,MACf;AACA;AAAA,IACD;AAAA,IACA,KAAK,gBAAgB;AACpB,YAAM,QAAQ,QAAQ,IAAI,IAAI,EAAE;AAChC,UAAI,OAAO;AACV,gBAAQ,OAAO,IAAI,EAAE;AACrB,cAAM,QAAQ,EAAE,MAAM,IAAI,MAAM,OAAO,IAAI,OAAO,UAAU,IAAI,SAAS,CAAC;AAAA,MAC3E;AACA;AAAA,IACD;AAAA,IACA,KAAK,mBAAmB;AACvB,YAAM,QAAQ,QAAQ,IAAI,IAAI,EAAE;AAChC,UAAI,OAAO;AACV,gBAAQ,OAAO,IAAI,EAAE;AAGrB,cAAM,QAAQ,EAAE,QAAQ,IAAI,OAAO,CAAC;AAAA,MACrC;AACA;AAAA,IACD;AAAA,IACA,KAAK,eAAe;AACnB,YAAM,QAAQ,QAAQ,IAAI,IAAI,EAAE;AAChC,UAAI,OAAO;AACV,gBAAQ,OAAO,IAAI,EAAE;AACrB,cAAM,OAAO,IAAI,MAAM,IAAI,KAAK,CAAC;AAAA,MAClC;AACA;AAAA,IACD;AAAA,EACD;AACD;AAAA,CAMC,YAAY;AACZ,MAAI;AAEJ,MAAI;AAEH,UAAM,MAAM,MAAM,WAAO,0BAAc,UAAU,EAAE;AACnD,cAAU,IAAI,WAAW,IAAI;AAC7B,QAAI,OAAO,YAAY,YAAY;AAClC,YAAM,IAAI;AAAA,QACT,aAAa,UAAU;AAAA,MACxB;AAAA,IACD;AAAA,EACD,SAAS,KAAK;AACb,UAAM,IAAI;AACV,cAAU,EAAE,WAAW,OAAO,GAAG,GAAG,WAAW,EAAE,KAAK;AACtD,YAAQ,KAAK,CAAC;AACd;AAAA,EACD;AAEA,QAAM,OAAO,MAAM;AACnB,QAAM,MAAM,SAAS,IAAI;AACzB,QAAM,YAAY,KAAK,IAAI;AAE3B,MAAI;AACH,UAAM,MAAM,MAAM,QAAQ,KAAK,KAAK,MAAM;AAC1C,UAAM,kBAAkB,KAAK,IAAI,IAAI;AAErC,UAAM,OAAO,oBAAoB,GAAG;AACpC,UAAM,QAAQ,OAAO,KAAK,UAAU,WAAW,IAAI,QAAQ,KAAK;AAChE,UAAM,aAAa,KAAK,MAAM,GAAG,CAAC;AAElC,UAAM,SAA+B;AAAA,MACpC,MAAM;AAAA,MACN;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACD;AACA,SAAK,MAAM;AAEX,YAAQ,OAAO,KAAK,SAAS,MAAM,QAAQ,KAAK,CAAC,CAAC;AAClD,iBAAa,MAAM,QAAQ,KAAK,CAAC,CAAC;AAAA,EACnC,SAAS,KAAK;AACb,UAAM,IAAI;AACV,cAAU,EAAE,WAAW,OAAO,GAAG,GAAG,WAAW,EAAE,KAAK;AACtD,YAAQ,KAAK,CAAC;AAAA,EACf;AACD,GAAG;AAOH,SAAS,oBAAoB,KAAiB;AAC7C,MAAI,CAAC,IAAK,QAAO,CAAC;AAClB,MAAI,MAAM,QAAQ,GAAG,EAAG,QAAO;AAC/B,MAAI,MAAM,QAAQ,IAAI,IAAI,EAAG,QAAO,IAAI;AACxC,MAAI,IAAI,QAAQ,OAAO,IAAI,SAAS,SAAU,QAAO,CAAC,IAAI,IAAI;AAC9D,SAAO,CAAC;AACT;AAOA,SAAS,kBACR,QACA,QACsB;AACtB,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,MAAM,EAAE,GAAG,OAAO;AACxB,aAAW,CAAC,MAAM,IAAI,KAAK,OAAO,QAAQ,MAAM,GAAG;AAClD,UAAM,IAAI,IAAI,IAAI;AAClB,QAAI,MAAM,UAAa,MAAM,KAAM;AACnC,QAAI,SAAS,UAAU,OAAO,MAAM,UAAU;AAC7C,YAAM,IAAI,IAAI,KAAK,CAAC;AACpB,UAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAG,KAAI,IAAI,IAAI;AAAA,IACtC,WAAW,SAAS,gBAAgB,OAAO,MAAM,UAAU;AAC1D,YAAM,OAAO,EAAE;AACf,YAAM,KAAK,EAAE;AACb,UAAI,IAAI,IAAI;AAAA,QACX,MAAM,OAAO,SAAS,WAAW,IAAI,KAAK,IAAI,IAAI;AAAA,QAClD,IAAI,OAAO,OAAO,WAAW,IAAI,KAAK,EAAE,IAAI;AAAA,MAC7C;AAAA,IACD;AAAA,EACD;AACA,SAAO;AACR;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/userResponse/scripts/script-bootstrap.ts","../../../src/userResponse/scripts/script-ipc.ts"],"sourcesContent":["/**\n * script-bootstrap.ts — Child-side entrypoint for subprocess script execution.\n *\n * Runs as a separate process spawned by ScriptRunner via tsx. Reads the\n * target user-script path from argv[2], listens for INIT on stdin, builds\n * the ctx object, dynamic-imports the user script's `getData` export,\n * and reports the result (or error) back via stdout.\n *\n * See backend/docs/SCRIPT-FLOW-IMPLEMENTATION.md § IPC Bridge.\n */\n\nimport { pathToFileURL } from 'url';\nimport {\n\tencodeMessage,\n\tLineSplitter,\n\tshortId,\n\ttype ChildToParentMessage,\n\ttype ParentToChildMessage,\n\ttype ExecutedQuery,\n\ttype QueryResultMessage,\n\ttype QueryErrorMessage,\n} from './script-ipc';\n\nconst SCRIPT_ARG = process.argv[2];\nif (!SCRIPT_ARG) {\n\tsendError('Bootstrap was invoked without a script path.', 'compile');\n\tprocess.exit(1);\n}\n\n/**\n * Pending queries await a QUERY_RESULT or QUERY_ERROR from the parent.\n * Keyed by the short uuid we generated when sending the QUERY message.\n */\n/**\n * One pending entry per in-flight ctx.query or ctx.runTool. The resolve\n * payload differs by call kind ({data, count, metadata} for query,\n * {result} for runTool) — each caller's resolve handler unwraps the shape\n * it expects, so the map uses `any` rather than a discriminated union to\n * keep the call sites simple.\n */\nconst pending = new Map<string, {\n\tresolve: (value: any) => void;\n\treject: (err: Error) => void;\n\ttoolId: string;\n\tsql: string;\n\tstartedAt: number;\n}>();\n\n/** Completed queries — reported to the parent as part of RESULT */\nconst executedQueries: ExecutedQuery[] = [];\n\n/** Resolvers for the INIT handshake — getData waits for this */\nlet initResolve: ((init: { params: Record<string, any>; now: Date }) => void) | null = null;\nconst initPromise = new Promise<{ params: Record<string, any>; now: Date }>((resolve) => {\n\tinitResolve = resolve;\n});\n\n/**\n * Source name lookup is not available in the child (the parent holds the\n * ExternalTool list). We use toolId as sourceName and let the parent fill\n * it in when building the final ScriptResult, if needed.\n */\nfunction buildCtx(init: { params: Record<string, any>; now: Date }) {\n\treturn {\n\t\tnow: init.now,\n\n\t\tasync query(toolId: string, sql: string): Promise<{ data: any[]; count: number }> {\n\t\t\tconst id = shortId();\n\t\t\tconst startedAt = Date.now();\n\t\t\tconst promise = new Promise<{ data: any[]; count: number; metadata?: any }>((resolve, reject) => {\n\t\t\t\tpending.set(id, { resolve, reject, toolId, sql, startedAt });\n\t\t\t});\n\n\t\t\tsend({ type: 'query', id, toolId, sql });\n\n\t\t\tconst result = await promise;\n\t\t\tconst executionTimeMs = Date.now() - startedAt;\n\n\t\t\texecutedQueries.push({\n\t\t\t\tsourceId: toolId,\n\t\t\t\tsourceName: toolId, // parent overwrites with the real name\n\t\t\t\tsql,\n\t\t\t\tdata: result.data,\n\t\t\t\tcount: result.count,\n\t\t\t\ttotalCount: result.metadata?.totalCount,\n\t\t\t\texecutionTimeMs,\n\t\t\t});\n\n\t\t\treturn { data: result.data, count: result.count };\n\t\t},\n\n\t\t/**\n\t\t * Call a tool with structured params (typically a direct tool whose\n\t\t * fn(params) does not expect a SQL string). Returns whatever the tool\n\t\t * returned — shape is up to the tool. The parent verifies toolId\n\t\t * against the authorized externalTools list before executing.\n\t\t *\n\t\t * Use this when you need a pre-built business-logic tool (e.g.,\n\t\t * \"Calculate Shelf Life\") that takes structured input rather than SQL.\n\t\t * For SQL source tools, keep using ctx.query.\n\t\t *\n\t\t * runTool results do NOT auto-flow into executedQueries. If you want\n\t\t * the result visualised, register it via ctx.emit() after extracting\n\t\t * the array you want to chart.\n\t\t */\n\t\tasync runTool<T = any>(toolId: string, params: Record<string, any> = {}): Promise<T> {\n\t\t\tconst id = shortId();\n\t\t\tconst promise = new Promise<any>((resolveResult, rejectResult) => {\n\t\t\t\tpending.set(id, {\n\t\t\t\t\tresolve: (payload: any) => resolveResult(payload?.result ?? payload),\n\t\t\t\t\treject: rejectResult,\n\t\t\t\t\ttoolId,\n\t\t\t\t\tsql: `-- runTool: ${toolId}`,\n\t\t\t\t\tstartedAt: Date.now(),\n\t\t\t\t});\n\t\t\t});\n\t\t\tsend({ type: 'run_tool', id, toolId, params });\n\t\t\treturn promise;\n\t\t},\n\n\t\t/**\n\t\t * Register a computed dataset. Flows into executedQueries alongside\n\t\t * real SQL query results so component generation can render post-SQL\n\t\t * transforms (weighted averages, clustering, joins) as charts.\n\t\t */\n\t\temit(name: string, rows: any[], meta?: { description?: string }): void {\n\t\t\texecutedQueries.push({\n\t\t\t\tsourceId: `computed:${name}`,\n\t\t\t\tsourceName: meta?.description || name,\n\t\t\t\tsql: `-- computed dataset: ${name}`,\n\t\t\t\tdata: Array.isArray(rows) ? rows : [],\n\t\t\t\tcount: Array.isArray(rows) ? rows.length : 0,\n\t\t\t\texecutionTimeMs: 0,\n\t\t\t\tvirtual: true,\n\t\t\t});\n\t\t},\n\n\t\t/**\n\t\t * Forward a short progress message to the parent's StreamBuffer so the UI\n\t\t * continues to see per-script feedback during execution.\n\t\t */\n\t\tlog(chunk: string): void {\n\t\t\tsend({ type: 'stream', chunk });\n\t\t},\n\t};\n}\n\nfunction send(msg: ChildToParentMessage): void {\n\tprocess.stdout.write(encodeMessage(msg));\n}\n\nfunction sendError(message: string, phase: 'compile' | 'runtime', stack?: string): void {\n\tsend({ type: 'error', message, stack, phase });\n}\n\n/**\n * Reject every in-flight ctx.query / ctx.runTool with the same reason. Used when\n * the IPC channel goes away (stdin end/error) so a pending query fails fast\n * instead of hanging until the parent's wall-clock SIGKILL. Idempotent — a\n * second call finds the map already drained.\n */\nfunction rejectAllPending(reason: string): void {\n\tfor (const [id, entry] of Array.from(pending.entries())) {\n\t\tpending.delete(id);\n\t\tentry.reject(new Error(reason));\n\t}\n}\n\n// ============================================\n// stdin reader — parse NDJSON messages from the parent\n// ============================================\n\nconst splitter = new LineSplitter();\n\nprocess.stdin.setEncoding('utf-8');\nprocess.stdin.on('data', (chunk: string) => {\n\tlet lines: string[];\n\ttry {\n\t\tlines = splitter.push(chunk);\n\t} catch (err) {\n\t\t// Oversized parent message (#9) — abort cleanly rather than buffer to OOM.\n\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\trejectAllPending(msg);\n\t\tsendError(msg, 'runtime');\n\t\tprocess.exit(1);\n\t}\n\tfor (const line of lines) {\n\t\tlet msg: ParentToChildMessage;\n\t\ttry {\n\t\t\tmsg = JSON.parse(line);\n\t\t} catch {\n\t\t\tcontinue; // ignore malformed input\n\t\t}\n\t\thandleMessage(msg);\n\t}\n});\n\nprocess.stdin.on('end', () => {\n\t// Parent closed our stdin — no further query replies can arrive. Reject any\n\t// in-flight queries now so getData fails fast (and we report ERROR) instead\n\t// of awaiting a reply that will never come, until the parent's wall-clock kill.\n\trejectAllPending('Parent closed the IPC channel (stdin end) before a query completed.');\n});\n\nprocess.stdin.on('error', (err: Error) => {\n\trejectAllPending(`IPC channel error before a query completed: ${err.message}`);\n});\n\nfunction handleMessage(msg: ParentToChildMessage): void {\n\tswitch (msg.type) {\n\t\tcase 'init': {\n\t\t\tif (initResolve) {\n\t\t\t\tconst now = new Date(msg.now || new Date().toISOString());\n\t\t\t\tconst params = restoreParamTypes(msg.params || {}, msg.paramSchema);\n\t\t\t\tinitResolve({ params, now });\n\t\t\t\tinitResolve = null;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase 'query_result': {\n\t\t\tconst entry = pending.get(msg.id);\n\t\t\tif (entry) {\n\t\t\t\tpending.delete(msg.id);\n\t\t\t\tentry.resolve({ data: msg.data, count: msg.count, metadata: msg.metadata });\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase 'run_tool_result': {\n\t\t\tconst entry = pending.get(msg.id);\n\t\t\tif (entry) {\n\t\t\t\tpending.delete(msg.id);\n\t\t\t\t// runTool's resolve unwraps payload.result — wrap here so the\n\t\t\t\t// same pending entry shape works for both query and runTool.\n\t\t\t\tentry.resolve({ result: msg.result });\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tcase 'query_error': {\n\t\t\tconst entry = pending.get(msg.id);\n\t\t\tif (entry) {\n\t\t\t\tpending.delete(msg.id);\n\t\t\t\tentry.reject(new Error(msg.error));\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n}\n\n// ============================================\n// Main — import the user script and run getData\n// ============================================\n\n(async () => {\n\tlet getData: (ctx: any, params: any) => Promise<any>;\n\n\ttry {\n\t\t// Dynamic import — tsx transpiles the user .ts file on the fly\n\t\tconst mod = await import(pathToFileURL(SCRIPT_ARG).href);\n\t\tgetData = mod.getData ?? mod.default;\n\t\tif (typeof getData !== 'function') {\n\t\t\tthrow new Error(\n\t\t\t\t`Script at ${SCRIPT_ARG} does not export getData — expected \\`export async function getData(ctx, params)\\``,\n\t\t\t);\n\t\t}\n\t} catch (err) {\n\t\tconst e = err as Error;\n\t\tsendError(e.message || String(err), 'compile', e.stack);\n\t\tprocess.exit(1);\n\t\treturn;\n\t}\n\n\tconst init = await initPromise;\n\tconst ctx = buildCtx(init);\n\tconst startedAt = Date.now();\n\n\ttry {\n\t\tconst raw = await getData(ctx, init.params);\n\t\tconst executionTimeMs = Date.now() - startedAt;\n\n\t\tconst data = normalizeResultData(raw);\n\t\tconst count = typeof raw?.count === 'number' ? raw.count : data.length;\n\t\tconst sampleRows = data.slice(0, 5);\n\n\t\tconst result: ChildToParentMessage = {\n\t\t\ttype: 'result',\n\t\t\tdata,\n\t\t\tcount,\n\t\t\texecutedQueries,\n\t\t\texecutionTimeMs,\n\t\t\tsampleRows,\n\t\t};\n\t\tsend(result);\n\t\t// Give stdout a tick to flush before we exit\n\t\tprocess.stdout.once('drain', () => process.exit(0));\n\t\tsetImmediate(() => process.exit(0));\n\t} catch (err) {\n\t\tconst e = err as Error;\n\t\tsendError(e.message || String(err), 'runtime', e.stack);\n\t\tprocess.exit(1);\n\t}\n})();\n\n/**\n * Normalize whatever getData returned into an array. Mirrors the old\n * ScriptRunner semantics: direct array, { data: [] }, or { data: {...} }\n * wrapped as single-element array.\n */\nfunction normalizeResultData(raw: any): any[] {\n\tif (!raw) return [];\n\tif (Array.isArray(raw)) return raw;\n\tif (Array.isArray(raw.data)) return raw.data;\n\tif (raw.data && typeof raw.data === 'object') return [raw.data];\n\treturn [];\n}\n\n/**\n * Restore typed values that JSON stripped during IPC transport. Date and\n * date_range params arrive as ISO strings; re-wrap them so scripts can call\n * `.toISOString()` and other Date methods the same way they would in-process.\n */\nfunction restoreParamTypes(\n\tparams: Record<string, any>,\n\tschema?: Record<string, 'string' | 'number' | 'date' | 'date_range' | 'enum' | 'boolean'>,\n): Record<string, any> {\n\tif (!schema) return params;\n\tconst out = { ...params };\n\tfor (const [name, type] of Object.entries(schema)) {\n\t\tconst v = out[name];\n\t\tif (v === undefined || v === null) continue;\n\t\tif (type === 'date' && typeof v === 'string') {\n\t\t\tconst d = new Date(v);\n\t\t\tif (!isNaN(d.getTime())) out[name] = d;\n\t\t} else if (type === 'date_range' && typeof v === 'object') {\n\t\t\tconst from = v.from;\n\t\t\tconst to = v.to;\n\t\t\tout[name] = {\n\t\t\t\tfrom: typeof from === 'string' ? new Date(from) : from,\n\t\t\t\tto: typeof to === 'string' ? new Date(to) : to,\n\t\t\t};\n\t\t}\n\t}\n\treturn out;\n}\n","/**\n * script-ipc.ts — Protocol definitions for parent/child IPC.\n *\n * Transport: newline-delimited JSON over the child's stdin/stdout.\n * stderr is passed through for uncaught exceptions and compile errors.\n *\n * See backend/docs/SCRIPT-FLOW-IMPLEMENTATION.md § IPC Bridge.\n */\n\n// ============================================\n// Parent → Child\n// ============================================\n\nexport interface InitMessage {\n\ttype: 'init';\n\t/**\n\t * Params passed to getData(). Date/date_range values cross the IPC boundary\n\t * as ISO strings and are restored to Date objects in the child via paramSchema.\n\t */\n\tparams: Record<string, any>;\n\t/** Frozen timestamp for ctx.now — ISO string */\n\tnow: string;\n\t/**\n\t * Maps each parameter name to its declared type so the child can restore\n\t * Date objects (lost during JSON serialization).\n\t */\n\tparamSchema?: Record<string, 'string' | 'number' | 'date' | 'date_range' | 'enum' | 'boolean'>;\n}\n\nexport interface QueryResultMessage {\n\ttype: 'query_result';\n\t/** Matches the `id` from the child's QUERY message */\n\tid: string;\n\tdata: any[];\n\tcount: number;\n\tmetadata?: {\n\t\ttotalCount?: number;\n\t\texecutionTimeMs?: number;\n\t};\n}\n\nexport interface QueryErrorMessage {\n\ttype: 'query_error';\n\tid: string;\n\terror: string;\n}\n\n/**\n * Response to a `run_tool` request — carries whatever the direct-tool's fn()\n * returned. Shape is arbitrary; the script is expected to know what its chosen\n * tool yields and unwrap accordingly.\n */\nexport interface RunToolResultMessage {\n\ttype: 'run_tool_result';\n\tid: string;\n\tresult: any;\n}\n\nexport type ParentToChildMessage =\n\t| InitMessage\n\t| QueryResultMessage\n\t| QueryErrorMessage\n\t| RunToolResultMessage;\n\n// ============================================\n// Child → Parent\n// ============================================\n\nexport interface QueryMessage {\n\ttype: 'query';\n\t/** Short uuid — parent uses this to correlate its response */\n\tid: string;\n\ttoolId: string;\n\tsql: string;\n}\n\n/**\n * Run an arbitrary tool with structured params. Used by ctx.runTool() to call\n * direct tools (and any tool whose `fn(params)` doesn't expect a SQL string).\n * Reply is RunToolResultMessage or QueryErrorMessage.\n */\nexport interface RunToolMessage {\n\ttype: 'run_tool';\n\tid: string;\n\ttoolId: string;\n\tparams: Record<string, any>;\n}\n\nexport interface StreamMessage {\n\ttype: 'stream';\n\tchunk: string;\n}\n\nexport interface ExecutedQuery {\n\tsourceId: string;\n\tsourceName: string;\n\tsql: string;\n\tdata: any[];\n\tcount: number;\n\ttotalCount?: number;\n\texecutionTimeMs: number;\n\t/**\n\t * When the script registered a computed dataset via ctx.emit(), this is\n\t * marked virtual so component generation can distinguish it from a real SQL query.\n\t */\n\tvirtual?: boolean;\n}\n\nexport interface ResultMessage {\n\ttype: 'result';\n\tdata: any[];\n\tcount: number;\n\texecutedQueries: ExecutedQuery[];\n\texecutionTimeMs: number;\n\t/** First 5 rows of the final `data` — lets the parent judge intent-match cheaply */\n\tsampleRows: any[];\n}\n\nexport interface ErrorMessage {\n\ttype: 'error';\n\tmessage: string;\n\tstack?: string;\n\t/** \"compile\" → tsx couldn't parse; \"runtime\" → thrown during getData */\n\tphase?: 'compile' | 'runtime';\n}\n\nexport type ChildToParentMessage =\n\t| QueryMessage\n\t| RunToolMessage\n\t| StreamMessage\n\t| ResultMessage\n\t| ErrorMessage;\n\n// ============================================\n// Utilities\n// ============================================\n\n/**\n * Encode a message for transport: JSON + newline.\n */\nexport function encodeMessage(msg: ParentToChildMessage | ChildToParentMessage): string {\n\treturn JSON.stringify(msg) + '\\n';\n}\n\n/**\n * Thrown when a single NDJSON line grows past the byte ceiling without a\n * newline — a runaway writer or a pathologically large message. Callers abort\n * (kill the child / exit) instead of buffering toward OOM. See #9 in\n * backend/docs/SCRIPT-FLOW-SCALING-ISSUES.md.\n */\nexport class LineOverflowError extends Error {\n\tconstructor(public readonly buffered: number, public readonly limit: number) {\n\t\tsuper(`IPC message exceeded ${limit} bytes without a newline (${buffered} buffered) — aborting to avoid unbounded memory.`);\n\t\tthis.name = 'LineOverflowError';\n\t}\n}\n\n/** Default per-message ceiling — generous enough for large result sets, low\n * enough to catch a runaway/infinite writer. Override via SCRIPT_MAX_IPC_BYTES. */\nconst DEFAULT_MAX_IPC_BYTES = (() => {\n\tconst envVal = Number(process.env.SCRIPT_MAX_IPC_BYTES);\n\treturn Number.isFinite(envVal) && envVal > 0 ? Math.floor(envVal) : 64 * 1024 * 1024;\n})();\n\n/**\n * Split a stream of data chunks on newlines and emit each complete JSON line.\n * Partial lines are buffered until the next chunk arrives, up to `maxBytes` —\n * past which `push` throws `LineOverflowError` rather than grow unbounded.\n */\nexport class LineSplitter {\n\tprivate buffer = '';\n\n\tconstructor(private readonly maxBytes: number = DEFAULT_MAX_IPC_BYTES) {}\n\n\tpush(chunk: string): string[] {\n\t\tthis.buffer += chunk;\n\t\tconst lines: string[] = [];\n\t\tlet idx: number;\n\t\twhile ((idx = this.buffer.indexOf('\\n')) !== -1) {\n\t\t\tconst line = this.buffer.slice(0, idx);\n\t\t\tthis.buffer = this.buffer.slice(idx + 1);\n\t\t\tif (line.length > 0) lines.push(line);\n\t\t}\n\t\t// A partial line past the ceiling means no newline is coming (or the\n\t\t// message is pathologically large). Drop the buffer and signal abort.\n\t\tif (this.buffer.length > this.maxBytes) {\n\t\t\tconst buffered = this.buffer.length;\n\t\t\tthis.buffer = '';\n\t\t\tthrow new LineOverflowError(buffered, this.maxBytes);\n\t\t}\n\t\treturn lines;\n\t}\n\n\t/** Flush any remaining partial data (useful on stream close) */\n\tflush(): string | null {\n\t\tif (this.buffer.length === 0) return null;\n\t\tconst rest = this.buffer;\n\t\tthis.buffer = '';\n\t\treturn rest;\n\t}\n}\n\n/**\n * Generate a short correlation id for in-flight queries.\n * Collision within a single execution is astronomically unlikely.\n */\nexport function shortId(): string {\n\treturn Math.random().toString(36).slice(2, 10);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/userResponse/scripts/script-bootstrap.ts","../../../src/userResponse/scripts/script-ipc.ts"],"sourcesContent":["/**\r\n * script-bootstrap.ts — Child-side entrypoint for subprocess script execution.\r\n *\r\n * Runs as a separate process spawned by ScriptRunner via tsx. Reads the\r\n * target user-script path from argv[2], listens for INIT on stdin, builds\r\n * the ctx object, dynamic-imports the user script's `getData` export,\r\n * and reports the result (or error) back via stdout.\r\n *\r\n * See backend/docs/SCRIPT-FLOW-IMPLEMENTATION.md § IPC Bridge.\r\n */\r\n\r\nimport { pathToFileURL } from 'url';\r\nimport {\r\n\tencodeMessage,\r\n\tLineSplitter,\r\n\tshortId,\r\n\ttype ChildToParentMessage,\r\n\ttype ParentToChildMessage,\r\n\ttype ExecutedQuery,\r\n\ttype QueryResultMessage,\r\n\ttype QueryErrorMessage,\r\n} from './script-ipc';\r\n\r\nconst SCRIPT_ARG = process.argv[2];\r\nif (!SCRIPT_ARG) {\r\n\tsendError('Bootstrap was invoked without a script path.', 'compile');\r\n\tprocess.exit(1);\r\n}\r\n\r\n/**\r\n * Pending queries await a QUERY_RESULT or QUERY_ERROR from the parent.\r\n * Keyed by the short uuid we generated when sending the QUERY message.\r\n */\r\n/**\r\n * One pending entry per in-flight ctx.query or ctx.runTool. The resolve\r\n * payload differs by call kind ({data, count, metadata} for query,\r\n * {result} for runTool) — each caller's resolve handler unwraps the shape\r\n * it expects, so the map uses `any` rather than a discriminated union to\r\n * keep the call sites simple.\r\n */\r\nconst pending = new Map<string, {\r\n\tresolve: (value: any) => void;\r\n\treject: (err: Error) => void;\r\n\ttoolId: string;\r\n\tsql: string;\r\n\tstartedAt: number;\r\n}>();\r\n\r\n/** Completed queries — reported to the parent as part of RESULT */\r\nconst executedQueries: ExecutedQuery[] = [];\r\n\r\n/** Resolvers for the INIT handshake — getData waits for this */\r\nlet initResolve: ((init: { params: Record<string, any>; now: Date }) => void) | null = null;\r\nconst initPromise = new Promise<{ params: Record<string, any>; now: Date }>((resolve) => {\r\n\tinitResolve = resolve;\r\n});\r\n\r\n/**\r\n * Source name lookup is not available in the child (the parent holds the\r\n * ExternalTool list). We use toolId as sourceName and let the parent fill\r\n * it in when building the final ScriptResult, if needed.\r\n */\r\nfunction buildCtx(init: { params: Record<string, any>; now: Date }) {\r\n\treturn {\r\n\t\tnow: init.now,\r\n\r\n\t\tasync query(toolId: string, sql: string): Promise<{ data: any[]; count: number }> {\r\n\t\t\tconst id = shortId();\r\n\t\t\tconst startedAt = Date.now();\r\n\t\t\tconst promise = new Promise<{ data: any[]; count: number; metadata?: any }>((resolve, reject) => {\r\n\t\t\t\tpending.set(id, { resolve, reject, toolId, sql, startedAt });\r\n\t\t\t});\r\n\r\n\t\t\tsend({ type: 'query', id, toolId, sql });\r\n\r\n\t\t\tconst result = await promise;\r\n\t\t\tconst executionTimeMs = Date.now() - startedAt;\r\n\r\n\t\t\texecutedQueries.push({\r\n\t\t\t\tsourceId: toolId,\r\n\t\t\t\tsourceName: toolId, // parent overwrites with the real name\r\n\t\t\t\tsql,\r\n\t\t\t\tdata: result.data,\r\n\t\t\t\tcount: result.count,\r\n\t\t\t\ttotalCount: result.metadata?.totalCount,\r\n\t\t\t\texecutionTimeMs,\r\n\t\t\t});\r\n\r\n\t\t\treturn { data: result.data, count: result.count };\r\n\t\t},\r\n\r\n\t\t/**\r\n\t\t * Call a tool with structured params (typically a direct tool whose\r\n\t\t * fn(params) does not expect a SQL string). Returns whatever the tool\r\n\t\t * returned — shape is up to the tool. The parent verifies toolId\r\n\t\t * against the authorized externalTools list before executing.\r\n\t\t *\r\n\t\t * Use this when you need a pre-built business-logic tool (e.g.,\r\n\t\t * \"Calculate Shelf Life\") that takes structured input rather than SQL.\r\n\t\t * For SQL source tools, keep using ctx.query.\r\n\t\t *\r\n\t\t * runTool results do NOT auto-flow into executedQueries. If you want\r\n\t\t * the result visualised, register it via ctx.emit() after extracting\r\n\t\t * the array you want to chart.\r\n\t\t */\r\n\t\tasync runTool<T = any>(toolId: string, params: Record<string, any> = {}): Promise<T> {\r\n\t\t\tconst id = shortId();\r\n\t\t\tconst promise = new Promise<any>((resolveResult, rejectResult) => {\r\n\t\t\t\tpending.set(id, {\r\n\t\t\t\t\tresolve: (payload: any) => resolveResult(payload?.result ?? payload),\r\n\t\t\t\t\treject: rejectResult,\r\n\t\t\t\t\ttoolId,\r\n\t\t\t\t\tsql: `-- runTool: ${toolId}`,\r\n\t\t\t\t\tstartedAt: Date.now(),\r\n\t\t\t\t});\r\n\t\t\t});\r\n\t\t\tsend({ type: 'run_tool', id, toolId, params });\r\n\t\t\treturn promise;\r\n\t\t},\r\n\r\n\t\t/**\r\n\t\t * Register a computed dataset. Flows into executedQueries alongside\r\n\t\t * real SQL query results so component generation can render post-SQL\r\n\t\t * transforms (weighted averages, clustering, joins) as charts.\r\n\t\t */\r\n\t\temit(name: string, rows: any[], meta?: { description?: string }): void {\r\n\t\t\texecutedQueries.push({\r\n\t\t\t\tsourceId: `computed:${name}`,\r\n\t\t\t\tsourceName: meta?.description || name,\r\n\t\t\t\tsql: `-- computed dataset: ${name}`,\r\n\t\t\t\tdata: Array.isArray(rows) ? rows : [],\r\n\t\t\t\tcount: Array.isArray(rows) ? rows.length : 0,\r\n\t\t\t\texecutionTimeMs: 0,\r\n\t\t\t\tvirtual: true,\r\n\t\t\t});\r\n\t\t},\r\n\r\n\t\t/**\r\n\t\t * Forward a short progress message to the parent's StreamBuffer so the UI\r\n\t\t * continues to see per-script feedback during execution.\r\n\t\t */\r\n\t\tlog(chunk: string): void {\r\n\t\t\tsend({ type: 'stream', chunk });\r\n\t\t},\r\n\t};\r\n}\r\n\r\nfunction send(msg: ChildToParentMessage): void {\r\n\tprocess.stdout.write(encodeMessage(msg));\r\n}\r\n\r\nfunction sendError(message: string, phase: 'compile' | 'runtime', stack?: string): void {\r\n\tsend({ type: 'error', message, stack, phase });\r\n}\r\n\r\n/**\r\n * Reject every in-flight ctx.query / ctx.runTool with the same reason. Used when\r\n * the IPC channel goes away (stdin end/error) so a pending query fails fast\r\n * instead of hanging until the parent's wall-clock SIGKILL. Idempotent — a\r\n * second call finds the map already drained.\r\n */\r\nfunction rejectAllPending(reason: string): void {\r\n\tfor (const [id, entry] of Array.from(pending.entries())) {\r\n\t\tpending.delete(id);\r\n\t\tentry.reject(new Error(reason));\r\n\t}\r\n}\r\n\r\n// ============================================\r\n// stdin reader — parse NDJSON messages from the parent\r\n// ============================================\r\n\r\nconst splitter = new LineSplitter();\r\n\r\nprocess.stdin.setEncoding('utf-8');\r\nprocess.stdin.on('data', (chunk: string) => {\r\n\tlet lines: string[];\r\n\ttry {\r\n\t\tlines = splitter.push(chunk);\r\n\t} catch (err) {\r\n\t\t// Oversized parent message (#9) — abort cleanly rather than buffer to OOM.\r\n\t\tconst msg = err instanceof Error ? err.message : String(err);\r\n\t\trejectAllPending(msg);\r\n\t\tsendError(msg, 'runtime');\r\n\t\tprocess.exit(1);\r\n\t}\r\n\tfor (const line of lines) {\r\n\t\tlet msg: ParentToChildMessage;\r\n\t\ttry {\r\n\t\t\tmsg = JSON.parse(line);\r\n\t\t} catch {\r\n\t\t\tcontinue; // ignore malformed input\r\n\t\t}\r\n\t\thandleMessage(msg);\r\n\t}\r\n});\r\n\r\nprocess.stdin.on('end', () => {\r\n\t// Parent closed our stdin — no further query replies can arrive. Reject any\r\n\t// in-flight queries now so getData fails fast (and we report ERROR) instead\r\n\t// of awaiting a reply that will never come, until the parent's wall-clock kill.\r\n\trejectAllPending('Parent closed the IPC channel (stdin end) before a query completed.');\r\n});\r\n\r\nprocess.stdin.on('error', (err: Error) => {\r\n\trejectAllPending(`IPC channel error before a query completed: ${err.message}`);\r\n});\r\n\r\nfunction handleMessage(msg: ParentToChildMessage): void {\r\n\tswitch (msg.type) {\r\n\t\tcase 'init': {\r\n\t\t\tif (initResolve) {\r\n\t\t\t\tconst now = new Date(msg.now || new Date().toISOString());\r\n\t\t\t\tconst params = restoreParamTypes(msg.params || {}, msg.paramSchema);\r\n\t\t\t\tinitResolve({ params, now });\r\n\t\t\t\tinitResolve = null;\r\n\t\t\t}\r\n\t\t\tbreak;\r\n\t\t}\r\n\t\tcase 'query_result': {\r\n\t\t\tconst entry = pending.get(msg.id);\r\n\t\t\tif (entry) {\r\n\t\t\t\tpending.delete(msg.id);\r\n\t\t\t\tentry.resolve({ data: msg.data, count: msg.count, metadata: msg.metadata });\r\n\t\t\t}\r\n\t\t\tbreak;\r\n\t\t}\r\n\t\tcase 'run_tool_result': {\r\n\t\t\tconst entry = pending.get(msg.id);\r\n\t\t\tif (entry) {\r\n\t\t\t\tpending.delete(msg.id);\r\n\t\t\t\t// runTool's resolve unwraps payload.result — wrap here so the\r\n\t\t\t\t// same pending entry shape works for both query and runTool.\r\n\t\t\t\tentry.resolve({ result: msg.result });\r\n\t\t\t}\r\n\t\t\tbreak;\r\n\t\t}\r\n\t\tcase 'query_error': {\r\n\t\t\tconst entry = pending.get(msg.id);\r\n\t\t\tif (entry) {\r\n\t\t\t\tpending.delete(msg.id);\r\n\t\t\t\tentry.reject(new Error(msg.error));\r\n\t\t\t}\r\n\t\t\tbreak;\r\n\t\t}\r\n\t}\r\n}\r\n\r\n// ============================================\r\n// Main — import the user script and run getData\r\n// ============================================\r\n\r\n(async () => {\r\n\tlet getData: (ctx: any, params: any) => Promise<any>;\r\n\r\n\ttry {\r\n\t\t// Dynamic import — tsx transpiles the user .ts file on the fly\r\n\t\tconst mod = await import(pathToFileURL(SCRIPT_ARG).href);\r\n\t\tgetData = mod.getData ?? mod.default;\r\n\t\tif (typeof getData !== 'function') {\r\n\t\t\tthrow new Error(\r\n\t\t\t\t`Script at ${SCRIPT_ARG} does not export getData — expected \\`export async function getData(ctx, params)\\``,\r\n\t\t\t);\r\n\t\t}\r\n\t} catch (err) {\r\n\t\tconst e = err as Error;\r\n\t\tsendError(e.message || String(err), 'compile', e.stack);\r\n\t\tprocess.exit(1);\r\n\t\treturn;\r\n\t}\r\n\r\n\tconst init = await initPromise;\r\n\tconst ctx = buildCtx(init);\r\n\tconst startedAt = Date.now();\r\n\r\n\ttry {\r\n\t\tconst raw = await getData(ctx, init.params);\r\n\t\tconst executionTimeMs = Date.now() - startedAt;\r\n\r\n\t\tconst data = normalizeResultData(raw);\r\n\t\tconst count = typeof raw?.count === 'number' ? raw.count : data.length;\r\n\t\tconst sampleRows = data.slice(0, 5);\r\n\r\n\t\tconst result: ChildToParentMessage = {\r\n\t\t\ttype: 'result',\r\n\t\t\tdata,\r\n\t\t\tcount,\r\n\t\t\texecutedQueries,\r\n\t\t\texecutionTimeMs,\r\n\t\t\tsampleRows,\r\n\t\t};\r\n\t\tsend(result);\r\n\t\t// Give stdout a tick to flush before we exit\r\n\t\tprocess.stdout.once('drain', () => process.exit(0));\r\n\t\tsetImmediate(() => process.exit(0));\r\n\t} catch (err) {\r\n\t\tconst e = err as Error;\r\n\t\tsendError(e.message || String(err), 'runtime', e.stack);\r\n\t\tprocess.exit(1);\r\n\t}\r\n})();\r\n\r\n/**\r\n * Normalize whatever getData returned into an array. Mirrors the old\r\n * ScriptRunner semantics: direct array, { data: [] }, or { data: {...} }\r\n * wrapped as single-element array.\r\n */\r\nfunction normalizeResultData(raw: any): any[] {\r\n\tif (!raw) return [];\r\n\tif (Array.isArray(raw)) return raw;\r\n\tif (Array.isArray(raw.data)) return raw.data;\r\n\tif (raw.data && typeof raw.data === 'object') return [raw.data];\r\n\treturn [];\r\n}\r\n\r\n/**\r\n * Restore typed values that JSON stripped during IPC transport. Date and\r\n * date_range params arrive as ISO strings; re-wrap them so scripts can call\r\n * `.toISOString()` and other Date methods the same way they would in-process.\r\n */\r\nfunction restoreParamTypes(\r\n\tparams: Record<string, any>,\r\n\tschema?: Record<string, 'string' | 'number' | 'date' | 'date_range' | 'enum' | 'boolean'>,\r\n): Record<string, any> {\r\n\tif (!schema) return params;\r\n\tconst out = { ...params };\r\n\tfor (const [name, type] of Object.entries(schema)) {\r\n\t\tconst v = out[name];\r\n\t\tif (v === undefined || v === null) continue;\r\n\t\tif (type === 'date' && typeof v === 'string') {\r\n\t\t\tconst d = new Date(v);\r\n\t\t\tif (!isNaN(d.getTime())) out[name] = d;\r\n\t\t} else if (type === 'date_range' && typeof v === 'object') {\r\n\t\t\tconst from = v.from;\r\n\t\t\tconst to = v.to;\r\n\t\t\tout[name] = {\r\n\t\t\t\tfrom: typeof from === 'string' ? new Date(from) : from,\r\n\t\t\t\tto: typeof to === 'string' ? new Date(to) : to,\r\n\t\t\t};\r\n\t\t}\r\n\t}\r\n\treturn out;\r\n}\r\n","/**\r\n * script-ipc.ts — Protocol definitions for parent/child IPC.\r\n *\r\n * Transport: newline-delimited JSON over the child's stdin/stdout.\r\n * stderr is passed through for uncaught exceptions and compile errors.\r\n *\r\n * See backend/docs/SCRIPT-FLOW-IMPLEMENTATION.md § IPC Bridge.\r\n */\r\n\r\n// ============================================\r\n// Parent → Child\r\n// ============================================\r\n\r\nexport interface InitMessage {\r\n\ttype: 'init';\r\n\t/**\r\n\t * Params passed to getData(). Date/date_range values cross the IPC boundary\r\n\t * as ISO strings and are restored to Date objects in the child via paramSchema.\r\n\t */\r\n\tparams: Record<string, any>;\r\n\t/** Frozen timestamp for ctx.now — ISO string */\r\n\tnow: string;\r\n\t/**\r\n\t * Maps each parameter name to its declared type so the child can restore\r\n\t * Date objects (lost during JSON serialization).\r\n\t */\r\n\tparamSchema?: Record<string, 'string' | 'number' | 'date' | 'date_range' | 'enum' | 'boolean'>;\r\n}\r\n\r\nexport interface QueryResultMessage {\r\n\ttype: 'query_result';\r\n\t/** Matches the `id` from the child's QUERY message */\r\n\tid: string;\r\n\tdata: any[];\r\n\tcount: number;\r\n\tmetadata?: {\r\n\t\ttotalCount?: number;\r\n\t\texecutionTimeMs?: number;\r\n\t};\r\n}\r\n\r\nexport interface QueryErrorMessage {\r\n\ttype: 'query_error';\r\n\tid: string;\r\n\terror: string;\r\n}\r\n\r\n/**\r\n * Response to a `run_tool` request — carries whatever the direct-tool's fn()\r\n * returned. Shape is arbitrary; the script is expected to know what its chosen\r\n * tool yields and unwrap accordingly.\r\n */\r\nexport interface RunToolResultMessage {\r\n\ttype: 'run_tool_result';\r\n\tid: string;\r\n\tresult: any;\r\n}\r\n\r\nexport type ParentToChildMessage =\r\n\t| InitMessage\r\n\t| QueryResultMessage\r\n\t| QueryErrorMessage\r\n\t| RunToolResultMessage;\r\n\r\n// ============================================\r\n// Child → Parent\r\n// ============================================\r\n\r\nexport interface QueryMessage {\r\n\ttype: 'query';\r\n\t/** Short uuid — parent uses this to correlate its response */\r\n\tid: string;\r\n\ttoolId: string;\r\n\tsql: string;\r\n}\r\n\r\n/**\r\n * Run an arbitrary tool with structured params. Used by ctx.runTool() to call\r\n * direct tools (and any tool whose `fn(params)` doesn't expect a SQL string).\r\n * Reply is RunToolResultMessage or QueryErrorMessage.\r\n */\r\nexport interface RunToolMessage {\r\n\ttype: 'run_tool';\r\n\tid: string;\r\n\ttoolId: string;\r\n\tparams: Record<string, any>;\r\n}\r\n\r\nexport interface StreamMessage {\r\n\ttype: 'stream';\r\n\tchunk: string;\r\n}\r\n\r\nexport interface ExecutedQuery {\r\n\tsourceId: string;\r\n\tsourceName: string;\r\n\tsql: string;\r\n\tdata: any[];\r\n\tcount: number;\r\n\ttotalCount?: number;\r\n\texecutionTimeMs: number;\r\n\t/**\r\n\t * When the script registered a computed dataset via ctx.emit(), this is\r\n\t * marked virtual so component generation can distinguish it from a real SQL query.\r\n\t */\r\n\tvirtual?: boolean;\r\n}\r\n\r\nexport interface ResultMessage {\r\n\ttype: 'result';\r\n\tdata: any[];\r\n\tcount: number;\r\n\texecutedQueries: ExecutedQuery[];\r\n\texecutionTimeMs: number;\r\n\t/** First 5 rows of the final `data` — lets the parent judge intent-match cheaply */\r\n\tsampleRows: any[];\r\n}\r\n\r\nexport interface ErrorMessage {\r\n\ttype: 'error';\r\n\tmessage: string;\r\n\tstack?: string;\r\n\t/** \"compile\" → tsx couldn't parse; \"runtime\" → thrown during getData */\r\n\tphase?: 'compile' | 'runtime';\r\n}\r\n\r\nexport type ChildToParentMessage =\r\n\t| QueryMessage\r\n\t| RunToolMessage\r\n\t| StreamMessage\r\n\t| ResultMessage\r\n\t| ErrorMessage;\r\n\r\n// ============================================\r\n// Utilities\r\n// ============================================\r\n\r\n/**\r\n * Encode a message for transport: JSON + newline.\r\n */\r\nexport function encodeMessage(msg: ParentToChildMessage | ChildToParentMessage): string {\r\n\treturn JSON.stringify(msg) + '\\n';\r\n}\r\n\r\n/**\r\n * Thrown when a single NDJSON line grows past the byte ceiling without a\r\n * newline — a runaway writer or a pathologically large message. Callers abort\r\n * (kill the child / exit) instead of buffering toward OOM. See #9 in\r\n * backend/docs/SCRIPT-FLOW-SCALING-ISSUES.md.\r\n */\r\nexport class LineOverflowError extends Error {\r\n\tconstructor(public readonly buffered: number, public readonly limit: number) {\r\n\t\tsuper(`IPC message exceeded ${limit} bytes without a newline (${buffered} buffered) — aborting to avoid unbounded memory.`);\r\n\t\tthis.name = 'LineOverflowError';\r\n\t}\r\n}\r\n\r\n/** Default per-message ceiling — generous enough for large result sets, low\r\n * enough to catch a runaway/infinite writer. Override via SCRIPT_MAX_IPC_BYTES. */\r\nconst DEFAULT_MAX_IPC_BYTES = (() => {\r\n\tconst envVal = Number(process.env.SCRIPT_MAX_IPC_BYTES);\r\n\treturn Number.isFinite(envVal) && envVal > 0 ? Math.floor(envVal) : 64 * 1024 * 1024;\r\n})();\r\n\r\n/**\r\n * Split a stream of data chunks on newlines and emit each complete JSON line.\r\n * Partial lines are buffered until the next chunk arrives, up to `maxBytes` —\r\n * past which `push` throws `LineOverflowError` rather than grow unbounded.\r\n */\r\nexport class LineSplitter {\r\n\tprivate buffer = '';\r\n\r\n\tconstructor(private readonly maxBytes: number = DEFAULT_MAX_IPC_BYTES) {}\r\n\r\n\tpush(chunk: string): string[] {\r\n\t\tthis.buffer += chunk;\r\n\t\tconst lines: string[] = [];\r\n\t\tlet idx: number;\r\n\t\twhile ((idx = this.buffer.indexOf('\\n')) !== -1) {\r\n\t\t\tconst line = this.buffer.slice(0, idx);\r\n\t\t\tthis.buffer = this.buffer.slice(idx + 1);\r\n\t\t\tif (line.length > 0) lines.push(line);\r\n\t\t}\r\n\t\t// A partial line past the ceiling means no newline is coming (or the\r\n\t\t// message is pathologically large). Drop the buffer and signal abort.\r\n\t\tif (this.buffer.length > this.maxBytes) {\r\n\t\t\tconst buffered = this.buffer.length;\r\n\t\t\tthis.buffer = '';\r\n\t\t\tthrow new LineOverflowError(buffered, this.maxBytes);\r\n\t\t}\r\n\t\treturn lines;\r\n\t}\r\n\r\n\t/** Flush any remaining partial data (useful on stream close) */\r\n\tflush(): string | null {\r\n\t\tif (this.buffer.length === 0) return null;\r\n\t\tconst rest = this.buffer;\r\n\t\tthis.buffer = '';\r\n\t\treturn rest;\r\n\t}\r\n}\r\n\r\n/**\r\n * Generate a short correlation id for in-flight queries.\r\n * Collision within a single execution is astronomically unlikely.\r\n */\r\nexport function shortId(): string {\r\n\treturn Math.random().toString(36).slice(2, 10);\r\n}\r\n"],"mappings":"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","names":[]}
|