@johannc-dev/zyrk 0.0.0-stage → 0.2.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.
package/dist/local.js ADDED
@@ -0,0 +1,1147 @@
1
+ import { randomFillSync } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { getHeapStatistics } from "node:v8";
6
+ import vm from "node:vm";
7
+ import { buildProgram } from "./build.js";
8
+ import { DELIVERY_BATCH, REPLAY_MAX_BATCH, MISSING_OPERATION_FIELDS, intervalEvents, replayEvents, } from "./local-replay.js";
9
+ import { LocalData, ReadFailed, spec as readSpec, } from "./local-data.js";
10
+ import { LocalStorage, StorageOpError, } from "./local-storage.js";
11
+ import { accountLimits, execLimits, subscriptionLimits } from "./limits.js";
12
+ const here = dirname(fileURLToPath(import.meta.url));
13
+ let batchReadsSaid = false;
14
+ function batchReadsFromEnv() {
15
+ const raw = process.env.FUNRUN_BATCH_READS;
16
+ const v = (raw ?? "").trim();
17
+ const on = v === "on" || v === "1" || v === "true";
18
+ if (raw !== undefined && !batchReadsSaid) {
19
+ batchReadsSaid = true;
20
+ process.stderr.write(`zyrk local runner: FUNRUN_BATCH_READS=${JSON.stringify(raw)}: batched history reads ${on ? "on" : "off"} (hosted functions batch only when the service has the same setting; off there by default)\n`);
21
+ }
22
+ return on;
23
+ }
24
+ let bootstrapText;
25
+ export function sandboxBootstrap() {
26
+ return (bootstrapText ??= readFileSync(join(here, "sandbox-bootstrap.js"), "utf8"));
27
+ }
28
+ class HostError extends Error {
29
+ kind;
30
+ code;
31
+ retryAfterMs;
32
+ constructor(kind, code, message, retryAfterMs) {
33
+ super(message);
34
+ this.kind = kind;
35
+ this.code = code;
36
+ this.retryAfterMs = retryAfterMs;
37
+ }
38
+ }
39
+ const program = (code, message) => new HostError("program", code, message);
40
+ const limit = (code, message) => new HostError("limit", code, message);
41
+ const LIMIT_CODES = new Set([
42
+ "read_limit",
43
+ "read_time_limit",
44
+ "op_limit",
45
+ "memory_limit",
46
+ "cpu_limit",
47
+ "wall_limit",
48
+ "output_limit",
49
+ "input_limit",
50
+ "stack_limit",
51
+ "query_work_limit",
52
+ ]);
53
+ const INFRA_CODES = new Set([
54
+ "history_preparing",
55
+ "storage_busy",
56
+ "history_unavailable",
57
+ ]);
58
+ const kindOfSdkCode = (code) => LIMIT_CODES.has(code)
59
+ ? "limit"
60
+ : INFRA_CODES.has(code)
61
+ ? "infrastructure"
62
+ : code === "cancelled"
63
+ ? "permission"
64
+ : "program";
65
+ const NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
66
+ const MAX_OP_BYTES = 2 * 1024 * 1024 + 4096;
67
+ const MAX_STORAGE_OP_BYTES = 6 * 1024 * 1024 + 4096;
68
+ const EMIT_OVERHEAD_BYTES = 768;
69
+ const WEBHOOK_MAX_PER_EXECUTION = 32;
70
+ const WEBHOOK_PAYLOAD_BYTES = 16 * 1024;
71
+ const WEBHOOK_OVERHEAD_BYTES = 256;
72
+ const WEBHOOK_QUEUE_LIMIT = 1000;
73
+ const MAX_MESSAGE_BYTES = 2048;
74
+ const openWhenExported = (v) => {
75
+ const c = v?.coverage;
76
+ return (typeof c === "object" &&
77
+ c !== null &&
78
+ (c.unsealed === true ||
79
+ (c.liveUntilMs !== null && c.liveUntilMs !== undefined)));
80
+ };
81
+ const jsonBytes = (v) => Buffer.byteLength(JSON.stringify(v) ?? "null");
82
+ const truncate = (m) => Buffer.byteLength(m) <= MAX_MESSAGE_BYTES
83
+ ? m
84
+ : Buffer.from(m).subarray(0, MAX_MESSAGE_BYTES).toString("utf8");
85
+ const final = (e) => (e.next ? final(e.next) : e);
86
+ const jobResult = (e) => {
87
+ const f = final(e);
88
+ const id = e.chain;
89
+ switch (f.status) {
90
+ case "succeeded":
91
+ return { id, status: "succeeded", result: f.result ?? null };
92
+ case "failed":
93
+ return { id, status: "failed", error: f.error };
94
+ case "queued":
95
+ case "running":
96
+ return { id, status: f.status };
97
+ default:
98
+ return { id, status: f.status };
99
+ }
100
+ };
101
+ export class LocalRunner {
102
+ data;
103
+ storage;
104
+ limits;
105
+ state = new Map();
106
+ subscriptions = new Map();
107
+ warnings = [];
108
+ #code;
109
+ #object;
110
+ #functions;
111
+ #options;
112
+ #name;
113
+ #version;
114
+ #jobs = new Map();
115
+ #queue = [];
116
+ #workflows = new Map();
117
+ #errors = new WeakMap();
118
+ #isolated = false;
119
+ #webhookSeq = 0;
120
+ #webhookPending = 0;
121
+ #running = 0;
122
+ #waiting = 0;
123
+ #runs = 0;
124
+ #idle = [];
125
+ #warned = new Set();
126
+ constructor(code, object, functions, subscriptions, options) {
127
+ this.#code = code;
128
+ this.#object = object;
129
+ this.#functions = new Set(functions);
130
+ this.#options = options;
131
+ this.#name = options.program ?? "local";
132
+ this.#version = object ? "local-object" : `local-${fnv(code)}`;
133
+ this.limits = {
134
+ exec: { ...execLimits, ...options.limits?.exec },
135
+ account: { ...accountLimits, ...options.limits?.account },
136
+ };
137
+ this.data =
138
+ typeof options.data === "string"
139
+ ? new LocalData(options.data)
140
+ : options.data;
141
+ this.storage =
142
+ options.storage instanceof LocalStorage
143
+ ? options.storage
144
+ : new LocalStorage(options.storage && options.storage !== "memory"
145
+ ? options.storage
146
+ : undefined, options.storageAreas, options.now);
147
+ for (const [name, s] of Object.entries(subscriptions))
148
+ this.subscriptions.set(name, s);
149
+ if (typeof options.state === "string") {
150
+ const file = this.#stateFile();
151
+ if (file && existsSync(file))
152
+ for (const [k, v] of Object.entries(JSON.parse(readFileSync(file, "utf8"))))
153
+ this.state.set(k, v);
154
+ }
155
+ else if (options.state)
156
+ for (const [k, v] of Object.entries(options.state))
157
+ this.state.set(k, v);
158
+ if (object)
159
+ this.#warn("A program object runs with Node's globals (fetch, timers, process): only a built bundle (an entry path) runs with the sandbox's globals.");
160
+ }
161
+ static async load(source, options = {}) {
162
+ let code = "";
163
+ let object;
164
+ if (typeof source === "string") {
165
+ code = (await buildProgram(source, { typecheck: options.typecheck ?? true })).code;
166
+ }
167
+ else if ("code" in source && typeof source.code === "string") {
168
+ code = source.code;
169
+ }
170
+ else {
171
+ object = source;
172
+ }
173
+ const probe = loadContext(code, object, () => ({ maxDepth: 16 }));
174
+ const description = JSON.parse(probe.describe());
175
+ if (description.sdk !== "0.2")
176
+ throw new Error("sdk_version_unsupported: build the program with defineProgram of @johannc-dev/zyrk 0.2");
177
+ return new LocalRunner(code, object, description.functions, description.subscriptions, options);
178
+ }
179
+ #stateFile() {
180
+ const dir = this.#options.state;
181
+ return typeof dir === "string"
182
+ ? join(dir, `${this.#name}.json`)
183
+ : undefined;
184
+ }
185
+ #saveState() {
186
+ const file = this.#stateFile();
187
+ if (!file)
188
+ return;
189
+ mkdirSync(dirname(file), { recursive: true });
190
+ writeFileSync(`${file}.tmp`, JSON.stringify(Object.fromEntries(this.state)));
191
+ renameSync(`${file}.tmp`, file);
192
+ }
193
+ #warn(message) {
194
+ if (this.#warned.has(message))
195
+ return;
196
+ this.#warned.add(message);
197
+ this.warnings.push(message);
198
+ }
199
+ #create(id, chain, workflow, fn, args, depth, onComplete, scale = 1) {
200
+ let finish = () => { };
201
+ const done = new Promise((r) => (finish = r));
202
+ const e = {
203
+ id,
204
+ chain,
205
+ workflow,
206
+ function: fn,
207
+ args,
208
+ depth,
209
+ onComplete,
210
+ status: "queued",
211
+ logs: [],
212
+ emits: [],
213
+ webhooks: [],
214
+ webhookCalls: 0,
215
+ scale,
216
+ usage: {
217
+ cpuMs: 0,
218
+ readMs: 0,
219
+ wallMs: 0,
220
+ readBytes: 0,
221
+ readCalls: 0,
222
+ hostOps: 0,
223
+ outputBytes: 0,
224
+ peakHeapBytes: 0,
225
+ },
226
+ children: [],
227
+ state: new Map(),
228
+ subs: new Map(),
229
+ readOver: false,
230
+ pending: 0,
231
+ io: { bytes: 0 },
232
+ startedAt: 0,
233
+ done,
234
+ finish,
235
+ };
236
+ if (!this.#workflows.has(workflow))
237
+ this.#workflows.set(workflow, { readBytes: 0, executions: 0 });
238
+ return e;
239
+ }
240
+ #schedule(e) {
241
+ if (e.id === e.chain)
242
+ this.#jobs.set(e.id, e);
243
+ this.#queue.push(e);
244
+ this.#pump();
245
+ }
246
+ #pump() {
247
+ while (this.#running < this.limits.account.parallelExecutions &&
248
+ this.#queue.length > 0) {
249
+ const e = this.#queue.shift();
250
+ this.#running++;
251
+ void this.#execute(e).finally(() => {
252
+ this.#running--;
253
+ this.#pump();
254
+ if (this.#running === 0 && this.#queue.length === 0) {
255
+ const waiting = this.#idle;
256
+ this.#idle = [];
257
+ for (const w of waiting)
258
+ w();
259
+ }
260
+ });
261
+ }
262
+ }
263
+ #cancelChildren(e) {
264
+ for (const id of e.children) {
265
+ const child = this.#jobs.get(id);
266
+ if (!child)
267
+ continue;
268
+ for (let x = child; x; x = x.next) {
269
+ if (x.status === "queued") {
270
+ const at = this.#queue.indexOf(x);
271
+ if (at >= 0)
272
+ this.#queue.splice(at, 1);
273
+ x.status = "cancelled";
274
+ x.error = {
275
+ kind: "permission",
276
+ code: "cancelled",
277
+ message: "Its parent failed: the job was cancelled",
278
+ };
279
+ x.finish();
280
+ this.#cancelChildren(x);
281
+ }
282
+ else if (x.status === "running")
283
+ x.cancelled = true;
284
+ }
285
+ }
286
+ }
287
+ idle() {
288
+ if (this.#running === 0 && this.#queue.length === 0)
289
+ return Promise.resolve();
290
+ return new Promise((r) => this.#idle.push(r));
291
+ }
292
+ jobs() {
293
+ return [...this.#jobs.values()].map(jobResult);
294
+ }
295
+ executions() {
296
+ const out = [];
297
+ for (const e of this.#jobs.values())
298
+ for (let x = e; x; x = x.next)
299
+ out.push(view(x));
300
+ return out;
301
+ }
302
+ async run(fn, args = null) {
303
+ return this.#run(fn, args, 1);
304
+ }
305
+ async #run(fn, args, scale) {
306
+ const id = `local-${++this.#runs}`;
307
+ const e = this.#create(id, id, id, fn, args, 0, null, scale);
308
+ this.#schedule(e);
309
+ return this.#settled(e);
310
+ }
311
+ async #settled(e) {
312
+ await chainDone(e);
313
+ const chain = [];
314
+ for (let x = e; x; x = x.next)
315
+ chain.push(x);
316
+ const f = chain[chain.length - 1];
317
+ return {
318
+ id: e.chain,
319
+ status: f.status === "succeeded"
320
+ ? "succeeded"
321
+ : f.status === "cancelled"
322
+ ? "cancelled"
323
+ : "failed",
324
+ ...(f.status === "succeeded"
325
+ ? { result: f.result ?? null, resultText: f.resultText ?? "null" }
326
+ : {}),
327
+ ...(f.error ? { error: f.error } : {}),
328
+ logs: chain.flatMap((x) => x.logs),
329
+ emits: chain.flatMap((x) => x.emits),
330
+ webhooks: chain.flatMap((x) => x.webhooks),
331
+ executions: chain.map(view),
332
+ warnings: [...this.warnings],
333
+ };
334
+ }
335
+ async #execute(e) {
336
+ e.status = "running";
337
+ e.startedAt = performance.now();
338
+ const cpu0 = process.cpuUsage();
339
+ const heap0 = getHeapStatistics().used_heap_size;
340
+ const wf = this.#workflows.get(e.workflow);
341
+ wf.executions++;
342
+ const sample = () => {
343
+ const used = getHeapStatistics().used_heap_size - heap0;
344
+ if (used > e.usage.peakHeapBytes)
345
+ e.usage.peakHeapBytes = used;
346
+ };
347
+ try {
348
+ if (wf.executions > this.limits.account.workflowAttempts)
349
+ throw limit("workflow_budget_exhausted", "Workflow budget exhausted");
350
+ const argsText = JSON.stringify(e.args) ?? "null";
351
+ const inputBytes = e.args?.kind === "events"
352
+ ? this.limits.exec.eventInputBytes * e.scale
353
+ : this.limits.exec.inputBytes;
354
+ if (Buffer.byteLength(argsText) > inputBytes)
355
+ throw limit("input_limit", `The arguments are larger than ${inputBytes} bytes`);
356
+ const guest = loadContext(this.#code, this.#object, (realm, mode) => this.#natives(e, realm, mode, sample));
357
+ const text = await guest.start(e.function, argsText, JSON.stringify({
358
+ id: e.id,
359
+ workflow: e.workflow,
360
+ program: this.#name,
361
+ version: this.#version,
362
+ attempt: 1,
363
+ }));
364
+ if (e.fatal)
365
+ throw e.fatal;
366
+ if (e.pending > 0)
367
+ throw program("unawaited_operation", "The function returned while a ctx call was still pending: await every ctx call");
368
+ const resultText = String(text);
369
+ const size = Buffer.byteLength(resultText);
370
+ if (e.usage.outputBytes + size > this.limits.exec.outputBytes * e.scale)
371
+ throw limit("output_limit", `Result, logs and emits went over ${this.limits.exec.outputBytes * e.scale} bytes`);
372
+ e.usage.outputBytes += size;
373
+ if (performance.now() - e.startedAt > this.limits.exec.wallMs)
374
+ throw limit("wall_limit", `The execution ran over ${this.limits.exec.wallMs} ms (measured locally, not preempted)`);
375
+ if (e.cancelled)
376
+ throw new HostError("permission", "cancelled", "Its parent failed: the job was cancelled");
377
+ for (const [k, v] of e.state)
378
+ if (v === undefined)
379
+ this.state.delete(k);
380
+ else
381
+ this.state.set(k, v);
382
+ for (const [k, s] of e.subs)
383
+ if (s === null)
384
+ this.subscriptions.delete(k);
385
+ else
386
+ this.subscriptions.set(k, s);
387
+ if (e.state.size > 0)
388
+ this.#saveState();
389
+ e.result = JSON.parse(resultText);
390
+ e.resultText = resultText;
391
+ e.status = "succeeded";
392
+ }
393
+ catch (error) {
394
+ const failure = e.fatal ??
395
+ (error instanceof HostError
396
+ ? error
397
+ : (this.#errors.get(error) ??
398
+ new HostError("program", "program_error", String(error?.stack ??
399
+ error?.message ??
400
+ error))));
401
+ e.status = "failed";
402
+ e.error = {
403
+ kind: failure.kind,
404
+ code: failure.code,
405
+ message: truncate(failure.message),
406
+ };
407
+ }
408
+ finally {
409
+ this.storage.endExecution(e.id);
410
+ sample();
411
+ const cpu = process.cpuUsage(cpu0);
412
+ e.usage.wallMs = performance.now() - e.startedAt;
413
+ e.usage.cpuMs = Math.max(0, (cpu.user + cpu.system) / 1000 - e.usage.readMs);
414
+ }
415
+ if (e.error?.code === "cancelled")
416
+ e.status = "cancelled";
417
+ if (e.status === "failed" || e.status === "cancelled")
418
+ this.#cancelChildren(e);
419
+ if (e.usage.cpuMs > this.limits.exec.cpuMs)
420
+ this.#warn(`${e.function}: about ${Math.round(e.usage.cpuMs)} ms of CPU, over the service's ${this.limits.exec.cpuMs} ms (cpu_limit). Measured roughly here: check with zyrk run.`);
421
+ if (e.usage.peakHeapBytes > this.limits.exec.heapBytes)
422
+ this.#warn(`${e.function}: about ${Math.round(e.usage.peakHeapBytes / 2 ** 20)} MiB of heap, over the service's ${this.limits.exec.heapBytes / 2 ** 20} MiB (memory_limit). Measured roughly here.`);
423
+ if (e.status === "succeeded" && e.continuation) {
424
+ const next = this.#create(e.continuation.id, e.chain, e.workflow, e.function, e.continuation.args, e.depth, e.onComplete, e.scale);
425
+ e.next = next;
426
+ e.finish();
427
+ this.#queue.push(next);
428
+ return;
429
+ }
430
+ e.finish();
431
+ const head = this.#jobs.get(e.chain);
432
+ if (head?.onComplete) {
433
+ const status = final(head).status;
434
+ const id = `${e.chain}:complete`;
435
+ this.#schedule(this.#create(id, id, e.workflow, head.onComplete, { kind: "completion", executionId: e.chain, status }, head.depth, null));
436
+ }
437
+ }
438
+ #natives(e, realm, mode, sample) {
439
+ const sdkError = (err) => {
440
+ const error = new realm.Error(err.message);
441
+ error.code = err.code;
442
+ if (err.retryAfterMs !== undefined)
443
+ error.retryAfterMs = err.retryAfterMs;
444
+ this.#errors.set(error, err);
445
+ return error;
446
+ };
447
+ const fromRead = (f) => new HostError(kindOfSdkCode(f.code), f.code, f.message, f.retryAfterMs);
448
+ const guestJson = (v) => realm.JSON.parse(JSON.stringify(v) ?? "null");
449
+ const ms = (v) => typeof v === "number" && Number.isFinite(v) && Math.abs(v) < 9e15
450
+ ? Math.trunc(v)
451
+ : undefined;
452
+ const readLimit = () => {
453
+ e.readOver = true;
454
+ return sdkError(limit("read_limit", `History reads went over ${this.limits.exec.readBytes} bytes`));
455
+ };
456
+ const readAllowed = () => {
457
+ if (mode() === "load")
458
+ throw sdkError(program("invalid_program", "History reads are not available at load"));
459
+ if (e.readOver || e.usage.readBytes >= this.limits.exec.readBytes)
460
+ throw readLimit();
461
+ if (e.usage.readMs > this.limits.exec.readMs) {
462
+ e.fatal = limit("read_time_limit", `History reads took over ${this.limits.exec.readMs} ms`);
463
+ throw sdkError(e.fatal);
464
+ }
465
+ this.#checkWall(e);
466
+ };
467
+ const charge = (bytes) => {
468
+ e.usage.readBytes += bytes;
469
+ const wf = this.#workflows.get(e.workflow);
470
+ wf.readBytes += bytes;
471
+ if (e.usage.readBytes > this.limits.exec.readBytes ||
472
+ wf.readBytes > this.limits.account.workflowReadBytes)
473
+ throw readLimit();
474
+ };
475
+ const data = () => {
476
+ if (!this.data)
477
+ throw sdkError(program("local_data_missing", "No data directory: export history with zyrk export, then pass --data (or { data })."));
478
+ return this.data;
479
+ };
480
+ const saved = (answer, failure, missing) => {
481
+ let value;
482
+ let failed;
483
+ try {
484
+ value = answer();
485
+ failed = value === undefined ? failure() : undefined;
486
+ }
487
+ catch (error) {
488
+ if (error instanceof ReadFailed)
489
+ throw sdkError(fromRead(error.failure));
490
+ throw error;
491
+ }
492
+ if (failed)
493
+ throw sdkError(fromRead(failed));
494
+ if (value === undefined)
495
+ throw sdkError(program("local_data_missing", missing));
496
+ if (openWhenExported(value))
497
+ this.#warn("Totals or volume search pages were exported while an hour was still open: the service may now answer otherwise. Run zyrk export again (--totals, --volume-search) to download them again.");
498
+ charge(jsonBytes(value));
499
+ return guestJson(value);
500
+ };
501
+ return {
502
+ maxDepth: this.limits.account.maxDepth,
503
+ batchReads: batchReadsFromEnv(),
504
+ hint: () => undefined,
505
+ fail: (code, message) => sdkError(new HostError(kindOfSdkCode(String(code)), String(code), String(message))),
506
+ op: (text, awaited) => this.#op(e, text, !!awaited, realm, mode, sdkError, guestJson, sample),
507
+ read: (kind, key, since, until, side, allowPartial, withSignatures, after, withMarket, withEvents, withOrder, withTx) => {
508
+ if (typeof key !== "string")
509
+ throw sdkError(program("invalid_argument", "Expected an address string"));
510
+ if (kind !== "wallet" && kind !== "token")
511
+ throw sdkError(program("invalid_argument", "Unknown history scope"));
512
+ const sinceMs = ms(since);
513
+ const untilMs = ms(until);
514
+ if (sinceMs === undefined || untilMs === undefined)
515
+ throw sdkError(program("invalid_window", "sinceMs and untilMs must be finite numbers"));
516
+ if (side !== "any" && side !== "buy" && side !== "sell")
517
+ throw sdkError(program("invalid_argument", 'side must be "buy" or "sell"'));
518
+ const afterHour = after === null || after === undefined ? null : ms(after);
519
+ if (afterHour === undefined)
520
+ throw sdkError(program("invalid_argument", "Invalid read cursor"));
521
+ readAllowed();
522
+ e.usage.readCalls++;
523
+ const t0 = performance.now();
524
+ let hour;
525
+ try {
526
+ hour = data().readHour(readSpec({
527
+ scope: kind,
528
+ key,
529
+ side,
530
+ withSignatures: withSignatures === true,
531
+ withMarket: withMarket === true,
532
+ withEvents: withEvents === true,
533
+ withOrder: withOrder === true,
534
+ withTx: withTx === true,
535
+ }), sinceMs, untilMs, allowPartial === true, afterHour, realm);
536
+ }
537
+ catch (error) {
538
+ if (error instanceof ReadFailed)
539
+ throw sdkError(fromRead(error.failure));
540
+ throw error;
541
+ }
542
+ finally {
543
+ e.usage.readMs += performance.now() - t0;
544
+ sample();
545
+ }
546
+ if (hour === null)
547
+ return null;
548
+ if (hour.stale)
549
+ this.#warn("Some hours were open or not sealed when they were exported, or were exported by an older version: the service may now answer otherwise. Run zyrk export again to download them again.");
550
+ if (hour.recut)
551
+ this.#warn("A read used a narrower window than its export: its edge hours were cut locally. Values are the service's; the address indexes and market.flags (chainBreak, chainUnchecked) of a cut hour may differ. Export the exact window for byte-identical batches.");
552
+ charge(hour.bytes);
553
+ return hour.batch;
554
+ },
555
+ totals: (mint, quote) => {
556
+ readAllowed();
557
+ e.usage.readCalls++;
558
+ if (typeof mint !== "string" ||
559
+ (quote !== null && typeof quote !== "string"))
560
+ throw sdkError(program("invalid_argument", "Expected a mint address"));
561
+ return saved(() => data().totals(mint, quote), () => data().totalsFailure(mint, quote), `No exported totals for ${mint}: zyrk export --totals ${mint}${quote ? ` --quote ${quote}` : ""}`);
562
+ },
563
+ info: (mint) => {
564
+ readAllowed();
565
+ e.usage.readCalls++;
566
+ if (typeof mint !== "string")
567
+ throw sdkError(program("invalid_argument", "Expected a mint address"));
568
+ return saved(() => data().info(mint), () => data().infoFailure(mint), `No exported info for ${mint} (exports by older versions saved only its totals): zyrk export --totals ${mint}`);
569
+ },
570
+ volume: (text) => {
571
+ readAllowed();
572
+ e.usage.readCalls++;
573
+ const params = JSON.parse(String(text));
574
+ return saved(() => data().volumePage(params), () => data().volumeFailure(params), `No exported volume page for these parameters: zyrk export --volume-search '${JSON.stringify(Object.fromEntries(params.filter(([k]) => k !== "cursor")))}'`);
575
+ },
576
+ encode: (text) => {
577
+ const bytes = Buffer.from(String(text), "utf8");
578
+ const out = new realm.Uint8Array(bytes.length);
579
+ out.set(bytes);
580
+ return out;
581
+ },
582
+ decode: (view, fatal, ignoreBOM) => new TextDecoder("utf-8", {
583
+ fatal: !!fatal,
584
+ ignoreBOM: !!ignoreBOM,
585
+ }).decode(new Uint8Array(view.buffer, view.byteOffset, view.byteLength)),
586
+ random: (array) => {
587
+ randomFillSync(new Uint8Array(array.buffer, array.byteOffset, array.byteLength));
588
+ },
589
+ b64encode: (view) => {
590
+ if (view.byteLength > 4 * 1024 * 1024)
591
+ throw new realm.Error("b64encode is limited to 4 MiB");
592
+ return Buffer.from(view.buffer, view.byteOffset, view.byteLength).toString("base64");
593
+ },
594
+ b64decodeInto: (text, out, offset) => {
595
+ if (typeof text !== "string")
596
+ return -1;
597
+ const bytes = Buffer.from(text, "base64");
598
+ if (offset + bytes.length > out.length)
599
+ return -1;
600
+ new Uint8Array(out.buffer, out.byteOffset, out.byteLength).set(bytes, offset);
601
+ return bytes.length;
602
+ },
603
+ };
604
+ }
605
+ #checkWall(e) {
606
+ if (performance.now() - e.startedAt > this.limits.exec.wallMs) {
607
+ e.fatal ??= limit("wall_limit", `The execution ran over ${this.limits.exec.wallMs} ms`);
608
+ throw e.fatal;
609
+ }
610
+ }
611
+ #op(e, text, awaited, realm, mode, sdkError, guestJson, sample) {
612
+ const reject = (err) => awaited ? realm.Promise.reject(sdkError(err)) : undefined;
613
+ if (mode() === "load")
614
+ return reject(program("invalid_program", "Context operations are not available at load"));
615
+ if (e.usage.hostOps >= this.limits.exec.hostOps * e.scale)
616
+ return reject(limit("op_limit", `More than ${this.limits.exec.hostOps * e.scale} context operations`));
617
+ const cap = /^\{"Storage/.test(String(text))
618
+ ? MAX_STORAGE_OP_BYTES
619
+ : MAX_OP_BYTES;
620
+ if (typeof text !== "string" || text.length > cap)
621
+ return reject(program("invalid_argument", "Context operation payload is too large"));
622
+ let name;
623
+ let payload;
624
+ try {
625
+ const op = JSON.parse(text);
626
+ name = Object.keys(op)[0];
627
+ payload = op[name];
628
+ if (typeof payload !== "object" || payload === null)
629
+ throw new Error();
630
+ }
631
+ catch {
632
+ return reject(program("invalid_argument", "Invalid context operation"));
633
+ }
634
+ e.usage.hostOps++;
635
+ if (awaited)
636
+ e.pending++;
637
+ const answer = new realm.Promise((resolve, rejectGuest) => {
638
+ this.#hostOp(e, name, payload).then((value) => {
639
+ if (awaited)
640
+ e.pending--;
641
+ sample();
642
+ resolve(guestJson(value));
643
+ }, (error) => {
644
+ if (awaited)
645
+ e.pending--;
646
+ const err = error instanceof HostError
647
+ ? error
648
+ : error instanceof StorageOpError
649
+ ? new HostError(error.kind, error.code, error.message)
650
+ : new HostError("infrastructure", "runtime_failure", String(error));
651
+ rejectGuest(sdkError(err));
652
+ });
653
+ });
654
+ if (awaited)
655
+ return answer;
656
+ answer.catch(() => { });
657
+ return undefined;
658
+ }
659
+ async #hostOp(e, name, a) {
660
+ this.#checkWall(e);
661
+ const account = this.limits.account;
662
+ const output = (size) => {
663
+ e.usage.outputBytes += size;
664
+ if (e.usage.outputBytes > this.limits.exec.outputBytes * e.scale)
665
+ throw limit("output_limit", `Result, logs and emits went over ${this.limits.exec.outputBytes * e.scale} bytes`);
666
+ };
667
+ const key = (k) => {
668
+ if (typeof k !== "string" || !NAME.test(k))
669
+ throw program("invalid_sdk_request", "key: expected ^[A-Za-z][A-Za-z0-9_-]{0,63}$");
670
+ return k;
671
+ };
672
+ const print = (kind, value) => {
673
+ const c = this.#options.console;
674
+ if (typeof c === "function")
675
+ c({ kind, execution: e.id, function: e.function, value });
676
+ else if (c)
677
+ process.stderr.write(`[${kind} ${e.function} ${e.id}] ${typeof value === "string" ? value : JSON.stringify(value)}\n`);
678
+ };
679
+ switch (name) {
680
+ case "StateGet": {
681
+ const k = key(a.key);
682
+ if (e.state.has(k))
683
+ return e.state.get(k) ?? null;
684
+ return this.state.get(k) ?? null;
685
+ }
686
+ case "StateSet": {
687
+ const k = key(a.key);
688
+ const value = (a.value ?? null);
689
+ if (jsonBytes(value) > account.stateValueBytes)
690
+ throw limit("state_value_too_large", "A state value is limited to 1 MiB");
691
+ e.state.set(k, value);
692
+ const merged = new Map(this.state);
693
+ for (const [x, v] of e.state)
694
+ if (v === undefined)
695
+ merged.delete(x);
696
+ else
697
+ merged.set(x, v);
698
+ let total = 0;
699
+ for (const [x, v] of merged)
700
+ total += jsonBytes(v) + x.length;
701
+ if (total > account.stateBytesPerProgram)
702
+ throw limit("state_too_large", "The state of a program is limited to 32 MiB");
703
+ return null;
704
+ }
705
+ case "StateDelete":
706
+ e.state.set(key(a.key), undefined);
707
+ return null;
708
+ case "Log": {
709
+ const value = (a.value ?? null);
710
+ output(jsonBytes(value) + 1);
711
+ e.logs.push(value);
712
+ print("log", value);
713
+ return null;
714
+ }
715
+ case "Emit": {
716
+ const value = (a.value ?? null);
717
+ output(jsonBytes(value) + EMIT_OVERHEAD_BYTES);
718
+ e.emits.push(value);
719
+ print("emit", value);
720
+ return null;
721
+ }
722
+ case "WebhookSend": {
723
+ if (e.webhookCalls >= WEBHOOK_MAX_PER_EXECUTION)
724
+ throw limit("webhook_limit", `At most ${WEBHOOK_MAX_PER_EXECUTION} ctx.webhook.send calls per execution`);
725
+ const payload = (a.payload ?? null);
726
+ const size = jsonBytes(payload);
727
+ if (size > WEBHOOK_PAYLOAD_BYTES)
728
+ throw limit("webhook_payload_too_large", `A webhook payload is limited to ${WEBHOOK_PAYLOAD_BYTES} bytes`);
729
+ output(size + WEBHOOK_OVERHEAD_BYTES);
730
+ const index = e.webhookCalls++;
731
+ if (this.#isolated) {
732
+ e.webhooks.push(payload);
733
+ print("webhook", payload);
734
+ return { id: null, seq: null, dropped: true, replayed: true };
735
+ }
736
+ const hook = this.#options.webhook;
737
+ if (!hook?.configured || hook.enabled === false)
738
+ throw program("webhook_not_configured", "No enabled webhook for this account: PUT /v1/functions/webhook first");
739
+ const queueLimit = hook.queueLimit ?? WEBHOOK_QUEUE_LIMIT;
740
+ if (this.#webhookPending >= queueLimit) {
741
+ if (a.drop_if_full === true)
742
+ return { id: null, seq: null, dropped: true };
743
+ throw limit("webhook_queue_full", `${this.#webhookPending} webhook messages are waiting for delivery (the limit): ` +
744
+ "check the receiver and GET /v1/functions/webhook/deliveries");
745
+ }
746
+ this.#webhookPending++;
747
+ this.#webhookSeq++;
748
+ e.webhooks.push(payload);
749
+ print("webhook", payload);
750
+ return {
751
+ id: `${e.id}~webhook-${index}`,
752
+ seq: this.#webhookSeq,
753
+ dropped: false,
754
+ };
755
+ }
756
+ case "Replay":
757
+ if (this.#isolated)
758
+ throw program("replay_isolated", "A replay never changes live subscriptions nor starts another replay");
759
+ throw program("local_unsupported", "ctx.subscriptions.replay is not run locally: use runner.replay(...) or zyrk dev --replay");
760
+ case "Continue": {
761
+ if (e.continuation)
762
+ throw program("continuation_already_requested", "ctx.jobs.continue was already called in this execution");
763
+ const args = (a.args ?? null);
764
+ if (jsonBytes(args) > this.limits.exec.inputBytes)
765
+ throw limit("continuation_state_too_large", "Continuation arguments over 32 KiB");
766
+ const id = `${e.id}~continue`;
767
+ e.continuation = { args, id };
768
+ return id;
769
+ }
770
+ case "JobStart": {
771
+ const fn = a.function;
772
+ if (typeof fn !== "string" || !this.#functions.has(fn))
773
+ throw program("unknown_function", `Unknown function: ${String(fn)}`);
774
+ const onComplete = (a.on_complete ?? null);
775
+ if (onComplete !== null && !this.#functions.has(onComplete))
776
+ throw program("unknown_function", `Unknown function: ${onComplete}`);
777
+ if (e.children.length >= account.maxChildren ||
778
+ e.depth + 1 > account.maxDepth)
779
+ throw limit("child_execution_limit", `At most ${account.maxChildren} jobs per execution, depth ${account.maxDepth}`);
780
+ const args = (a.args ?? null);
781
+ if (jsonBytes(args) > this.limits.exec.inputBytes)
782
+ throw limit("input_too_large", "Job arguments over 32 KiB");
783
+ const id = `${e.chain}.${e.children.length}`;
784
+ e.children.push(id);
785
+ this.#schedule(this.#create(id, id, e.workflow, fn, args, e.depth + 1, onComplete));
786
+ return id;
787
+ }
788
+ case "JobWait": {
789
+ const ids = (Array.isArray(a.ids) ? a.ids : []);
790
+ const jobs = ids.map((id) => {
791
+ const j = typeof id === "string" ? this.#jobs.get(id) : undefined;
792
+ if (!j)
793
+ throw program("execution_not_found", `Unknown execution: ${String(id)}`);
794
+ return j;
795
+ });
796
+ if (this.#waiting >= account.parallelExecutions - 1)
797
+ throw limit("job_wait_limit", `At most ${account.parallelExecutions - 1} executions wait at once`);
798
+ const left = this.limits.exec.wallMs - (performance.now() - e.startedAt);
799
+ const timeout = Math.max(0, Math.min(typeof a.timeout_ms === "number" ? a.timeout_ms : left, left));
800
+ this.#waiting++;
801
+ let timer;
802
+ try {
803
+ await Promise.race([
804
+ Promise.all(jobs.map((j) => chainDone(j))),
805
+ new Promise((r) => (timer = setTimeout(r, timeout))),
806
+ ]);
807
+ }
808
+ finally {
809
+ clearTimeout(timer);
810
+ this.#waiting--;
811
+ }
812
+ return jobs.map(jobResult);
813
+ }
814
+ case "JobResult": {
815
+ const j = typeof a.id === "string" ? this.#jobs.get(a.id) : undefined;
816
+ if (!j)
817
+ throw program("execution_not_found", `Unknown execution: ${String(a.id)}`);
818
+ return jobResult(j);
819
+ }
820
+ case "Subscribe":
821
+ case "Unsubscribe":
822
+ if (this.#isolated)
823
+ throw program("replay_isolated", "A replay cannot change subscriptions");
824
+ }
825
+ switch (name) {
826
+ case "Subscribe": {
827
+ const n = a.name;
828
+ if (typeof n !== "string" || !NAME.test(n))
829
+ throw program("invalid_sdk_request", "invalid subscription name");
830
+ const s = a.subscription;
831
+ if (!s || typeof s !== "object" || !this.#functions.has(s.function))
832
+ throw program("invalid_sdk_request", "invalid subscription");
833
+ const kinds = [
834
+ "trade",
835
+ "volume",
836
+ "priceVariation",
837
+ "interval",
838
+ "poolEvent",
839
+ ];
840
+ if (!s.trigger || !kinds.includes(s.trigger.kind))
841
+ throw program("invalid_sdk_request", "invalid subscription trigger");
842
+ const after = new Map(this.subscriptions);
843
+ for (const [k, v] of e.subs)
844
+ after.set(k, v);
845
+ after.set(n, s);
846
+ const active = [...after.values()].filter((v) => v !== null);
847
+ if (active.length > account.maxSubscriptions)
848
+ throw limit("subscription_limit", "At most 64 subscriptions");
849
+ if (active.filter((v) => v.trigger.kind === "priceVariation").length >
850
+ account.maxPriceSubscriptions)
851
+ throw limit("price_subscription_limit", "At most 8 price subscriptions");
852
+ e.subs.set(n, s);
853
+ return null;
854
+ }
855
+ case "Unsubscribe": {
856
+ const n = a.name;
857
+ if (typeof n !== "string" || !NAME.test(n))
858
+ throw program("invalid_sdk_request", "invalid subscription name");
859
+ e.subs.set(n, null);
860
+ return null;
861
+ }
862
+ default:
863
+ if (name.startsWith("Storage"))
864
+ return this.storage.op(name, a, e.id, e.io);
865
+ throw program("invalid_argument", `Invalid context operation: ${name}`);
866
+ }
867
+ }
868
+ async replay(options) {
869
+ const s = this.subscriptions.get(options.subscription);
870
+ if (!s)
871
+ throw new Error(`Unknown subscription ${options.subscription}: declare it in subscriptions or set it with ctx.subscriptions.set`);
872
+ const progress = {
873
+ eventsRead: 0,
874
+ eventsDelivered: 0,
875
+ deliveries: 0,
876
+ gaps: 0,
877
+ orderFallbacks: 0,
878
+ };
879
+ const warnings = [];
880
+ const pending = this.#replayEvents(s, options, progress, warnings);
881
+ if (options.batchSize !== undefined &&
882
+ (!Number.isInteger(options.batchSize) ||
883
+ options.batchSize < 1 ||
884
+ options.batchSize > REPLAY_MAX_BATCH))
885
+ throw new Error(`batchSize: 1 to ${REPLAY_MAX_BATCH} events`);
886
+ if (options.batchSize !== undefined && s.trigger.kind === "interval")
887
+ throw new Error("batchSize: an interval replay delivers one tick at a time");
888
+ const per = s.trigger.kind === "interval"
889
+ ? 1
890
+ : (options.batchSize ??
891
+ Math.min(Math.max(1, options.eventsPerDelivery ?? DELIVERY_BATCH), subscriptionLimits.eventsPerDelivery));
892
+ const scale = Math.ceil(per / DELIVERY_BATCH);
893
+ const keep = options.keepStorage && checkKeep(options.keepStorage);
894
+ const short = this.storage.areas.short;
895
+ const scratch = new LocalStorage(undefined, this.storage.areas, this.storage.now, short
896
+ ? {
897
+ quotaBytes: short.quotaBytes,
898
+ outside: () => this.storage.usedBytes("short"),
899
+ }
900
+ : undefined);
901
+ const isolated = new LocalRunner(this.#code, this.#object, [...this.#functions], {}, {
902
+ ...this.#options,
903
+ state: {},
904
+ storage: scratch,
905
+ data: this.data ?? "",
906
+ limits: this.limits,
907
+ });
908
+ isolated.#isolated = true;
909
+ const id = `replay-${++this.#runs}`;
910
+ const runs = [];
911
+ const emits = [];
912
+ const feedLimit = options.feedBytes ?? REPLAY_FEED_BYTES;
913
+ let feedBytes = 0;
914
+ let feedSeq = 0;
915
+ const send = (item) => {
916
+ const sent = { ...item, seq: ++feedSeq };
917
+ if (options.onFeed)
918
+ return options.onFeed(sent);
919
+ feedBytes += jsonBytes(sent);
920
+ if (feedBytes > feedLimit)
921
+ throw new HostError("limit", "replay_feed_full", `The emits and results of this replay went over ${feedLimit} bytes without a reader: pass onFeed to receive them as they come`);
922
+ };
923
+ const deadline = performance.now() + (options.maxDurationMs ?? REPLAY_MAX_DURATION_MS);
924
+ let error;
925
+ for (let i = 0; i < pending.length; i += per) {
926
+ if (options.maxDeliveries !== undefined &&
927
+ progress.deliveries >= options.maxDeliveries)
928
+ break;
929
+ if (performance.now() >= deadline) {
930
+ error = {
931
+ kind: "limit",
932
+ code: "replay_duration_limit",
933
+ message: `A replay runs at most ${(options.maxDurationMs ?? REPLAY_MAX_DURATION_MS) / 60_000} minutes: split the window`,
934
+ };
935
+ break;
936
+ }
937
+ const chunk = pending.slice(i, i + per);
938
+ const clock = chunk[chunk.length - 1].time;
939
+ progress.eventsDelivered += chunk.filter((p) => !p.gap).length;
940
+ progress.deliveries++;
941
+ const run = await isolated.#run(s.function, {
942
+ kind: "events",
943
+ subscription: options.subscription,
944
+ events: chunk.map((p) => p.event),
945
+ replay: {
946
+ id,
947
+ delivery: progress.deliveries,
948
+ clockMs: clock,
949
+ sinceMs: options.sinceMs,
950
+ untilMs: options.untilMs,
951
+ simulated: true,
952
+ },
953
+ }, scale);
954
+ await isolated.idle();
955
+ try {
956
+ for (const value of run.emits)
957
+ send({ kind: "emitted", data: value });
958
+ send({
959
+ kind: "execution",
960
+ execution: {
961
+ id: run.id,
962
+ status: run.status,
963
+ ...(run.result !== undefined ? { result: run.result } : {}),
964
+ ...(run.error ? { error: run.error } : {}),
965
+ logs: run.logs,
966
+ },
967
+ });
968
+ }
969
+ catch (e) {
970
+ if (!(e instanceof HostError))
971
+ throw e;
972
+ error = { kind: e.kind, code: e.code, message: e.message };
973
+ break;
974
+ }
975
+ if (!options.onFeed) {
976
+ runs.push(run);
977
+ emits.push(...run.emits);
978
+ }
979
+ if (run.status !== "succeeded") {
980
+ error = {
981
+ kind: run.error?.kind ?? "program",
982
+ code: "delivery_failed",
983
+ message: `Delivery ${progress.deliveries} failed: ${run.error?.code ?? run.status}`,
984
+ };
985
+ break;
986
+ }
987
+ }
988
+ for (const w of isolated.warnings)
989
+ if (!warnings.includes(w))
990
+ warnings.push(w);
991
+ let keptStorage;
992
+ if (keep && !error) {
993
+ try {
994
+ const copied = this.storage.copyFrom(scratch, keep.from ?? keep.to, keep.to, keep.keys);
995
+ keptStorage = { copied: true, ...copied };
996
+ }
997
+ catch (e) {
998
+ const f = e;
999
+ keptStorage = {
1000
+ copied: false,
1001
+ error: {
1002
+ kind: (f.kind ?? "limit"),
1003
+ code: f.code ?? "quota_exceeded",
1004
+ message: f.message ?? String(e),
1005
+ },
1006
+ };
1007
+ }
1008
+ }
1009
+ return {
1010
+ id,
1011
+ status: error ? "failed" : "succeeded",
1012
+ ...(error ? { error } : {}),
1013
+ runs,
1014
+ state: new Map(isolated.state),
1015
+ emits,
1016
+ progress,
1017
+ warnings,
1018
+ ...(keptStorage ? { keptStorage } : {}),
1019
+ };
1020
+ }
1021
+ replayEvents(s, o) {
1022
+ return this.#replayEvents(s, o, {
1023
+ eventsRead: 0,
1024
+ eventsDelivered: 0,
1025
+ deliveries: 0,
1026
+ gaps: 0,
1027
+ orderFallbacks: 0,
1028
+ }, []);
1029
+ }
1030
+ #replayEvents(s, o, progress, warnings) {
1031
+ const t = s.trigger;
1032
+ if (t.kind === "interval")
1033
+ return intervalEvents(t.everyMs, o.sinceMs, o.untilMs);
1034
+ if (t.kind === "priceVariation")
1035
+ throw new Error("priceVariation signals come from the service's price engine: replay them with client.replay (they cannot be rebuilt from exported trades)");
1036
+ if (!this.data)
1037
+ throw new Error("replay needs exported history (data)");
1038
+ const wallet = t.kind === "trade" ? t.wallet : undefined;
1039
+ const scope = t.mint
1040
+ ? "token"
1041
+ : wallet
1042
+ ? "wallet"
1043
+ : undefined;
1044
+ const key = t.mint ?? wallet;
1045
+ if (!scope || !key)
1046
+ throw new Error("A local replay needs a trigger with a mint (or a wallet for trade): market-wide triggers cannot be exported");
1047
+ const events = t.kind === "poolEvent";
1048
+ const sinceCreation = t.kind === "volume" && t.windowMs === undefined;
1049
+ let reads;
1050
+ try {
1051
+ reads = this.data.reads();
1052
+ }
1053
+ catch (e) {
1054
+ throw new Error(e instanceof Error ? e.message : String(e));
1055
+ }
1056
+ const meta = reads
1057
+ .filter((r) => r.scope === scope &&
1058
+ r.key === key &&
1059
+ r.side === "any" &&
1060
+ r.sinceMs <= o.sinceMs &&
1061
+ o.untilMs <= r.untilMs &&
1062
+ r.done &&
1063
+ (!events || r.withEvents))
1064
+ .sort((a, b) => Number(b.withOrder) - Number(a.withOrder) ||
1065
+ Number(!!b.withTx) - Number(!!a.withTx) ||
1066
+ Number(b.withMarket) - Number(a.withMarket))[0];
1067
+ const command = `zyrk export --${scope} ${key} --since ${new Date(o.sinceMs).toISOString()} --until ${new Date(o.untilMs).toISOString()} --with-order --with-tx${events ? " --with-events" : sinceCreation ? " --with-market" : ""}`;
1068
+ if (!meta)
1069
+ throw new Error(`No export covers this replay: ${command}`);
1070
+ if (!meta.withOrder)
1071
+ warnings.push(`The export has no on-chain order: events come in provider order, each with orderFallback: true. For the service's order: ${command}`);
1072
+ if (!meta.withTx)
1073
+ warnings.push(`The export has no transaction ids: every operation.tx is unknown. For the service's: ${command}`);
1074
+ if (sinceCreation && !meta.withMarket)
1075
+ warnings.push(`volume since creation needs the creations (market flags): without them no token has a complete total and nothing fires. Export with: ${command}`);
1076
+ warnings.push(`Replayed operations lack fields of the live JSON: ${MISSING_OPERATION_FIELDS.join(", ")}.`);
1077
+ return replayEvents(this.data, meta, s, o.sinceMs, o.untilMs, progress);
1078
+ }
1079
+ }
1080
+ export const REPLAY_MAX_DURATION_MS = 30 * 60_000;
1081
+ export const REPLAY_FEED_BYTES = 16 * 1024 * 1024;
1082
+ const checkKeep = (k) => {
1083
+ if (k.to !== "long" && k.to !== "durable")
1084
+ throw new Error("keepStorage.to: expected long or durable");
1085
+ if (k.from !== undefined && !["short", "long", "durable"].includes(k.from))
1086
+ throw new Error("keepStorage.from: expected short, long or durable");
1087
+ if (k.keys !== undefined && (k.keys.length < 1 || k.keys.length > 1000))
1088
+ throw new Error("keepStorage.keys: 1 to 1000 keys");
1089
+ return k;
1090
+ };
1091
+ const chainDone = async (e) => {
1092
+ for (let x = e; x; x = x.next)
1093
+ await x.done;
1094
+ };
1095
+ function view(e) {
1096
+ return {
1097
+ id: e.id,
1098
+ function: e.function,
1099
+ args: e.args,
1100
+ status: e.status,
1101
+ ...(e.result !== undefined ? { result: e.result } : {}),
1102
+ ...(e.error ? { error: e.error } : {}),
1103
+ logs: e.logs,
1104
+ emits: e.emits,
1105
+ usage: { ...e.usage },
1106
+ children: [...e.children],
1107
+ ...(e.next ? { next: e.next.id } : {}),
1108
+ };
1109
+ }
1110
+ function loadContext(code, object, natives) {
1111
+ const context = vm.createContext({});
1112
+ const realm = vm.runInContext("({ Uint8Array, Uint16Array, Uint32Array, Int32Array, BigUint64Array, Float64Array, Array, Object, Error, Promise, JSON })", context);
1113
+ let mode = "load";
1114
+ const N = natives(realm, () => mode);
1115
+ const factory = vm.runInContext(sandboxBootstrap(), context, {
1116
+ filename: "bootstrap.js",
1117
+ });
1118
+ const guest = factory(N);
1119
+ if (object)
1120
+ context.__app = { default: object };
1121
+ else
1122
+ vm.runInContext(code, context, {
1123
+ filename: "program.js",
1124
+ timeout: execLimits.cpuMs,
1125
+ });
1126
+ return {
1127
+ describe: () => guest.describe(),
1128
+ start: (fn, args, execution) => {
1129
+ mode = "run";
1130
+ return guest.start(fn, args, execution);
1131
+ },
1132
+ };
1133
+ }
1134
+ function fnv(text) {
1135
+ let h = 0xcbf29ce484222325n;
1136
+ for (let i = 0; i < text.length; i++) {
1137
+ h ^= BigInt(text.charCodeAt(i));
1138
+ h = (h * 0x100000001b3n) & 0xffffffffffffffffn;
1139
+ }
1140
+ return h.toString(16).padStart(16, "0");
1141
+ }
1142
+ export async function runLocal(source, fn, args = null, options = {}) {
1143
+ const runner = await LocalRunner.load(source, options);
1144
+ const run = await runner.run(fn, args);
1145
+ await runner.idle();
1146
+ return { ...run, warnings: [...runner.warnings] };
1147
+ }