inline.ts101 lines · main
1import { resolve } from 'node:path';
2import { pathToFileURL } from 'node:url';
3
4import { newId } from '@briven/shared';
5
6import { withProjectTx } from '../db.js';
7import { createLogCollector, installConsolePatch, runWithCollector } from '../log-collector.js';
8import { publishInvocation } from '../log-publisher.js';
9import { makeCtx } from '../query-builder.js';
10import type { Bundle, InvokeRequest, InvokeResult } from '../types.js';
11
12// One-time install at module load — no-op on re-import.
13installConsolePatch();
14
15/**
16 * Phase 1 executor — runs user code inline in the runtime host process,
17 * inside a per-invoke transaction scoped to the project's data-plane
18 * schema.
19 *
20 * **NOT isolated.** Dogfood-only shortcut per BUILD_PLAN Phase 1 week 5-6.
21 * Flip BRIVEN_RUNTIME_EXECUTOR=deno once that executor lands. Do NOT use
22 * `inline` in any environment that accepts external traffic.
23 *
24 * Per-invocation, we create a LogCollector, run the user function inside
25 * an AsyncLocalStorage binding so both `ctx.log.*` and `console.*` route
26 * through it, then publish a single structured envelope to Redis for
27 * `briven logs --tail` and the log-fanout worker.
28 */
29export async function invokeInline(bundle: Bundle, request: InvokeRequest): Promise<InvokeResult> {
30 if (!bundle.functionNames.includes(request.functionName)) {
31 return {
32 ok: false,
33 code: 'function_not_found',
34 message: `function '${request.functionName}' not found in deployment ${bundle.deploymentId}`,
35 durationMs: 0,
36 };
37 }
38
39 const modPath = resolve(bundle.directory, 'functions', `${request.functionName}.ts`);
40 const started = performance.now();
41 const collector = createLogCollector();
42 const invocationId = newId('inv');
43
44 let result: InvokeResult;
45 try {
46 const mod = await runWithCollector(collector, () => import(pathToFileURL(modPath).href));
47 const fn = mod[request.functionName] ?? mod.default;
48 if (typeof fn !== 'function') {
49 result = {
50 ok: false,
51 code: 'function_not_exported',
52 message: `module did not export '${request.functionName}' or default`,
53 durationMs: Math.round(performance.now() - started),
54 };
55 } else {
56 const { value, touched } = await withProjectTx(request.projectId, async (tx) => {
57 const { ctx, touched } = makeCtx(tx, {
58 requestId: request.requestId,
59 auth: request.auth,
60 env: request.env,
61 log: collector,
62 });
63 const v = await runWithCollector(collector, () => fn(ctx, request.args));
64 return { value: v, touched };
65 });
66 result = {
67 ok: true,
68 value,
69 durationMs: Math.round(performance.now() - started),
70 touchedTables: [...touched],
71 };
72 }
73 } catch (err) {
74 result = {
75 ok: false,
76 code: 'function_threw',
77 // Never leak stack traces to the caller — only the type+message.
78 message: err instanceof Error ? err.message : String(err),
79 durationMs: Math.round(performance.now() - started),
80 };
81 }
82
83 // Publish the envelope regardless of success — failures are the thing
84 // `briven logs --tail` users most want to see.
85 const userLogs = collector.drain();
86 await publishInvocation({
87 projectId: request.projectId,
88 deploymentId: request.deploymentId,
89 invocationId,
90 functionName: request.functionName,
91 status: result.ok ? 'ok' : 'err',
92 durationMs: result.durationMs,
93 touchedTables: result.ok ? result.touchedTables : (result.touchedTables ?? []),
94 userLogs,
95 errCode: result.ok ? undefined : result.code,
96 errMessage: result.ok ? undefined : result.message,
97 ts: new Date().toISOString(),
98 });
99
100 return result;
101}