convex-rust 0.0.0-stage → 0.1.1

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.
Files changed (90) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +137 -2
  3. package/dist/cli/bin.d.ts +2 -0
  4. package/dist/cli/bin.js +200 -0
  5. package/dist/cli/build.d.ts +146 -0
  6. package/dist/cli/build.js +902 -0
  7. package/dist/cli/demangle.d.ts +7 -0
  8. package/dist/cli/demangle.js +328 -0
  9. package/dist/cli/diagnostics.d.ts +34 -0
  10. package/dist/cli/diagnostics.js +296 -0
  11. package/dist/cli/generate.d.ts +29 -0
  12. package/dist/cli/generate.js +82 -0
  13. package/dist/cli/manifest.d.ts +24 -0
  14. package/dist/cli/manifest.js +75 -0
  15. package/dist/cli/rustApi.d.ts +78 -0
  16. package/dist/cli/rustApi.js +609 -0
  17. package/dist/cli/scaffold.d.ts +22 -0
  18. package/dist/cli/scaffold.js +114 -0
  19. package/dist/cli/shim.d.ts +30 -0
  20. package/dist/cli/shim.js +140 -0
  21. package/dist/cli/size.d.ts +17 -0
  22. package/dist/cli/size.js +73 -0
  23. package/dist/cli/split.d.ts +20 -0
  24. package/dist/cli/split.js +140 -0
  25. package/dist/cli/symbols.d.ts +30 -0
  26. package/dist/cli/symbols.js +133 -0
  27. package/dist/cli/toolchain.d.ts +53 -0
  28. package/dist/cli/toolchain.js +167 -0
  29. package/dist/cli/validators.d.ts +6 -0
  30. package/dist/cli/validators.js +66 -0
  31. package/dist/cli/wasisdk.d.ts +82 -0
  32. package/dist/cli/wasisdk.js +220 -0
  33. package/dist/cli/wasmHints.d.ts +29 -0
  34. package/dist/cli/wasmHints.js +172 -0
  35. package/dist/runtime/base64.d.ts +4 -0
  36. package/dist/runtime/base64.js +77 -0
  37. package/dist/runtime/crypto.d.ts +7 -0
  38. package/dist/runtime/crypto.js +421 -0
  39. package/dist/runtime/fastjson.d.ts +3 -0
  40. package/dist/runtime/fastjson.js +170 -0
  41. package/dist/runtime/host.d.ts +73 -0
  42. package/dist/runtime/host.js +1230 -0
  43. package/dist/runtime/http.d.ts +40 -0
  44. package/dist/runtime/http.js +157 -0
  45. package/dist/runtime/index.d.ts +2 -0
  46. package/dist/runtime/index.js +1 -0
  47. package/dist/runtime/intl.d.ts +5 -0
  48. package/dist/runtime/intl.js +69 -0
  49. package/dist/runtime/tmpfs.d.ts +129 -0
  50. package/dist/runtime/tmpfs.js +624 -0
  51. package/dist/runtime/wasi.d.ts +33 -0
  52. package/dist/runtime/wasi.js +216 -0
  53. package/package.json +66 -4
  54. package/rust/convex_fn/Cargo.toml +32 -0
  55. package/rust/convex_fn/LICENSE +201 -0
  56. package/rust/convex_fn/build.rs +3 -0
  57. package/rust/convex_fn/src/args.rs +416 -0
  58. package/rust/convex_fn/src/crypto.rs +237 -0
  59. package/rust/convex_fn/src/ctx.rs +1044 -0
  60. package/rust/convex_fn/src/dns.rs +356 -0
  61. package/rust/convex_fn/src/error.rs +179 -0
  62. package/rust/convex_fn/src/executor.rs +577 -0
  63. package/rust/convex_fn/src/fanout.rs +106 -0
  64. package/rust/convex_fn/src/fs.rs +19 -0
  65. package/rust/convex_fn/src/function_ref.rs +219 -0
  66. package/rust/convex_fn/src/http.rs +441 -0
  67. package/rust/convex_fn/src/intl.rs +75 -0
  68. package/rust/convex_fn/src/lib.rs +242 -0
  69. package/rust/convex_fn/src/logging.rs +222 -0
  70. package/rust/convex_fn/src/native.rs +360 -0
  71. package/rust/convex_fn/src/net.rs +122 -0
  72. package/rust/convex_fn/src/sys.rs +178 -0
  73. package/rust/convex_fn/src/values.rs +121 -0
  74. package/rust/convex_fn/src/wire.rs +579 -0
  75. package/rust/convex_fn_macros/Cargo.toml +13 -0
  76. package/rust/convex_fn_macros/LICENSE +201 -0
  77. package/rust/convex_fn_macros/src/derive.rs +632 -0
  78. package/rust/convex_fn_macros/src/lib.rs +338 -0
  79. package/rust/convex_reqwest/Cargo.toml +69 -0
  80. package/rust/convex_reqwest/LICENSE +201 -0
  81. package/rust/convex_reqwest/src/blocking.rs +287 -0
  82. package/rust/convex_reqwest/src/body.rs +152 -0
  83. package/rust/convex_reqwest/src/client.rs +359 -0
  84. package/rust/convex_reqwest/src/config_types.rs +150 -0
  85. package/rust/convex_reqwest/src/error.rs +241 -0
  86. package/rust/convex_reqwest/src/into_url.rs +57 -0
  87. package/rust/convex_reqwest/src/lib.rs +42 -0
  88. package/rust/convex_reqwest/src/multipart.rs +140 -0
  89. package/rust/convex_reqwest/src/request.rs +358 -0
  90. package/rust/convex_reqwest/src/response.rs +310 -0
@@ -0,0 +1,1230 @@
1
+ // JS host for the guest ABI v1 (contracts/ABI.md), running inside a normal
2
+ // Convex action (default runtime, JSPI). See contracts/V8HOST.md.
3
+ import { ConvexError, convexToJson } from "convex/values";
4
+ import { makeFunctionReference } from "convex/server";
5
+ import { decodeBase64, encodeBase64 } from "./base64.js";
6
+ import { cryptoOp } from "./crypto.js";
7
+ import { parseConvexValue, stringifyConvex } from "./fastjson.js";
8
+ import { LineBuffer, makeWasi, ENOSYS, ProcExit } from "./wasi.js";
9
+ import { intlOp } from "./intl.js";
10
+ import { TmpFs, DEFAULT_TMP_BYTES } from "./tmpfs.js";
11
+ import { HttpSink, bufferedResponse, requestHead } from "./http.js";
12
+ export const PANIC_LOG_PREFIX = "Uncaught panic: ";
13
+ const MAX_OUTSTANDING_TASKS = 1000;
14
+ const MAX_LOG_MESSAGE_BYTES = 32 * 1024;
15
+ const MAX_RESULT_BYTES = 16 * 1024 * 1024 + 64 * 1024;
16
+ const STREAM_COALESCE_BYTES = 1024 * 1024;
17
+ const STREAM_PASSTHROUGH_BYTES = 8 * 1024;
18
+ const MAX_TASK_REQUEST_BYTES = 64 * 1024 * 1024;
19
+ /** Largest single `{ptr,len}` body (fetch / storageStore): one JS ArrayBuffer; bigger bodies stream. */
20
+ const MAX_BODY_REF_BYTES = 64 * 1024 * 1024;
21
+ /** Bytes a streamed request body buffers before `net/body_write` suspends the guest (backpressure). */
22
+ const REQUEST_BODY_HWM = 1024 * 1024;
23
+ const textEncoder = new TextEncoder();
24
+ const textDecoder = new TextDecoder();
25
+ const SCRATCH_MAX_BYTES = 1024 * 1024;
26
+ let scratch = new Uint8Array(4096);
27
+ function pack(ptr, len) {
28
+ return (BigInt(ptr >>> 0) << 32n) | BigInt(len >>> 0);
29
+ }
30
+ function unpack(v) {
31
+ const u = BigInt.asUintN(64, v);
32
+ return [Number(u >> 32n), Number(u & 0xffffffffn)];
33
+ }
34
+ function truncateUtf8(s, max) {
35
+ if (s.length <= max / 4)
36
+ return s;
37
+ const b = textEncoder.encode(s);
38
+ if (b.length <= max)
39
+ return s;
40
+ let cut = max;
41
+ while (cut > 0 && (b[cut] & 0xc0) === 0x80)
42
+ cut--;
43
+ return textDecoder.decode(b.subarray(0, cut));
44
+ }
45
+ function errorJson(e) {
46
+ // `instanceof` fails when two copies of `convex` are bundled; accept the duck type too.
47
+ if (e instanceof ConvexError || (e?.name === "ConvexError" && "data" in e)) {
48
+ const data = e.data;
49
+ return {
50
+ message: typeof e.message === "string" ? e.message : String(data),
51
+ data: convexToJson(data === undefined ? null : data),
52
+ };
53
+ }
54
+ const message = e && typeof e.message === "string" ? e.message : String(e);
55
+ return { message };
56
+ }
57
+ function isBodyRef(b) {
58
+ return !!b && typeof b === "object" && Number.isInteger(b.ptr) && Number.isInteger(b.len);
59
+ }
60
+ function toConvexJson(v) {
61
+ return convexToJson((v === undefined ? null : v));
62
+ }
63
+ const TO_REFERENCE_PATH = Symbol.for("toReferencePath");
64
+ /**
65
+ * The function argument for ctx.run* / scheduler from a syscall's args, as `getFunctionAddress`
66
+ * in `convex/server` reads it: `{functionHandle}` (a `function://` string), `{reference}` (a
67
+ * component function, carried under `Symbol.for("toReferencePath")`) or `{name}` (a path).
68
+ */
69
+ export function functionRef(syscall, args) {
70
+ if (typeof args?.functionHandle === "string")
71
+ return args.functionHandle;
72
+ if (typeof args?.reference === "string")
73
+ return { [TO_REFERENCE_PATH]: args.reference };
74
+ if (typeof args?.name !== "string") {
75
+ throw new Error(`${syscall}: missing \`name\`, \`functionHandle\` or \`reference\``);
76
+ }
77
+ return makeFunctionReference(args.name);
78
+ }
79
+ /** Rebuilds a vector filter from its wire expressions (`$eq` / `$or`) with the public builder. */
80
+ export function vectorFilter(q, e) {
81
+ if (e && Array.isArray(e.$or))
82
+ return q.or(...e.$or.map((x) => vectorFilter(q, x)));
83
+ const eq = e?.$eq;
84
+ if (Array.isArray(eq) && typeof eq[0]?.$field === "string") {
85
+ return q.eq(eq[0].$field, parseConvexValue(eq[1]));
86
+ }
87
+ throw new Error(`vectorSearch: unsupported filter expression ${JSON.stringify(e)}`);
88
+ }
89
+ /** Sync syscall `1.0/componentArgument`: `{value}` (Convex JSON) or `{}` when unset. */
90
+ function componentArgument(args) {
91
+ if (typeof args?.name !== "string") {
92
+ return { error: { message: "componentArgument: missing `name`" } };
93
+ }
94
+ const convex = globalThis.Convex;
95
+ if (typeof convex?.syscall !== "function") {
96
+ return { error: { message: "componentArgument: not available in this runtime" } };
97
+ }
98
+ try {
99
+ const out = convex.syscall("1.0/componentArgument", JSON.stringify({ name: args.name }));
100
+ const r = typeof out === "string" ? JSON.parse(out) : out;
101
+ return { ok: r?.value ?? null };
102
+ }
103
+ catch (e) {
104
+ return { error: errorJson(e) };
105
+ }
106
+ }
107
+ function webAssembly() {
108
+ return globalThis.WebAssembly;
109
+ }
110
+ const moduleInfo = new WeakMap();
111
+ function inspectModule(module) {
112
+ let info = moduleInfo.get(module);
113
+ if (info)
114
+ return info;
115
+ const wasiFns = [];
116
+ const unsupported = [];
117
+ for (const imp of webAssembly().Module.imports(module)) {
118
+ if (imp.module === "wasi_snapshot_preview1" && imp.kind === "function") {
119
+ wasiFns.push(imp.name);
120
+ }
121
+ else if (imp.module !== "convex") {
122
+ unsupported.push(`${imp.module}.${imp.name}`);
123
+ }
124
+ }
125
+ if (unsupported.length > 0) {
126
+ throw new Error(`The "use rust" module has unsupported imports: ${unsupported.join(", ")}`);
127
+ }
128
+ info = { wasiFns };
129
+ moduleInfo.set(module, info);
130
+ return info;
131
+ }
132
+ function parseExportName(exportName) {
133
+ // convex_run:<modulePath>:<name>, modulePath = crate::a::b
134
+ const rest = exportName.startsWith("convex_run:")
135
+ ? exportName.slice("convex_run:".length)
136
+ : exportName;
137
+ const i = rest.lastIndexOf(":");
138
+ const modulePath = i < 0 ? rest : rest.slice(0, i);
139
+ const name = i < 0 ? rest : rest.slice(i + 1);
140
+ const segs = modulePath.split("::").slice(1);
141
+ return { module: `rust/${segs.join("/")}.js`, name };
142
+ }
143
+ /** Reuse is dropped when the guest's linear memory grows past this (wasm linear memory never shrinks). */
144
+ const MAX_REUSE_MEMORY_BYTES = 128 * 1024 * 1024;
145
+ const MAX_REUSE_CALLS = 10_000;
146
+ /** A guest-fed request body: `net/body_write` enqueues, backpressure suspends the writer. */
147
+ class RequestBody {
148
+ stream;
149
+ controller;
150
+ closed = false;
151
+ waiters = [];
152
+ constructor() {
153
+ this.stream = new ReadableStream({
154
+ start: (c) => {
155
+ this.controller = c;
156
+ },
157
+ pull: () => this.release(),
158
+ cancel: () => {
159
+ this.closed = true;
160
+ this.release();
161
+ },
162
+ }, { highWaterMark: REQUEST_BODY_HWM, size: (c) => c?.byteLength ?? 0 });
163
+ }
164
+ release() {
165
+ const w = this.waiters;
166
+ this.waiters = [];
167
+ for (const f of w)
168
+ f();
169
+ }
170
+ /** Resolves once the stream can take more data. */
171
+ write(data) {
172
+ if (this.closed)
173
+ throw new Error("request body is closed (the request finished or was cancelled)");
174
+ this.controller.enqueue(data);
175
+ if ((this.controller.desiredSize ?? 1) > 0)
176
+ return;
177
+ return new Promise((resolve) => this.waiters.push(resolve));
178
+ }
179
+ end() {
180
+ if (this.closed)
181
+ return;
182
+ this.closed = true;
183
+ this.controller.close();
184
+ this.release();
185
+ }
186
+ abort(message) {
187
+ if (this.closed)
188
+ return;
189
+ this.closed = true;
190
+ this.controller.error(new Error(message));
191
+ this.release();
192
+ }
193
+ }
194
+ function traceEnabled() {
195
+ try {
196
+ const v = globalThis.process?.env?.CONVEX_RUST_TRACE;
197
+ return typeof v === "string" && v !== "" && v !== "0" && v !== "false";
198
+ }
199
+ catch {
200
+ return false;
201
+ }
202
+ }
203
+ const now = () => {
204
+ const p = globalThis.performance;
205
+ return p && typeof p.now === "function" ? p.now() : Date.now();
206
+ };
207
+ /**
208
+ * One wasm instance plus the per-call state around it. A Host that finished a call cleanly may serve
209
+ * the next call of the same isolate; after a trap / panic / leftover work it is dropped.
210
+ */
211
+ class Host {
212
+ instance;
213
+ memory;
214
+ nextTaskId = 0;
215
+ nextStreamId = 0;
216
+ /** Queued events: JSON text, or a stream chunk kept as parts until `task_poll` frames it (binary events). */
217
+ events = [];
218
+ binary;
219
+ /** Pending sleeps ordered by (deadline, creation); one timer serves the earliest. */
220
+ sleeps = [];
221
+ sleepSeq = 0;
222
+ sleepTimer = null;
223
+ sleepTimerAt = 0;
224
+ waiter = null;
225
+ pendingTasks = 0;
226
+ openStreams = 0;
227
+ /** Cancellable fetch tasks by task id (`net/cancel`, `net/body_*`). */
228
+ tasks = new Map();
229
+ /** Cancel handles of open body streams by stream id (`net/cancel` {streamId}). */
230
+ streamCancels = new Map();
231
+ panic = null;
232
+ /** The last `Uncaught panic: ` line, kept until a guest that confirms panics (level 4) confirms it. */
233
+ panicCandidate = null;
234
+ /** The guest exports `convex_panic_marker`: only a level-4 confirmation makes a panic line count. */
235
+ panicMarker = false;
236
+ done = false;
237
+ stdout = new LineBuffer((l) => console.log(l));
238
+ stderr = new LineBuffer((l) => console.error(l));
239
+ ctx;
240
+ /** The HTTP action this call serves (`runRustHttp`), if any. */
241
+ http = null;
242
+ /** WASI environ, fixed at instantiation: libc caches it on first read, so a change retires the instance. */
243
+ environ = [];
244
+ envKey = "";
245
+ instantiated = false;
246
+ /** A call is in flight (JSPI may have it suspended): never hand this Host to another call. */
247
+ busy = false;
248
+ /** False once the instance trapped / panicked / has leftover work. */
249
+ reusable = true;
250
+ calls = 0;
251
+ /** Cap on the in-memory `/tmp` in bytes for the current call (0 = no `/tmp` preopen). */
252
+ tmpBytes = DEFAULT_TMP_BYTES;
253
+ /** Whether this instance was linked with a `/tmp` preopen. */
254
+ tmpOn = false;
255
+ tmp;
256
+ // ---- memory helpers ----
257
+ bytes(ptr, len) {
258
+ return new Uint8Array(this.memory.buffer, ptr >>> 0, len >>> 0);
259
+ }
260
+ readString(ptr, len) {
261
+ return textDecoder.decode(this.bytes(ptr, len));
262
+ }
263
+ /**
264
+ * UTF-8 encodes `text` into a reused scratch buffer and copies it into guest memory: no per-event
265
+ * `Uint8Array`. The guest allocation must be exactly the byte length (`convex_free` / `Vec` capacity), so
266
+ * the text cannot be encoded straight into an over-sized guest block.
267
+ */
268
+ writeGuestString(text) {
269
+ const max = text.length * 3;
270
+ if (max > SCRATCH_MAX_BYTES)
271
+ return this.writeGuest(textEncoder.encode(text));
272
+ if (scratch.length < max)
273
+ scratch = new Uint8Array(Math.max(max, 4096));
274
+ const { written } = textEncoder.encodeInto(text, scratch);
275
+ const alloc = this.instance.exports.convex_alloc;
276
+ const ptr = alloc(written) >>> 0;
277
+ this.bytes(ptr, written).set(scratch.subarray(0, written));
278
+ return pack(ptr, written);
279
+ }
280
+ writeGuest(data) {
281
+ const alloc = this.instance.exports.convex_alloc;
282
+ const ptr = alloc(data.length) >>> 0;
283
+ this.bytes(ptr, data.length).set(data);
284
+ return pack(ptr, data.length);
285
+ }
286
+ // ---- logging ----
287
+ flushStdio() {
288
+ this.stdout.finish();
289
+ this.stderr.finish();
290
+ }
291
+ log(level, message) {
292
+ if (level === 0)
293
+ console.debug(message);
294
+ else if (level === 2)
295
+ console.warn(message);
296
+ else if (level === 3)
297
+ console.error(message);
298
+ else
299
+ console.log(message);
300
+ }
301
+ // ---- event queue ----
302
+ push(ev) {
303
+ this.events.push(JSON.stringify(ev));
304
+ this.wake();
305
+ }
306
+ wake() {
307
+ const w = this.waiter;
308
+ this.waiter = null;
309
+ if (w)
310
+ w();
311
+ }
312
+ /** Queues a stream data chunk: a binary event for guests that export `convex_binary_events`, else JSON+base64. */
313
+ pushChunk(sid, parts, size) {
314
+ if (this.binary === undefined) {
315
+ this.binary = typeof this.instance.exports.convex_binary_events === "function";
316
+ }
317
+ if (!this.binary || sid.length * 3 > 255) {
318
+ let data = parts[0];
319
+ if (parts.length !== 1) {
320
+ data = new Uint8Array(size);
321
+ let o = 0;
322
+ for (const p of parts) {
323
+ data.set(p, o);
324
+ o += p.length;
325
+ }
326
+ }
327
+ this.push({ type: "chunk", streamId: sid, data: encodeBase64(data) });
328
+ return;
329
+ }
330
+ this.events.push({ sid, parts, size });
331
+ this.wake();
332
+ }
333
+ // ---- sleeps: one timer for the earliest deadline, every due sleep delivered in one wake ----
334
+ sleep(ms, signal) {
335
+ return new Promise((resolve) => {
336
+ const at = now() + ms;
337
+ const seq = this.sleepSeq++;
338
+ const list = this.sleeps;
339
+ let i = list.length;
340
+ while (i > 0 && list[i - 1].at > at)
341
+ i--;
342
+ const entry = { at, seq, resolve: () => resolve(null) };
343
+ list.splice(i, 0, entry);
344
+ if (this.sleepTimer === null || at < this.sleepTimerAt)
345
+ this.armSleep(list[0].at);
346
+ // `net/cancel` on a sleep task: drop its deadline now so it neither keeps the call
347
+ // outstanding nor leaves a timer behind (the guest abandoned it).
348
+ signal?.addEventListener("abort", () => {
349
+ const j = this.sleeps.indexOf(entry);
350
+ if (j >= 0)
351
+ this.sleeps.splice(j, 1);
352
+ if (this.sleeps.length === 0 && this.sleepTimer !== null) {
353
+ clearTimeout(this.sleepTimer);
354
+ this.sleepTimer = null;
355
+ }
356
+ resolve(null);
357
+ }, { once: true });
358
+ });
359
+ }
360
+ armSleep(at) {
361
+ if (this.sleepTimer !== null)
362
+ clearTimeout(this.sleepTimer);
363
+ this.sleepTimerAt = at;
364
+ this.sleepTimer = setTimeout(() => this.fireSleeps(), Math.max(0, Math.ceil(at - now())));
365
+ }
366
+ fireSleeps() {
367
+ this.sleepTimer = null;
368
+ const list = this.sleeps;
369
+ const t = now();
370
+ // The timer fired for the earliest sleep; also take everything else already due.
371
+ let n = 1;
372
+ while (n < list.length && list[n].at <= t)
373
+ n++;
374
+ for (const s of list.splice(0, n))
375
+ s.resolve();
376
+ if (list.length > 0)
377
+ this.armSleep(list[0].at);
378
+ }
379
+ outstanding() {
380
+ return this.pendingTasks > 0 || this.openStreams > 0;
381
+ }
382
+ async poll(block) {
383
+ for (;;) {
384
+ const ev = this.events.shift();
385
+ if (ev !== undefined) {
386
+ if (typeof ev === "string")
387
+ return this.writeGuestString(ev);
388
+ // Binary chunk frame (contracts/ABI.md): 0x01 | u8 id_len | id | bytes, straight into guest memory.
389
+ const id = textEncoder.encode(ev.sid);
390
+ const n = 2 + id.length + ev.size;
391
+ const alloc = this.instance.exports.convex_alloc;
392
+ const ptr = alloc(n) >>> 0;
393
+ const mem = this.bytes(ptr, n);
394
+ mem[0] = 1;
395
+ mem[1] = id.length;
396
+ mem.set(id, 2);
397
+ let o = 2 + id.length;
398
+ for (const p of ev.parts) {
399
+ mem.set(p, o);
400
+ o += p.length;
401
+ }
402
+ return pack(ptr, n);
403
+ }
404
+ if (!block)
405
+ return 0n;
406
+ if (!this.outstanding())
407
+ return -1n;
408
+ await new Promise((resolve) => {
409
+ this.waiter = resolve;
410
+ });
411
+ }
412
+ }
413
+ // ---- task_submit ----
414
+ submit(ptr, len) {
415
+ if (len >>> 0 > MAX_TASK_REQUEST_BYTES) {
416
+ this.log(3, `task_submit: request of ${len >>> 0} bytes exceeds ${MAX_TASK_REQUEST_BYTES}`);
417
+ return -1n;
418
+ }
419
+ if (this.pendingTasks >= MAX_OUTSTANDING_TASKS) {
420
+ this.log(3, `task_submit: more than ${MAX_OUTSTANDING_TASKS} outstanding tasks`);
421
+ return -1n;
422
+ }
423
+ let req;
424
+ try {
425
+ req = JSON.parse(this.readString(ptr, len));
426
+ }
427
+ catch (e) {
428
+ this.log(3, `task_submit: malformed request: ${e?.message ?? e}`);
429
+ return -1n;
430
+ }
431
+ // A rejected request still consumes an id (as in the backend host).
432
+ const id = this.nextTaskId++;
433
+ let start;
434
+ try {
435
+ start = this.parseRequest(req, id);
436
+ }
437
+ catch (e) {
438
+ this.log(3, `task_submit: ${e?.message ?? e}`);
439
+ return -1n;
440
+ }
441
+ this.pendingTasks++;
442
+ // Never let a task failure escape as an unhandled rejection.
443
+ void (async () => {
444
+ let ev;
445
+ try {
446
+ ev = { type: "done", taskId: id, ok: await start() };
447
+ }
448
+ catch (e) {
449
+ ev = { type: "done", taskId: id, error: errorJson(e) };
450
+ }
451
+ this.pendingTasks--;
452
+ const ctl = this.tasks.get(id);
453
+ this.tasks.delete(id);
454
+ // A cancelled task's guest future is gone: skip the event.
455
+ if (!this.done && !ctl?.cancelled)
456
+ this.push(ev);
457
+ else
458
+ this.wake(); // a blocked poll must notice there is nothing left to wait for
459
+ })();
460
+ return BigInt(id);
461
+ }
462
+ /** Validates the request now (so a bad one returns -1) and returns the work. */
463
+ parseRequest(req, taskId) {
464
+ const ty = req?.type;
465
+ if (typeof ty !== "string")
466
+ throw new Error("missing request type");
467
+ const str = (k) => {
468
+ if (typeof req[k] !== "string")
469
+ throw new Error(`${ty}: missing string field \`${k}\``);
470
+ return req[k];
471
+ };
472
+ const body = () => {
473
+ const b = req.body;
474
+ if (b === undefined || b === null)
475
+ return null;
476
+ if (isBodyRef(b)) {
477
+ // The guest may reuse its buffer once `task_submit` returns: copy now.
478
+ return this.refBytes(b, ty).slice();
479
+ }
480
+ if (typeof b !== "string") {
481
+ throw new Error(`${ty}: \`body\` must be a base64 string, a {ptr,len} reference or null`);
482
+ }
483
+ try {
484
+ return decodeBase64(b);
485
+ }
486
+ catch (e) {
487
+ throw new Error(`${ty}: invalid base64 body: ${e?.message ?? e}`);
488
+ }
489
+ };
490
+ switch (ty) {
491
+ case "syscall": {
492
+ const name = str("name");
493
+ const args = req.args ?? {};
494
+ return () => this.syscall(name, args);
495
+ }
496
+ case "fetch": {
497
+ const url = str("url");
498
+ try {
499
+ new URL(url);
500
+ }
501
+ catch (e) {
502
+ throw new Error(`fetch: ${e?.message ?? e}`);
503
+ }
504
+ const method = typeof req.method === "string" ? req.method : "GET";
505
+ const headers = new Headers();
506
+ if (Array.isArray(req.headers)) {
507
+ for (const pair of req.headers) {
508
+ if (!Array.isArray(pair) || typeof pair[0] !== "string" || typeof pair[1] !== "string") {
509
+ throw new Error("fetch: headers must be [name, value] pairs");
510
+ }
511
+ headers.append(pair[0], pair[1]);
512
+ }
513
+ }
514
+ const ctl = { abort: new AbortController(), cancelled: false };
515
+ let b = null;
516
+ if (req.body && typeof req.body === "object" && req.body.stream === true) {
517
+ ctl.body = new RequestBody();
518
+ }
519
+ else {
520
+ b = body();
521
+ }
522
+ this.tasks.set(taskId, ctl);
523
+ return () => this.fetch(url, method, headers, b, ctl);
524
+ }
525
+ case "httpStart": {
526
+ const status = req.status;
527
+ const headers = req.headers;
528
+ return async () => {
529
+ this.requireHttp().start(status, headers);
530
+ return null;
531
+ };
532
+ }
533
+ case "httpWrite": {
534
+ const b = body() ?? new Uint8Array(0);
535
+ return async () => {
536
+ await this.requireHttp().write(b);
537
+ return null;
538
+ };
539
+ }
540
+ case "httpEnd":
541
+ return async () => {
542
+ this.requireHttp().end();
543
+ return null;
544
+ };
545
+ case "sleep": {
546
+ const ms = req.ms;
547
+ if (typeof ms !== "number" || !Number.isInteger(ms) || ms < 0) {
548
+ throw new Error("sleep: missing integer field `ms`");
549
+ }
550
+ const ctl = { abort: new AbortController(), cancelled: false };
551
+ this.tasks.set(taskId, ctl);
552
+ return () => this.sleep(ms, ctl.abort.signal);
553
+ }
554
+ case "storageStore": {
555
+ const contentType = typeof req.contentType === "string" ? req.contentType : undefined;
556
+ const opts = contentType ? { type: contentType } : {};
557
+ // A {ptr,len} body goes from guest memory straight into the Blob (the constructor snapshots
558
+ // it synchronously): one copy, no intermediate buffer, no base64.
559
+ const blob = isBodyRef(req.body)
560
+ ? new Blob([this.refBytes(req.body, ty)], opts)
561
+ : new Blob([(body() ?? new Uint8Array(0))], opts);
562
+ return async () => await this.requireStorage().store(blob);
563
+ }
564
+ case "storageGet": {
565
+ const id = str("storageId");
566
+ return async () => {
567
+ const blob = await this.requireStorage().get(id);
568
+ if (blob === null || blob === undefined)
569
+ return null;
570
+ const bodyStreamId = this.openStream(blob.stream ? blob.stream() : blob);
571
+ return {
572
+ bodyStreamId,
573
+ contentLength: blob.size,
574
+ contentType: blob.type ? blob.type : null,
575
+ };
576
+ };
577
+ }
578
+ default:
579
+ throw new Error(`unknown request type ${JSON.stringify(ty)}`);
580
+ }
581
+ }
582
+ requireHttp() {
583
+ if (!this.http)
584
+ throw new Error("httpStart/httpWrite/httpEnd are only available in an HTTP action");
585
+ return this.http;
586
+ }
587
+ requireStorage() {
588
+ if (!this.ctx.storage)
589
+ throw new Error("ctx.storage is not available in this context");
590
+ return this.ctx.storage;
591
+ }
592
+ // ---- async syscalls ----
593
+ async syscall(name, args) {
594
+ const ctx = this.ctx;
595
+ const fnRef = () => functionRef(name, args);
596
+ const fnArgs = () => parseConvexValue(args?.args ?? {});
597
+ switch (name) {
598
+ case "1.0/actions/query":
599
+ return toConvexJson(await ctx.runQuery(fnRef(), fnArgs()));
600
+ case "1.0/actions/mutation":
601
+ return toConvexJson(await ctx.runMutation(fnRef(), fnArgs()));
602
+ case "1.0/actions/action":
603
+ return toConvexJson(await ctx.runAction(fnRef(), fnArgs()));
604
+ case "1.0/actions/schedule": {
605
+ if (!ctx.scheduler)
606
+ throw new Error("ctx.scheduler is not available in this context");
607
+ const ts = args?.ts;
608
+ if (typeof ts !== "number")
609
+ throw new Error(`${name}: missing numeric \`ts\``);
610
+ // The wire `ts` is in seconds; ctx.scheduler.runAt takes milliseconds.
611
+ return toConvexJson(await ctx.scheduler.runAt(ts * 1000, fnRef(), fnArgs()));
612
+ }
613
+ case "1.0/actions/cancel_job": {
614
+ if (!ctx.scheduler)
615
+ throw new Error("ctx.scheduler is not available in this context");
616
+ await ctx.scheduler.cancel(args?.id);
617
+ return null;
618
+ }
619
+ case "1.0/getUserIdentity": {
620
+ if (!ctx.auth)
621
+ throw new Error("ctx.auth is not available in this context");
622
+ return toConvexJson(await ctx.auth.getUserIdentity());
623
+ }
624
+ case "1.0/storageGetUrl":
625
+ return toConvexJson(await this.requireStorage().getUrl(args?.storageId));
626
+ case "1.0/storageGetMetadata": {
627
+ const s = this.requireStorage();
628
+ if (!s.getMetadata)
629
+ throw new Error("ctx.storage.getMetadata is not available");
630
+ return toConvexJson(await s.getMetadata(args?.storageId));
631
+ }
632
+ case "1.0/storageDelete":
633
+ await this.requireStorage().delete(args?.storageId);
634
+ return null;
635
+ case "1.0/storageGenerateUploadUrl": {
636
+ const s = this.requireStorage();
637
+ if (!s.generateUploadUrl)
638
+ throw new Error("ctx.storage.generateUploadUrl is not available");
639
+ return toConvexJson(await s.generateUploadUrl());
640
+ }
641
+ case "1.0/actions/vectorSearch": {
642
+ if (!ctx.vectorSearch)
643
+ throw new Error("ctx.vectorSearch is not available in this context");
644
+ const q = args?.query;
645
+ const dot = typeof q?.indexName === "string" ? q.indexName.indexOf(".") : -1;
646
+ if (dot < 0)
647
+ throw new Error(`${name}: \`query.indexName\` must be "<table>.<index>"`);
648
+ const query = { vector: q.vector };
649
+ if (q.limit !== undefined && q.limit !== null)
650
+ query.limit = q.limit;
651
+ if (q.expressions)
652
+ query.filter = (f) => vectorFilter(f, q.expressions);
653
+ const results = await ctx.vectorSearch(q.indexName.slice(0, dot), q.indexName.slice(dot + 1), query);
654
+ return { results: toConvexJson(results) };
655
+ }
656
+ default: {
657
+ // Any other name goes to the runtime's own async syscall, as the native host does.
658
+ const convex = globalThis.Convex;
659
+ if (typeof convex?.asyncSyscall !== "function") {
660
+ throw new Error(`Unsupported syscall in the V8 host: ${name}`);
661
+ }
662
+ const out = await convex.asyncSyscall(name, JSON.stringify(args ?? {}));
663
+ return typeof out === "string" ? JSON.parse(out) : (out ?? null);
664
+ }
665
+ }
666
+ }
667
+ // ---- fetch and body streams ----
668
+ async fetch(url, method, headers, body, ctl) {
669
+ const init = { method, headers, redirect: "manual", signal: ctl.abort.signal };
670
+ if (ctl.body) {
671
+ init.body = ctl.body.stream;
672
+ init.duplex = "half";
673
+ }
674
+ else if (body !== null && method !== "GET" && method !== "HEAD")
675
+ init.body = body;
676
+ let res;
677
+ try {
678
+ res = await fetch(url, init);
679
+ }
680
+ catch (e) {
681
+ // Release a guest blocked in `net/body_write` backpressure.
682
+ ctl.body?.abort("request failed");
683
+ throw e;
684
+ }
685
+ const pairs = [];
686
+ res.headers.forEach((value, key) => pairs.push([key, value]));
687
+ const bodyStreamId = res.body ? this.openStream(res.body, () => ctl.abort.abort()) : null;
688
+ return {
689
+ status: res.status,
690
+ statusText: res.statusText ?? "",
691
+ headers: pairs,
692
+ url: res.url || url,
693
+ bodyStreamId,
694
+ };
695
+ }
696
+ /** Registers a body stream; chunk events follow the `done` of its task. */
697
+ openStream(stream, cancel) {
698
+ const streamId = `s${this.nextStreamId++}`;
699
+ this.openStreams++;
700
+ let cancelled = false;
701
+ const reader0 = typeof stream.getReader === "function"
702
+ ? stream.getReader()
703
+ : null;
704
+ this.streamCancels.set(streamId, () => {
705
+ cancelled = true;
706
+ if (cancel)
707
+ cancel();
708
+ else if (reader0)
709
+ void reader0.cancel().catch(() => { });
710
+ });
711
+ void (async () => {
712
+ let end = { type: "chunk", streamId, end: true };
713
+ try {
714
+ if (reader0) {
715
+ const reader = reader0;
716
+ // Coalesce small chunks (< 8 KiB) that arrive back to back into one guest event of up to
717
+ // ~1 MiB (one base64 pass and one wake-up instead of many); larger chunks pass through as is.
718
+ let parts = [];
719
+ let size = 0;
720
+ const flush = () => {
721
+ if (size === 0)
722
+ return;
723
+ const ps = parts;
724
+ const sz = size;
725
+ parts = [];
726
+ size = 0;
727
+ if (!this.done && !cancelled)
728
+ this.pushChunk(streamId, ps, sz);
729
+ };
730
+ const TICK = Symbol("tick");
731
+ let pending = null;
732
+ for (;;) {
733
+ pending ??= reader.read();
734
+ let r;
735
+ if (size === 0)
736
+ r = await pending;
737
+ else {
738
+ let timer;
739
+ r = await Promise.race([
740
+ pending,
741
+ new Promise((res) => {
742
+ timer = setTimeout(() => res(TICK), 0);
743
+ }),
744
+ ]);
745
+ clearTimeout(timer);
746
+ }
747
+ if (r === TICK) {
748
+ flush();
749
+ continue;
750
+ }
751
+ pending = null;
752
+ if (r.done)
753
+ break;
754
+ if (r.value && r.value.length >= STREAM_PASSTHROUGH_BYTES) {
755
+ // Already a decent size: keep the network's chunking, just keep the order.
756
+ flush();
757
+ parts.push(r.value);
758
+ size += r.value.length;
759
+ flush();
760
+ }
761
+ else if (r.value && r.value.length > 0) {
762
+ parts.push(r.value);
763
+ size += r.value.length;
764
+ if (size >= STREAM_COALESCE_BYTES)
765
+ flush();
766
+ }
767
+ }
768
+ flush();
769
+ }
770
+ else {
771
+ const buf = new Uint8Array(await stream.arrayBuffer());
772
+ if (buf.length > 0)
773
+ this.pushChunk(streamId, [buf], buf.length);
774
+ }
775
+ }
776
+ catch (e) {
777
+ end = { type: "chunk", streamId, error: errorJson(e).message };
778
+ }
779
+ this.openStreams--;
780
+ this.streamCancels.delete(streamId);
781
+ if (!this.done && !cancelled)
782
+ this.push(end);
783
+ else
784
+ this.wake();
785
+ })();
786
+ return streamId;
787
+ }
788
+ // ---- net/* sync ops: cancel, guest-fed request bodies (contracts/ABI.md) ----
789
+ refBytes(ref, what) {
790
+ const ptr = ref.ptr >>> 0;
791
+ const len = ref.len >>> 0;
792
+ if (len > MAX_BODY_REF_BYTES) {
793
+ throw new Error(`${what}: body of ${len} bytes exceeds ${MAX_BODY_REF_BYTES}; stream it instead`);
794
+ }
795
+ if (ptr + len > this.memory.buffer.byteLength)
796
+ throw new Error(`${what}: body reference out of bounds`);
797
+ return this.bytes(ptr, len);
798
+ }
799
+ netOp(op, args) {
800
+ const reply = (x) => this.writeGuest(textEncoder.encode(JSON.stringify(x)));
801
+ const fail = (e) => reply({ error: { message: `${op}: ${errorJson(e).message}` } });
802
+ try {
803
+ const taskCtl = () => {
804
+ const ctl = this.tasks.get(args?.taskId);
805
+ if (!ctl?.body)
806
+ throw new Error(`no streamed request body for task ${args?.taskId}`);
807
+ return ctl;
808
+ };
809
+ switch (op) {
810
+ case "net/caps":
811
+ return reply({
812
+ ok: { version: 1, ops: ["cancel", "bodyRef", "requestStream"], maxBodyRef: MAX_BODY_REF_BYTES },
813
+ });
814
+ case "net/cancel": {
815
+ let found = false;
816
+ if (typeof args?.taskId === "number") {
817
+ const ctl = this.tasks.get(args.taskId);
818
+ if (ctl) {
819
+ found = true;
820
+ ctl.cancelled = true;
821
+ ctl.body?.abort("cancelled");
822
+ ctl.abort.abort();
823
+ }
824
+ }
825
+ else if (typeof args?.streamId === "string") {
826
+ const c = this.streamCancels.get(args.streamId);
827
+ if (c) {
828
+ found = true;
829
+ c();
830
+ }
831
+ }
832
+ return reply({ ok: found });
833
+ }
834
+ case "net/body_write": {
835
+ const ctl = taskCtl();
836
+ // Copy: the guest reuses the buffer as soon as the call returns.
837
+ const data = this.refBytes(args.data, op).slice();
838
+ const w = ctl.body.write(data);
839
+ return w ? w.then(() => reply({ ok: true }), fail) : reply({ ok: true });
840
+ }
841
+ case "net/body_end":
842
+ taskCtl().body.end();
843
+ return reply({ ok: true });
844
+ case "net/body_abort":
845
+ taskCtl().body.abort(typeof args?.message === "string" ? args.message : "aborted");
846
+ return reply({ ok: true });
847
+ default:
848
+ return reply({ error: { message: `unknown sync op ${op}` } });
849
+ }
850
+ }
851
+ catch (e) {
852
+ return fail(e);
853
+ }
854
+ }
855
+ // ---- sync syscalls ----
856
+ syncSyscall(opPtr, opLen, argsPtr, argsLen) {
857
+ let reply;
858
+ if ((opLen >>> 0) > 4096 || (argsLen >>> 0) > MAX_TASK_REQUEST_BYTES) {
859
+ reply = { error: { message: "sync_syscall: request too large" } };
860
+ }
861
+ else {
862
+ const op = this.readString(opPtr, opLen);
863
+ let args;
864
+ let bad = null;
865
+ try {
866
+ args = JSON.parse(this.readString(argsPtr, argsLen));
867
+ }
868
+ catch (e) {
869
+ bad = `sync op ${op}: invalid args: ${e?.message ?? e}`;
870
+ }
871
+ if (bad !== null)
872
+ reply = { error: { message: bad } };
873
+ else if (op === "now")
874
+ reply = { ok: Date.now() };
875
+ else if (op.startsWith("intl/"))
876
+ reply = intlOp(op, args);
877
+ else if (op.startsWith("net/"))
878
+ return this.netOp(op, args);
879
+ else if (op.startsWith("crypto/")) {
880
+ const r = cryptoOp(op, args, () => this.memory);
881
+ const write = (x) => this.writeGuest(textEncoder.encode(JSON.stringify(x)));
882
+ return r instanceof Promise ? r.then(write) : write(r);
883
+ }
884
+ else if (op === "componentArgument") {
885
+ reply = componentArgument(args);
886
+ }
887
+ else
888
+ reply = { error: { message: `unknown sync op ${op}` } };
889
+ }
890
+ return this.writeGuest(textEncoder.encode(JSON.stringify(reply)));
891
+ }
892
+ // ---- imports ----
893
+ imports(module) {
894
+ const WA = webAssembly();
895
+ if (typeof WA.Suspending !== "function" || typeof WA.promising !== "function") {
896
+ throw new Error('WebAssembly JSPI is not available in this runtime. "use rust" actions need the default Convex runtime (not "use node").');
897
+ }
898
+ const info = inspectModule(module);
899
+ const tmpBytes = this.tmpBytes;
900
+ this.tmpOn = tmpBytes > 0;
901
+ const exportNames = new Set(WA.Module.exports(module).map((x) => x.name));
902
+ // File data goes into the guest's own linear memory when it exports its allocator (see tmpfs.ts).
903
+ const ex = () => this.instance.exports;
904
+ this.tmp =
905
+ tmpBytes > 0
906
+ ? new TmpFs({
907
+ maxBytes: tmpBytes,
908
+ memory: () => this.memory,
909
+ alloc: exportNames.has("convex_alloc") ? (n) => ex().convex_alloc(n) : undefined,
910
+ free: exportNames.has("convex_free") ? (p, n) => ex().convex_free(p, n) : undefined,
911
+ })
912
+ : undefined;
913
+ const wasi = makeWasi({
914
+ memory: () => this.memory,
915
+ environ: () => this.environ,
916
+ line: () => { },
917
+ tmp: this.tmp,
918
+ }, { 1: this.stdout, 2: this.stderr });
919
+ const wasiImports = {};
920
+ for (const name of info.wasiFns) {
921
+ wasiImports[name] =
922
+ name === "poll_oneoff" && wasi[name]
923
+ ? new WA.Suspending(wasi[name])
924
+ : (wasi[name] ?? (() => ENOSYS));
925
+ }
926
+ const convex = {
927
+ task_submit: (ptr, len) => this.submit(ptr, len),
928
+ task_poll: new WA.Suspending((block) => this.poll(block)),
929
+ sync_syscall: new WA.Suspending((a, b, c, d) => this.syncSyscall(a, b, c, d)),
930
+ log: (level, ptr, len) => {
931
+ const max = MAX_LOG_MESSAGE_BYTES;
932
+ let message = this.readString(ptr, Math.min(len >>> 0, max));
933
+ message = truncateUtf8(message, max);
934
+ // Keep stdout / stderr lines written so far ahead of this one.
935
+ this.stdout.finish();
936
+ this.stderr.finish();
937
+ if (level === 4 && this.panicMarker) {
938
+ // The guest's panic hook confirms the line it just logged; user log lines cannot send level 4.
939
+ this.panic = this.panicCandidate;
940
+ return;
941
+ }
942
+ if (level === 3 && message.startsWith(PANIC_LOG_PREFIX)) {
943
+ const text = message.slice(PANIC_LOG_PREFIX.length);
944
+ if (this.panicMarker)
945
+ this.panicCandidate = text;
946
+ else
947
+ this.panic = text; // older guests: the prefixed line alone is the signal
948
+ }
949
+ this.log(level, message);
950
+ },
951
+ };
952
+ return { convex, wasi_snapshot_preview1: wasiImports };
953
+ }
954
+ /** Instantiates the module and runs `_initialize` (once per Host). */
955
+ instantiate(module, ph) {
956
+ const WA = webAssembly();
957
+ let t = now();
958
+ const imports = this.imports(module);
959
+ ph.imports = now() - t;
960
+ t = now();
961
+ this.instance = new WA.Instance(module, imports);
962
+ ph.instantiate = now() - t;
963
+ const exports = this.instance.exports;
964
+ this.memory = exports.memory;
965
+ this.panicMarker = typeof exports.convex_panic_marker === "function";
966
+ t = now();
967
+ const init = exports._initialize;
968
+ if (typeof init === "function")
969
+ init();
970
+ ph.init = now() - t;
971
+ this.instantiated = true;
972
+ }
973
+ async run(ctx, environ, module, exportName, args, ph, http = null) {
974
+ const WA = webAssembly();
975
+ this.ctx = ctx;
976
+ this.http = http;
977
+ if (!this.instantiated) {
978
+ this.environ = environ;
979
+ this.envKey = environ.join("\0");
980
+ }
981
+ if (this.tmp)
982
+ this.tmp.maxBytes = Math.max(1, this.tmpBytes);
983
+ this.nextTaskId = 0;
984
+ this.nextStreamId = 0;
985
+ this.events = [];
986
+ this.waiter = null;
987
+ this.sleeps = [];
988
+ this.sleepTimer = null;
989
+ this.tasks.clear();
990
+ this.streamCancels.clear();
991
+ this.panic = null;
992
+ this.panicCandidate = null;
993
+ this.done = false;
994
+ this.calls++;
995
+ // Anything but a clean finish (trap, panic, error envelope, leftover work) retires this instance.
996
+ let clean = false;
997
+ try {
998
+ let t = now();
999
+ // An HTTP action's args are the request head; its body is a host stream the guest reads.
1000
+ const argsJson = stringifyConvex(http
1001
+ ? { ...http.head, bodyStreamId: http.requestBody ? this.openStream(http.requestBody) : null }
1002
+ : args);
1003
+ ph.encodeArgs = now() - t;
1004
+ if (!this.instantiated)
1005
+ this.instantiate(module, ph);
1006
+ const exports = this.instance.exports;
1007
+ const run = exports[exportName];
1008
+ if (typeof run !== "function") {
1009
+ const { module: m, name } = parseExportName(exportName);
1010
+ throw new Error(`Could not find function ${name} in module ${m}`);
1011
+ }
1012
+ if (typeof exports.convex_alloc !== "function" || !this.memory) {
1013
+ throw new Error('The "use rust" module broke the guest ABI: module has no `convex_alloc` or `memory` export');
1014
+ }
1015
+ t = now();
1016
+ const argBytes = textEncoder.encode(argsJson);
1017
+ const argPtr = exports.convex_alloc(argBytes.length) >>> 0;
1018
+ this.bytes(argPtr, argBytes.length).set(argBytes);
1019
+ let packed;
1020
+ ph.prepare = now() - t;
1021
+ t = now();
1022
+ try {
1023
+ packed = await WA.promising(run)(argPtr, argBytes.length);
1024
+ }
1025
+ catch (e) {
1026
+ this.flushStdio();
1027
+ if (e instanceof ProcExit) {
1028
+ throw new Error(`WebAssembly trap: ${e.message}`);
1029
+ }
1030
+ if (e instanceof WA.RuntimeError) {
1031
+ if (this.panic !== null)
1032
+ throw new Error(this.panic);
1033
+ throw new Error(`WebAssembly trap: ${e.message}`);
1034
+ }
1035
+ throw e;
1036
+ }
1037
+ this.flushStdio();
1038
+ ph.call = now() - t;
1039
+ t = now();
1040
+ // Reuse only when the guest finished with nothing left running and its memory stayed modest.
1041
+ clean =
1042
+ !this.outstanding() &&
1043
+ this.panic === null &&
1044
+ this.calls < MAX_REUSE_CALLS &&
1045
+ this.memory.buffer.byteLength <= MAX_REUSE_MEMORY_BYTES;
1046
+ const [rptr, rlen] = unpack(packed);
1047
+ if (rlen > MAX_RESULT_BYTES) {
1048
+ clean = false; // the oversized buffer is never read or freed: do not reuse this instance
1049
+ throw new Error(`Function result is too large (${rlen} bytes, the limit is ${MAX_RESULT_BYTES} bytes)`);
1050
+ }
1051
+ let envelope;
1052
+ try {
1053
+ envelope = JSON.parse(this.readString(rptr, rlen));
1054
+ }
1055
+ catch (e) {
1056
+ clean = false;
1057
+ throw new Error(`The "use rust" module broke the guest ABI: result envelope: ${e?.message ?? e}`);
1058
+ }
1059
+ finally {
1060
+ // Optional export (contracts/V8HOST.md): hand the result buffer back to the guest allocator.
1061
+ const free = exports.convex_free;
1062
+ if (typeof free === "function" && rlen > 0)
1063
+ free(rptr, rlen);
1064
+ }
1065
+ if (envelope && "ok" in envelope) {
1066
+ // An HTTP action's value is the plain response description, not a Convex value.
1067
+ const v = http ? envelope.ok : parseConvexValue(envelope.ok);
1068
+ ph.decode = now() - t;
1069
+ return v;
1070
+ }
1071
+ if (envelope && envelope.error) {
1072
+ const err = envelope.error;
1073
+ const message = typeof err.message === "string" ? err.message : "unknown error";
1074
+ if (err.kind === "convex" && "data" in err) {
1075
+ throw new ConvexError(parseConvexValue(err.data));
1076
+ }
1077
+ throw new Error(message);
1078
+ }
1079
+ throw new Error('The "use rust" module broke the guest ABI: result envelope has neither `ok` nor `error`');
1080
+ }
1081
+ finally {
1082
+ this.done = true;
1083
+ this.flushStdio();
1084
+ if (!clean)
1085
+ this.reusable = false;
1086
+ // `/tmp` lives for one call: hand its guest-memory segments back (a retired instance is just dropped).
1087
+ try {
1088
+ this.tmp?.reset(clean);
1089
+ }
1090
+ catch {
1091
+ this.reusable = false;
1092
+ }
1093
+ }
1094
+ }
1095
+ }
1096
+ function tmpBytesFromEnv() {
1097
+ try {
1098
+ const v = globalThis.process?.env?.CONVEX_RUST_TMP_BYTES;
1099
+ if (v !== undefined && v !== "" && Number.isFinite(Number(v)) && Number(v) >= 0)
1100
+ return Math.floor(Number(v));
1101
+ }
1102
+ catch {
1103
+ // process.env may be unavailable
1104
+ }
1105
+ return DEFAULT_TMP_BYTES;
1106
+ }
1107
+ /** Reusable hosts per compiled module (one per isolate, since module scope is per isolate). */
1108
+ const hosts = new WeakMap();
1109
+ /** Modules compiled from bytes by {@link runRust}, so the compile happens once per isolate. */
1110
+ const compiled = new WeakMap();
1111
+ function toModule(m) {
1112
+ const WA = webAssembly();
1113
+ if (m instanceof WA.Module)
1114
+ return m;
1115
+ const hit = compiled.get(m);
1116
+ if (hit)
1117
+ return hit;
1118
+ const c = new WA.Module(m);
1119
+ compiled.set(m, c);
1120
+ return c;
1121
+ }
1122
+ /**
1123
+ * Runs one `convex_run:*` export of a `"use rust"` module (guest ABI v1). `module` is the
1124
+ * `WebAssembly.Module` the bundler produced for `import wasm from "./x.wasm"` (compiled once per
1125
+ * isolate); raw wasm bytes are also accepted and compiled once.
1126
+ *
1127
+ * The instance is reused by later calls in the same isolate after a call that completed cleanly
1128
+ * (like JS module globals, Rust statics persist). After a trap / panic / OOM, or if another call is
1129
+ * still using it, a fresh instance is used. Set the deployment variable `CONVEX_RUST_TRACE=1` to log
1130
+ * per-call phase timings.
1131
+ */
1132
+ export async function runRust(ctx, module, exportName, args, options) {
1133
+ return execute(ctx, module, exportName, args, options, null);
1134
+ }
1135
+ async function execute(ctx, module, exportName, args, options, http) {
1136
+ const trace = traceEnabled();
1137
+ const t0 = now();
1138
+ const ph = {};
1139
+ const mod = toModule(module);
1140
+ let host = hosts.get(mod);
1141
+ const environ = collectEnviron(options?.env ?? []);
1142
+ const tmpCap = options?.tmpBytes ?? tmpBytesFromEnv();
1143
+ // Whether the guest sees a `/tmp` preopen is fixed at instantiation; its cap is applied per call.
1144
+ const reused = host !== undefined &&
1145
+ host.reusable &&
1146
+ !host.busy &&
1147
+ host.envKey === environ.join("\0") &&
1148
+ host.tmpOn === (tmpCap > 0);
1149
+ if (!reused) {
1150
+ const fresh = new Host();
1151
+ // Do not displace a busy cached host (its call is suspended); the fresh one is single-use.
1152
+ if (host === undefined || !host.busy)
1153
+ hosts.set(mod, fresh);
1154
+ host = fresh;
1155
+ }
1156
+ const h = host;
1157
+ h.tmpBytes = tmpCap;
1158
+ h.busy = true;
1159
+ let ok = false;
1160
+ try {
1161
+ const r = await h.run(ctx, environ, mod, exportName, args, ph, http);
1162
+ ok = true;
1163
+ return r;
1164
+ }
1165
+ finally {
1166
+ h.busy = false;
1167
+ if (!h.reusable && hosts.get(mod) === h)
1168
+ hosts.delete(mod);
1169
+ if (trace) {
1170
+ ph.total = now() - t0;
1171
+ const f = Object.entries(ph).map(([k, v]) => `${k}=${v.toFixed(2)}`).join(" ");
1172
+ console.log(`[convex-rust] ${exportName} reused=${reused} ok=${ok} ${f}`);
1173
+ }
1174
+ }
1175
+ }
1176
+ /**
1177
+ * Runs a `#[http_action]` export of a `"use rust"` module as the handler of a Convex `httpAction`
1178
+ * (contracts/V8HOST.md "HTTP actions"). The request head is passed as the call args and its body
1179
+ * as a host stream the guest reads incrementally; the guest answers with a buffered response or
1180
+ * streams it (`httpStart` / `httpWrite` / `httpEnd` tasks), in which case the returned Response
1181
+ * is available as soon as the guest started it and the body follows while the guest runs.
1182
+ *
1183
+ * Both bodies are limited to 20 MiB, like any Convex HTTP action; a buffered body also has to fit
1184
+ * the 16 MiB result limit (stream bigger ones).
1185
+ */
1186
+ export async function runRustHttp(ctx, module, exportName, request, options) {
1187
+ const sink = new HttpSink(requestHead(request), request.body ?? null);
1188
+ const call = execute(ctx, module, exportName, null, options, sink).then((ok) => ({ ok }), (error) => ({ error }));
1189
+ const first = await Promise.race([call, sink.started.then(() => "started")]);
1190
+ if (first === "started") {
1191
+ // The guest keeps running while the client reads; its end (or failure) closes (or errors) the body.
1192
+ void call.then((r) => ("error" in r ? sink.fail(r.error) : sink.end()));
1193
+ return sink.response;
1194
+ }
1195
+ if ("error" in first)
1196
+ throw first.error;
1197
+ // A guest that started a stream returns `{streamed: true}` only after `httpEnd`.
1198
+ if (sink.response)
1199
+ return sink.response;
1200
+ return bufferedResponse(first.ok);
1201
+ }
1202
+ /**
1203
+ * Environment for the guest: only the named variables (`names`, the build-time `--env` list) plus those
1204
+ * listed in the deployment variable `CONVEX_RUST_ENV`. The rest of `process.env` is never enumerated.
1205
+ */
1206
+ export function collectEnviron(names) {
1207
+ const env = (globalThis.process?.env ?? {});
1208
+ const out = new Map();
1209
+ let extra = [];
1210
+ try {
1211
+ extra = String(env.CONVEX_RUST_ENV ?? "").split(",");
1212
+ }
1213
+ catch {
1214
+ // ignore
1215
+ }
1216
+ for (const raw of [...names, ...extra]) {
1217
+ const name = raw.trim();
1218
+ if (name === "" || name.includes("="))
1219
+ continue;
1220
+ try {
1221
+ const v = env[name];
1222
+ if (typeof v === "string")
1223
+ out.set(name, v);
1224
+ }
1225
+ catch {
1226
+ // ignore
1227
+ }
1228
+ }
1229
+ return [...out].map(([k, v]) => `${k}=${v}`);
1230
+ }