@superatomai/sdk-node 0.0.3-s → 0.0.4-cp

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.
@@ -7,8 +7,21 @@ var import_url = require("url");
7
7
  function encodeMessage(msg) {
8
8
  return JSON.stringify(msg) + "\n";
9
9
  }
10
+ var LineOverflowError = class extends Error {
11
+ constructor(buffered, limit) {
12
+ super(`IPC message exceeded ${limit} bytes without a newline (${buffered} buffered) \u2014 aborting to avoid unbounded memory.`);
13
+ this.buffered = buffered;
14
+ this.limit = limit;
15
+ this.name = "LineOverflowError";
16
+ }
17
+ };
18
+ var DEFAULT_MAX_IPC_BYTES = (() => {
19
+ const envVal = Number(process.env.SCRIPT_MAX_IPC_BYTES);
20
+ return Number.isFinite(envVal) && envVal > 0 ? Math.floor(envVal) : 64 * 1024 * 1024;
21
+ })();
10
22
  var LineSplitter = class {
11
- constructor() {
23
+ constructor(maxBytes = DEFAULT_MAX_IPC_BYTES) {
24
+ this.maxBytes = maxBytes;
12
25
  this.buffer = "";
13
26
  }
14
27
  push(chunk) {
@@ -20,6 +33,11 @@ var LineSplitter = class {
20
33
  this.buffer = this.buffer.slice(idx + 1);
21
34
  if (line.length > 0) lines.push(line);
22
35
  }
36
+ if (this.buffer.length > this.maxBytes) {
37
+ const buffered = this.buffer.length;
38
+ this.buffer = "";
39
+ throw new LineOverflowError(buffered, this.maxBytes);
40
+ }
23
41
  return lines;
24
42
  }
25
43
  /** Flush any remaining partial data (useful on stream close) */
@@ -30,8 +48,126 @@ var LineSplitter = class {
30
48
  return rest;
31
49
  }
32
50
  };
33
- function shortId() {
34
- return Math.random().toString(36).slice(2, 10);
51
+
52
+ // src/userResponse/scripts/fmt.ts
53
+ var DASH = "\u2014";
54
+ var DEFAULT_FMT_CONFIG = { locale: "en-IN", currency: "INR" };
55
+ var MONEY_TIERS = {
56
+ "en-IN": [[1e7, "Cr"], [1e5, "L"]],
57
+ "hi-IN": [[1e7, "Cr"], [1e5, "L"]]
58
+ };
59
+ var DEFAULT_MONEY_TIERS = [[1e9, "B"], [1e6, "M"]];
60
+ function moneyTiers(locale) {
61
+ return MONEY_TIERS[locale] ?? DEFAULT_MONEY_TIERS;
62
+ }
63
+ var COUNT_SCALE_MIN = 1e7;
64
+ function toNum(v) {
65
+ if (v === null || v === void 0 || v === "") return null;
66
+ const n = typeof v === "number" ? v : Number(v);
67
+ return Number.isFinite(n) ? n : null;
68
+ }
69
+ var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
70
+ var ISO_DATE = /^\d{4}-\d{2}-\d{2}([T ]|$)/;
71
+ function toDate(v) {
72
+ if (v === null || v === void 0 || v === "") return null;
73
+ if (v instanceof Date) return Number.isNaN(v.getTime()) ? null : v;
74
+ if (typeof v === "number") {
75
+ const d = new Date(v);
76
+ return Number.isNaN(d.getTime()) ? null : d;
77
+ }
78
+ if (typeof v === "string" && ISO_DATE.test(v.trim())) {
79
+ const d = new Date(v.trim());
80
+ return Number.isNaN(d.getTime()) ? null : d;
81
+ }
82
+ return null;
83
+ }
84
+ function makeFmt(cfg = {}) {
85
+ const locale = cfg.locale || DEFAULT_FMT_CONFIG.locale;
86
+ const currency = cfg.currency || DEFAULT_FMT_CONFIG.currency;
87
+ const group = (n, decimals = 0) => n.toLocaleString(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
88
+ const symbol = (() => {
89
+ try {
90
+ const parts = new Intl.NumberFormat(locale, { style: "currency", currency }).formatToParts(0);
91
+ return parts.find((p) => p.type === "currency")?.value ?? "";
92
+ } catch {
93
+ return "";
94
+ }
95
+ })();
96
+ const scaled = (abs, tiers, decimals) => {
97
+ for (const [threshold, unit] of tiers) {
98
+ if (abs >= threshold) return `${group(abs / threshold, decimals)} ${unit}`;
99
+ }
100
+ return null;
101
+ };
102
+ function money(v, decimals = 2) {
103
+ const n = toNum(v);
104
+ if (n === null) return DASH;
105
+ const sign = n < 0 ? "-" : "";
106
+ const abs = Math.abs(n);
107
+ const tiers = moneyTiers(locale);
108
+ for (let i = 0; i < tiers.length; i++) {
109
+ const [threshold, unit] = tiers[i];
110
+ if (abs < threshold) continue;
111
+ const value = Number((abs / threshold).toFixed(decimals));
112
+ const higher = tiers[i - 1];
113
+ if (higher && value * threshold >= higher[0]) {
114
+ return `${sign}${symbol}${group(abs / higher[0], decimals)} ${higher[1]}`;
115
+ }
116
+ return `${sign}${symbol}${group(abs / threshold, decimals)} ${unit}`;
117
+ }
118
+ try {
119
+ return new Intl.NumberFormat(locale, {
120
+ style: "currency",
121
+ currency,
122
+ minimumFractionDigits: Number.isInteger(n) ? 0 : decimals,
123
+ maximumFractionDigits: decimals
124
+ }).format(n);
125
+ } catch {
126
+ return `${sign}${symbol}${group(abs, Number.isInteger(abs) ? 0 : decimals)}`;
127
+ }
128
+ }
129
+ function num(v, decimals) {
130
+ const n = toNum(v);
131
+ if (n === null) return DASH;
132
+ return group(n, decimals ?? (Number.isInteger(n) ? 0 : 2));
133
+ }
134
+ function count(v, decimals = 2) {
135
+ const n = toNum(v);
136
+ if (n === null) return DASH;
137
+ const abs = Math.abs(n);
138
+ const s = abs >= COUNT_SCALE_MIN ? scaled(abs, moneyTiers(locale), decimals) : null;
139
+ if (s) return `${n < 0 ? "-" : ""}${s}`;
140
+ return group(n, Number.isInteger(n) ? 0 : decimals);
141
+ }
142
+ function pct(part, whole, decimals = 1) {
143
+ const p = toNum(part), w = toNum(whole);
144
+ if (p === null || w === null || w === 0) return DASH;
145
+ return `${group(p / w * 100, decimals)}%`;
146
+ }
147
+ function delta(current, previous, decimals = 1) {
148
+ const c = toNum(current), p = toNum(previous);
149
+ if (c === null || p === null || p === 0) return DASH;
150
+ const change = (c - p) / Math.abs(p) * 100;
151
+ return `${change >= 0 ? "+" : "-"}${group(Math.abs(change), decimals)}%`;
152
+ }
153
+ function date(v) {
154
+ const d = toDate(v);
155
+ if (!d) return DASH;
156
+ return `${d.getUTCDate()} ${MONTHS[d.getUTCMonth()]} ${d.getUTCFullYear()}`;
157
+ }
158
+ function range(from, to) {
159
+ const a = toDate(from), b = toDate(to);
160
+ if (!a || !b) return DASH;
161
+ const left = a.getUTCFullYear() === b.getUTCFullYear() ? `${a.getUTCDate()} ${MONTHS[a.getUTCMonth()]}` : date(a);
162
+ return `${left} \u2013 ${date(b)}`;
163
+ }
164
+ function list(items, conjunction = "and") {
165
+ const parts = (items || []).map((i) => String(i ?? "").trim()).filter(Boolean);
166
+ if (!parts.length) return DASH;
167
+ if (parts.length === 1) return parts[0];
168
+ return `${parts.slice(0, -1).join(", ")} ${conjunction} ${parts[parts.length - 1]}`;
169
+ }
170
+ return { money, num, count, pct, delta, date, range, list };
35
171
  }
36
172
 
37
173
  // src/userResponse/scripts/script-bootstrap.ts
@@ -40,8 +176,22 @@ if (!SCRIPT_ARG) {
40
176
  sendError("Bootstrap was invoked without a script path.", "compile");
41
177
  process.exit(1);
42
178
  }
43
- var pending = /* @__PURE__ */ new Map();
179
+ var BRIDGE_URL = process.env.SCRIPT_BRIDGE_URL;
44
180
  var executedQueries = [];
181
+ var toolIdCallCounts = {};
182
+ var PREVIEW_MAX_ROWS = 10;
183
+ var PREVIEW_MAX_CHARS = 200;
184
+ var QUERY_ROW_CAP = 250;
185
+ function previewRows(data) {
186
+ return data.slice(0, PREVIEW_MAX_ROWS).map((row) => {
187
+ if (!row || typeof row !== "object") return row;
188
+ const out = {};
189
+ for (const [k, v] of Object.entries(row)) {
190
+ out[k] = typeof v === "string" && v.length > PREVIEW_MAX_CHARS ? v.slice(0, PREVIEW_MAX_CHARS) + "\u2026" : v;
191
+ }
192
+ return out;
193
+ });
194
+ }
45
195
  var initResolve = null;
46
196
  var initPromise = new Promise((resolve) => {
47
197
  initResolve = resolve;
@@ -50,31 +200,85 @@ function buildCtx(init) {
50
200
  return {
51
201
  now: init.now,
52
202
  async query(toolId, sql) {
53
- const id = shortId();
203
+ if (!BRIDGE_URL) throw new Error("SCRIPT_BRIDGE_URL is not set \u2014 script bridge server is not running");
54
204
  const startedAt = Date.now();
55
- const promise = new Promise((resolve, reject) => {
56
- pending.set(id, { resolve, reject, toolId, sql, startedAt });
205
+ send({ type: "stream", chunk: `
206
+ \u{1F4DD} **Querying ${toolId}:**
207
+ \`\`\`sql
208
+ ${sql}
209
+ \`\`\`
210
+
211
+ ` });
212
+ send({ type: "stream", chunk: `__QUERY_TIMER_START_Executing query__` });
213
+ const res = await fetch(`${BRIDGE_URL}/query`, {
214
+ method: "POST",
215
+ headers: { "content-type": "application/json" },
216
+ body: JSON.stringify({ toolId, sql, limit: QUERY_ROW_CAP })
57
217
  });
58
- send({ type: "query", id, toolId, sql });
59
- const result = await promise;
218
+ const json = await res.json();
60
219
  const executionTimeMs = Date.now() - startedAt;
220
+ send({ type: "stream", chunk: `__QUERY_TIMER_DONE_${(executionTimeMs / 1e3).toFixed(1)}__
221
+
222
+ ` });
223
+ if (!res.ok || !json.success) {
224
+ const errMsg = json.error || `Bridge query failed (${res.status})`;
225
+ send({ type: "stream", chunk: `\u274C **Query failed on ${toolId}:** ${errMsg}
226
+
227
+ ` });
228
+ throw new Error(errMsg);
229
+ }
230
+ let truncated = false;
231
+ let totalCount;
232
+ if (json.metadata?.truncated) {
233
+ truncated = true;
234
+ totalCount = typeof json.metadata.totalCount === "number" ? json.metadata.totalCount : void 0;
235
+ const total = json.metadata.totalCount ?? "more";
236
+ const hasGroupBy = /\bGROUP\s+BY\b/i.test(sql);
237
+ const explicitLimitMatch = sql.match(/\bLIMIT\s+(\d+)/i);
238
+ const hasDeliberateLimit = explicitLimitMatch != null && Number(explicitLimitMatch[1]) < QUERY_ROW_CAP;
239
+ if (hasGroupBy) {
240
+ send({ type: "stream", chunk: `\u2139\uFE0F Only the top ${QUERY_ROW_CAP} of ${total} grouped rows are returned (query is already aggregated via GROUP BY) \u2014 expected for a top-N request.
241
+
242
+ ` });
243
+ } else if (hasDeliberateLimit) {
244
+ send({ type: "stream", chunk: `\u2139\uFE0F Only the ${explicitLimitMatch[1]} rows requested (of ${total} matching) are returned \u2014 expected for a top-N request with an explicit LIMIT.
245
+
246
+ ` });
247
+ } else {
248
+ const msg = `ctx.query truncated the result of this query to ${QUERY_ROW_CAP} rows (it matched ${total}). Do NOT compute over raw rows \u2014 AGGREGATE IN SQL (GROUP BY / mode() / mode(...) FILTER (WHERE \u2026) / COUNT(...) FILTER (\u2026) / CASE) so the query returns the final, already-reduced rows, then have getData return those.`;
249
+ send({ type: "stream", chunk: `\u274C **Result truncated on ${toolId}:** ${msg}
250
+
251
+ ` });
252
+ throw new Error(msg);
253
+ }
254
+ }
255
+ const data = json.data ?? [];
256
+ const count = json.count ?? data.length;
257
+ send({ type: "stream", chunk: `\u2705 **${count} rows from ${toolId}**
258
+
259
+ ` });
260
+ if (data.length > 0) {
261
+ send({ type: "stream", chunk: `<DataTable>${JSON.stringify(previewRows(data))}</DataTable>
262
+
263
+ ` });
264
+ }
265
+ const callNumber = toolIdCallCounts[toolId] = (toolIdCallCounts[toolId] ?? 0) + 1;
266
+ const sourceId = callNumber === 1 ? toolId : `${toolId}#${callNumber}`;
61
267
  executedQueries.push({
62
- sourceId: toolId,
268
+ sourceId,
63
269
  sourceName: toolId,
64
- // parent overwrites with the real name
270
+ // enriched by runner after result arrives
65
271
  sql,
66
- data: result.data,
67
- count: result.count,
68
- totalCount: result.metadata?.totalCount,
272
+ data,
273
+ count,
69
274
  executionTimeMs
70
275
  });
71
- return { data: result.data, count: result.count };
276
+ return { data, count, truncated, totalCount };
72
277
  },
73
278
  /**
74
279
  * Call a tool with structured params (typically a direct tool whose
75
280
  * fn(params) does not expect a SQL string). Returns whatever the tool
76
- * returned — shape is up to the tool. The parent verifies toolId
77
- * against the authorized externalTools list before executing.
281
+ * returned — shape is up to the tool.
78
282
  *
79
283
  * Use this when you need a pre-built business-logic tool (e.g.,
80
284
  * "Calculate Shelf Life") that takes structured input rather than SQL.
@@ -85,18 +289,15 @@ function buildCtx(init) {
85
289
  * the array you want to chart.
86
290
  */
87
291
  async runTool(toolId, params = {}) {
88
- const id = shortId();
89
- const promise = new Promise((resolveResult, rejectResult) => {
90
- pending.set(id, {
91
- resolve: (payload) => resolveResult(payload?.result ?? payload),
92
- reject: rejectResult,
93
- toolId,
94
- sql: `-- runTool: ${toolId}`,
95
- startedAt: Date.now()
96
- });
292
+ if (!BRIDGE_URL) throw new Error("SCRIPT_BRIDGE_URL is not set \u2014 script bridge server is not running");
293
+ const res = await fetch(`${BRIDGE_URL}/run-tool`, {
294
+ method: "POST",
295
+ headers: { "content-type": "application/json" },
296
+ body: JSON.stringify({ toolId, params })
97
297
  });
98
- send({ type: "run_tool", id, toolId, params });
99
- return promise;
298
+ const json = await res.json();
299
+ if (!res.ok || !json.success) throw new Error(json.error || `Bridge run-tool failed (${res.status})`);
300
+ return json.result;
100
301
  },
101
302
  /**
102
303
  * Register a computed dataset. Flows into executedQueries alongside
@@ -123,8 +324,33 @@ function buildCtx(init) {
123
324
  }
124
325
  };
125
326
  }
327
+ function guardCtx(ctx) {
328
+ const PASSTHROUGH = /* @__PURE__ */ new Set([
329
+ "then",
330
+ "catch",
331
+ "finally",
332
+ "constructor",
333
+ "toJSON",
334
+ "inspect",
335
+ "valueOf",
336
+ "toString",
337
+ "hasOwnProperty",
338
+ "nodeType"
339
+ ]);
340
+ return new Proxy(ctx, {
341
+ get(target, prop, receiver) {
342
+ if (typeof prop === "symbol" || prop in target || PASSTHROUGH.has(prop)) {
343
+ return Reflect.get(target, prop, receiver);
344
+ }
345
+ const available = Reflect.ownKeys(target).filter((k) => typeof k === "string").join(", ");
346
+ throw new TypeError(
347
+ `ctx.${String(prop)} does not exist. ctx has exactly: ${available}. There are no formatting/parsing/date helpers on ctx \u2014 interpolate ISO date strings straight into the SQL and do date arithmetic in SQL.`
348
+ );
349
+ }
350
+ });
351
+ }
126
352
  function send(msg) {
127
- process.stdout.write(encodeMessage(msg));
353
+ return process.stdout.write(encodeMessage(msg));
128
354
  }
129
355
  function sendError(message, phase, stack) {
130
356
  send({ type: "error", message, stack, phase });
@@ -132,7 +358,15 @@ function sendError(message, phase, stack) {
132
358
  var splitter = new LineSplitter();
133
359
  process.stdin.setEncoding("utf-8");
134
360
  process.stdin.on("data", (chunk) => {
135
- for (const line of splitter.push(chunk)) {
361
+ let lines;
362
+ try {
363
+ lines = splitter.push(chunk);
364
+ } catch (err) {
365
+ const msg = err instanceof Error ? err.message : String(err);
366
+ sendError(msg, "runtime");
367
+ process.exit(1);
368
+ }
369
+ for (const line of lines) {
136
370
  let msg;
137
371
  try {
138
372
  msg = JSON.parse(line);
@@ -142,55 +376,71 @@ process.stdin.on("data", (chunk) => {
142
376
  handleMessage(msg);
143
377
  }
144
378
  });
145
- process.stdin.on("end", () => {
146
- });
147
379
  function handleMessage(msg) {
148
380
  switch (msg.type) {
149
381
  case "init": {
150
382
  if (initResolve) {
151
383
  const now = new Date(msg.now || (/* @__PURE__ */ new Date()).toISOString());
152
384
  const params = restoreParamTypes(msg.params || {}, msg.paramSchema);
153
- initResolve({ params, now });
385
+ initResolve({ params, now, mode: msg.mode, data: msg.data, datasets: msg.datasets, fmt: msg.fmt, entities: msg.entities });
154
386
  initResolve = null;
155
387
  }
156
388
  break;
157
389
  }
158
- case "query_result": {
159
- const entry = pending.get(msg.id);
160
- if (entry) {
161
- pending.delete(msg.id);
162
- entry.resolve({ data: msg.data, count: msg.count, metadata: msg.metadata });
163
- }
164
- break;
390
+ }
391
+ }
392
+ function collectDatasets(queries) {
393
+ const out = {};
394
+ for (const q of queries) {
395
+ if (q.virtual && q.sourceId.startsWith("computed:")) {
396
+ out[q.sourceId.slice("computed:".length)] = q.data;
165
397
  }
166
- case "run_tool_result": {
167
- const entry = pending.get(msg.id);
168
- if (entry) {
169
- pending.delete(msg.id);
170
- entry.resolve({ result: msg.result });
171
- }
172
- break;
398
+ }
399
+ return out;
400
+ }
401
+ async function runComponents(mod, data, params, fmtCfg, datasets = {}, entities = []) {
402
+ const getComponents = mod?.getComponents ?? mod?.default?.getComponents;
403
+ if (typeof getComponents !== "function") return {};
404
+ try {
405
+ const out = await getComponents(data, params, makeFmt(fmtCfg), datasets, entities);
406
+ if (!Array.isArray(out)) {
407
+ return { componentsError: "getComponents must return an ARRAY of component specs" };
173
408
  }
174
- case "query_error": {
175
- const entry = pending.get(msg.id);
176
- if (entry) {
177
- pending.delete(msg.id);
178
- entry.reject(new Error(msg.error));
179
- }
180
- break;
409
+ if (out.some((c) => !c || typeof c !== "object")) {
410
+ return { componentsError: "every entry getComponents returns must be a component spec object" };
411
+ }
412
+ return { components: out };
413
+ } catch (err) {
414
+ const e = err;
415
+ return { componentsError: e?.message || String(err) };
416
+ }
417
+ }
418
+ async function runAnalysis(mod, data, params, fmtCfg, datasets = {}, entities = []) {
419
+ const getAnalysis = mod?.getAnalysis ?? mod?.default?.getAnalysis;
420
+ if (typeof getAnalysis !== "function") return {};
421
+ try {
422
+ const out = await getAnalysis(data, params, makeFmt(fmtCfg), datasets, entities);
423
+ if (!out || typeof out !== "object") {
424
+ return { analysisError: "getAnalysis must return an object with { analysis, summary, method }" };
181
425
  }
426
+ return {
427
+ analysis: {
428
+ analysis: typeof out.analysis === "string" ? out.analysis : void 0,
429
+ summary: Array.isArray(out.summary) ? out.summary.map((s) => String(s)) : void 0,
430
+ method: typeof out.method === "string" ? out.method : void 0
431
+ }
432
+ };
433
+ } catch (err) {
434
+ const e = err;
435
+ return { analysisError: e?.message || String(err) };
182
436
  }
183
437
  }
184
438
  (async () => {
439
+ let mod;
185
440
  let getData;
186
441
  try {
187
- const mod = await import((0, import_url.pathToFileURL)(SCRIPT_ARG).href);
188
- getData = mod.getData ?? mod.default;
189
- if (typeof getData !== "function") {
190
- throw new Error(
191
- `Script at ${SCRIPT_ARG} does not export getData \u2014 expected \`export async function getData(ctx, params)\``
192
- );
193
- }
442
+ mod = await import((0, import_url.pathToFileURL)(SCRIPT_ARG).href);
443
+ getData = mod.getData ?? (typeof mod.default === "function" ? mod.default : mod.default?.getData);
194
444
  } catch (err) {
195
445
  const e = err;
196
446
  sendError(e.message || String(err), "compile", e.stack);
@@ -198,7 +448,33 @@ function handleMessage(msg) {
198
448
  return;
199
449
  }
200
450
  const init = await initPromise;
201
- const ctx = buildCtx(init);
451
+ if (init.mode === "analysis" || init.mode === "components") {
452
+ const started = Date.now();
453
+ const rows = Array.isArray(init.data) ? init.data : [];
454
+ const ds = init.datasets ?? {};
455
+ const authored = init.mode === "analysis" ? await runAnalysis(mod, rows, init.params, init.fmt, ds, init.entities) : await runComponents(mod, rows, init.params, init.fmt, ds, init.entities);
456
+ const flushedAuthored = send({
457
+ type: "result",
458
+ data: [],
459
+ count: rows.length,
460
+ executedQueries: [],
461
+ executionTimeMs: Date.now() - started,
462
+ sampleRows: [],
463
+ ...authored
464
+ });
465
+ if (flushedAuthored) process.exit(0);
466
+ else process.stdout.once("drain", () => process.exit(0));
467
+ return;
468
+ }
469
+ if (typeof getData !== "function") {
470
+ sendError(
471
+ `Script at ${SCRIPT_ARG} does not export getData \u2014 expected \`export async function getData(ctx, params)\``,
472
+ "compile"
473
+ );
474
+ process.exit(1);
475
+ return;
476
+ }
477
+ const ctx = guardCtx(buildCtx(init));
202
478
  const startedAt = Date.now();
203
479
  try {
204
480
  const raw = await getData(ctx, init.params);
@@ -206,17 +482,27 @@ function handleMessage(msg) {
206
482
  const data = normalizeResultData(raw);
207
483
  const count = typeof raw?.count === "number" ? raw.count : data.length;
208
484
  const sampleRows = data.slice(0, 5);
485
+ const ds = collectDatasets(executedQueries);
486
+ const { analysis, analysisError } = await runAnalysis(mod, data, init.params, init.fmt, ds, init.entities);
487
+ const { components, componentsError } = await runComponents(mod, data, init.params, init.fmt, ds, init.entities);
209
488
  const result = {
210
489
  type: "result",
211
490
  data,
212
491
  count,
213
492
  executedQueries,
214
493
  executionTimeMs,
215
- sampleRows
494
+ sampleRows,
495
+ analysis,
496
+ analysisError,
497
+ components,
498
+ componentsError
216
499
  };
217
- send(result);
218
- process.stdout.once("drain", () => process.exit(0));
219
- setImmediate(() => process.exit(0));
500
+ const flushed = send(result);
501
+ if (flushed) {
502
+ process.exit(0);
503
+ } else {
504
+ process.stdout.once("drain", () => process.exit(0));
505
+ }
220
506
  } catch (err) {
221
507
  const e = err;
222
508
  sendError(e.message || String(err), "runtime", e.stack);