@johannc-dev/zyrk 0.0.0-stage → 0.2.0

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