@johannc-dev/zyrk 0.2.0 → 0.2.2

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 CHANGED
@@ -6,10 +6,35 @@ import { getHeapStatistics } from "node:v8";
6
6
  import vm from "node:vm";
7
7
  import { buildProgram } from "./build.js";
8
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";
9
+ import { LocalData, canonical, ReadFailed, spec as readSpec, } from "./local-data.js";
10
10
  import { LocalStorage, StorageOpError, } from "./local-storage.js";
11
11
  import { accountLimits, execLimits, subscriptionLimits } from "./limits.js";
12
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
+ const COMPUTE_OUTPUT_BYTES = 64 * 2 ** 20;
25
+ const computeLine = (r, row) => {
26
+ const parts = [...r.keys, ...r.measures].map((n) => {
27
+ const c = r.columns[n];
28
+ const v = c.values[row];
29
+ const value = c.type === "f64"
30
+ ? typeof v === "number" && Number.isFinite(v)
31
+ ? v
32
+ : null
33
+ : v;
34
+ return `${JSON.stringify(n)}:${JSON.stringify(value)}`;
35
+ });
36
+ return `{${parts.join(",")}}\n`;
37
+ };
13
38
  let bootstrapText;
14
39
  export function sandboxBootstrap() {
15
40
  return (bootstrapText ??= readFileSync(join(here, "sandbox-bootstrap.js"), "utf8"));
@@ -60,6 +85,13 @@ const WEBHOOK_PAYLOAD_BYTES = 16 * 1024;
60
85
  const WEBHOOK_OVERHEAD_BYTES = 256;
61
86
  const WEBHOOK_QUEUE_LIMIT = 1000;
62
87
  const MAX_MESSAGE_BYTES = 2048;
88
+ const openWhenExported = (v) => {
89
+ const c = v?.coverage;
90
+ return (typeof c === "object" &&
91
+ c !== null &&
92
+ (c.unsealed === true ||
93
+ (c.liveUntilMs !== null && c.liveUntilMs !== undefined)));
94
+ };
63
95
  const jsonBytes = (v) => Buffer.byteLength(JSON.stringify(v) ?? "null");
64
96
  const truncate = (m) => Buffer.byteLength(m) <= MAX_MESSAGE_BYTES
65
97
  ? m
@@ -212,6 +244,7 @@ export class LocalRunner {
212
244
  pending: 0,
213
245
  io: { bytes: 0 },
214
246
  startedAt: 0,
247
+ readWaitMs: 0,
215
248
  done,
216
249
  finish,
217
250
  };
@@ -352,7 +385,7 @@ export class LocalRunner {
352
385
  if (e.usage.outputBytes + size > this.limits.exec.outputBytes * e.scale)
353
386
  throw limit("output_limit", `Result, logs and emits went over ${this.limits.exec.outputBytes * e.scale} bytes`);
354
387
  e.usage.outputBytes += size;
355
- if (performance.now() - e.startedAt > this.limits.exec.wallMs)
388
+ if (this.#wallUsed(e) > this.limits.exec.wallMs)
356
389
  throw limit("wall_limit", `The execution ran over ${this.limits.exec.wallMs} ms (measured locally, not preempted)`);
357
390
  if (e.cancelled)
358
391
  throw new HostError("permission", "cancelled", "Its parent failed: the job was cancelled");
@@ -428,6 +461,38 @@ export class LocalRunner {
428
461
  };
429
462
  const fromRead = (f) => new HostError(kindOfSdkCode(f.code), f.code, f.message, f.retryAfterMs);
430
463
  const guestJson = (v) => realm.JSON.parse(JSON.stringify(v) ?? "null");
464
+ const computeOutputs = new Map();
465
+ let computeNext = 0;
466
+ const computeSummary = (r) => ({
467
+ length: r.length,
468
+ keys: r.keys,
469
+ measures: r.measures,
470
+ coverage: r.coverage,
471
+ complete: r.complete,
472
+ skipped: r.skipped,
473
+ stats: r.stats,
474
+ });
475
+ const computeObject = (r) => {
476
+ const o = guestJson(computeSummary(r));
477
+ const columns = new realm.Object();
478
+ for (const n of [...r.keys, ...r.measures]) {
479
+ const c = r.columns[n];
480
+ columns[n] =
481
+ c.type === "f64"
482
+ ? realm.Float64Array.from(c.values.map((v) => v === "NaN"
483
+ ? NaN
484
+ : v === "Infinity"
485
+ ? Infinity
486
+ : v === "-Infinity"
487
+ ? -Infinity
488
+ : Number(v)))
489
+ : c.type === "int"
490
+ ? realm.Array.from(c.values.map((v) => (v === null ? null : BigInt(String(v)))))
491
+ : realm.Array.from(c.values);
492
+ }
493
+ o.columns = columns;
494
+ return o;
495
+ };
431
496
  const ms = (v) => typeof v === "number" && Number.isFinite(v) && Math.abs(v) < 9e15
432
497
  ? Math.trunc(v)
433
498
  : undefined;
@@ -454,13 +519,43 @@ export class LocalRunner {
454
519
  wf.readBytes > this.limits.account.workflowReadBytes)
455
520
  throw readLimit();
456
521
  };
522
+ const waited = (t0) => {
523
+ e.readWaitMs += Math.min(performance.now() - t0, this.limits.exec.readCallWaitMs);
524
+ };
457
525
  const data = () => {
458
526
  if (!this.data)
459
527
  throw sdkError(program("local_data_missing", "No data directory: export history with zyrk export, then pass --data (or { data })."));
460
528
  return this.data;
461
529
  };
530
+ const saved = (answer, failure, missing) => {
531
+ let value;
532
+ let failed;
533
+ const t0 = performance.now();
534
+ try {
535
+ value = answer();
536
+ failed = value === undefined ? failure() : undefined;
537
+ }
538
+ catch (error) {
539
+ if (error instanceof ReadFailed)
540
+ throw sdkError(fromRead(error.failure));
541
+ throw error;
542
+ }
543
+ finally {
544
+ waited(t0);
545
+ }
546
+ if (failed)
547
+ throw sdkError(fromRead(failed));
548
+ if (value === undefined)
549
+ throw sdkError(program("local_data_missing", missing));
550
+ if (openWhenExported(value))
551
+ 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.");
552
+ charge(jsonBytes(value));
553
+ return guestJson(value);
554
+ };
462
555
  return {
463
556
  maxDepth: this.limits.account.maxDepth,
557
+ batchReads: batchReadsFromEnv(),
558
+ hint: () => undefined,
464
559
  fail: (code, message) => sdkError(new HostError(kindOfSdkCode(String(code)), String(code), String(message))),
465
560
  op: (text, awaited) => this.#op(e, text, !!awaited, realm, mode, sdkError, guestJson, sample),
466
561
  read: (kind, key, since, until, side, allowPartial, withSignatures, after, withMarket, withEvents, withOrder, withTx) => {
@@ -500,12 +595,13 @@ export class LocalRunner {
500
595
  }
501
596
  finally {
502
597
  e.usage.readMs += performance.now() - t0;
598
+ waited(t0);
503
599
  sample();
504
600
  }
505
601
  if (hour === null)
506
602
  return null;
507
603
  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.");
604
+ 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.");
509
605
  if (hour.recut)
510
606
  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
607
  charge(hour.bytes);
@@ -517,47 +613,77 @@ export class LocalRunner {
517
613
  if (typeof mint !== "string" ||
518
614
  (quote !== null && typeof quote !== "string"))
519
615
  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);
616
+ return saved(() => data().totals(mint, quote), () => data().totalsFailure(mint, quote), `No exported totals for ${mint}: zyrk export --totals ${mint}${quote ? ` --quote ${quote}` : ""}`);
525
617
  },
526
618
  info: (mint) => {
527
619
  readAllowed();
528
620
  e.usage.readCalls++;
529
621
  if (typeof mint !== "string")
530
622
  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);
623
+ 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}`);
624
+ },
625
+ computeAsk: () => realm.Promise.resolve(),
626
+ compute: (text, toStorage) => {
627
+ readAllowed();
628
+ e.usage.readCalls++;
629
+ let plan;
630
+ try {
631
+ plan = JSON.parse(String(text));
632
+ }
633
+ catch {
634
+ throw sdkError(program("invalid_plan", "The plan is not JSON"));
635
+ }
636
+ const answer = saved(() => data().computeResult(plan), () => data().computeFailure(plan), `No exported result for this plan: zyrk export --compute '${canonical(plan)}'`);
637
+ if (toStorage === true) {
638
+ if (computeOutputs.size > 0)
639
+ throw sdkError(program("invalid_argument", "One ctx.compute output at a time per execution: await the one being written first"));
640
+ if (Array.isArray(answer))
641
+ throw sdkError(program("invalid_plan", "output writes the result of one plan"));
642
+ const result = answer;
643
+ let bytes = 0;
644
+ for (let row = 0; row < result.length; row++)
645
+ bytes += Buffer.byteLength(computeLine(result, row), "utf8");
646
+ if (bytes > COMPUTE_OUTPUT_BYTES)
647
+ throw sdkError(limit("compute_result_limit", `the result is ${Math.ceil(bytes / 2 ** 20)} MiB of JSON lines (${result.length} rows), more than the ${COMPUTE_OUTPUT_BYTES / 2 ** 20} MiB of one storage object: add a limit, a top or a having, group less finely, or split the range into several calls`));
648
+ const handle = computeNext++;
649
+ computeOutputs.set(handle, {
650
+ result: answer,
651
+ row: 0,
652
+ });
653
+ return guestJson({
654
+ handle,
655
+ summary: computeSummary(answer),
656
+ });
657
+ }
658
+ return Array.isArray(answer)
659
+ ? realm.Array.from(answer.map((a) => computeObject(a)))
660
+ : computeObject(answer);
661
+ },
662
+ computeChunk: (handle) => {
663
+ const o = computeOutputs.get(Number(handle));
664
+ if (!o)
665
+ throw sdkError(program("invalid_argument", "Unknown compute output"));
666
+ const r = o.result;
667
+ if (o.row >= r.length)
668
+ return null;
669
+ let text = "";
670
+ while (o.row < r.length && text.length < 4 * 1024 * 1024 - 64 * 1024) {
671
+ text += computeLine(r, o.row);
672
+ o.row++;
673
+ }
674
+ const bytes = Buffer.from(text, "utf8");
675
+ const out = new realm.Uint8Array(bytes.length);
676
+ out.set(bytes);
677
+ return out;
678
+ },
679
+ computeDrop: (handle) => {
680
+ computeOutputs.delete(Number(handle));
551
681
  },
552
682
  volume: (text) => {
553
683
  readAllowed();
554
684
  e.usage.readCalls++;
555
685
  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);
686
+ 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")))}'`);
561
687
  },
562
688
  encode: (text) => {
563
689
  const bytes = Buffer.from(String(text), "utf8");
@@ -588,8 +714,13 @@ export class LocalRunner {
588
714
  },
589
715
  };
590
716
  }
717
+ #wallUsed(e) {
718
+ return (performance.now() -
719
+ e.startedAt -
720
+ Math.min(e.readWaitMs, this.limits.exec.readWaitMs));
721
+ }
591
722
  #checkWall(e) {
592
- if (performance.now() - e.startedAt > this.limits.exec.wallMs) {
723
+ if (this.#wallUsed(e) > this.limits.exec.wallMs) {
593
724
  e.fatal ??= limit("wall_limit", `The execution ran over ${this.limits.exec.wallMs} ms`);
594
725
  throw e.fatal;
595
726
  }
@@ -781,7 +912,7 @@ export class LocalRunner {
781
912
  });
782
913
  if (this.#waiting >= account.parallelExecutions - 1)
783
914
  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);
915
+ const left = this.limits.exec.wallMs - this.#wallUsed(e);
785
916
  const timeout = Math.max(0, Math.min(typeof a.timeout_ms === "number" ? a.timeout_ms : left, left));
786
917
  this.#waiting++;
787
918
  let timer;
@@ -299,25 +299,25 @@
299
299
  const summary = (r) => freeze({ key: r.key, size: r.size, contentType: r.contentType, lastModified: r.lastModified, expiresAt: r.expiresAt });
300
300
  const storageArea = (area) => {
301
301
  const read = async (key, offset, length) => {
302
- const first = await call({ StorageGet: { area, key, offset, length: length > CHUNK ? CHUNK : length } });
303
- if (first === null) return null;
304
- const end = Math.min(first.size, offset + length);
305
- const total = Math.max(0, end - first.offset);
302
+ const first2 = await call({ StorageGet: { area, key, offset, length: length > CHUNK ? CHUNK : length } });
303
+ if (first2 === null) return null;
304
+ const end = Math.min(first2.size, offset + length);
305
+ const total = Math.max(0, end - first2.offset);
306
306
  const out = new U8(total);
307
307
  let at = 0;
308
- let r = first;
308
+ let r = first2;
309
309
  for (; ; ) {
310
310
  const n = N.b64decodeInto(r.data, out, at);
311
311
  if (n < 0) throw fail("unavailable", "Corrupt storage chunk");
312
312
  at += n;
313
313
  if (at >= total || n === 0) break;
314
314
  r = await call({
315
- StorageGet: { area, key, offset: first.offset + at, length: Math.min(CHUNK, total - at) }
315
+ StorageGet: { area, key, offset: first2.offset + at, length: Math.min(CHUNK, total - at) }
316
316
  });
317
- if (r === null || r.etag !== first.etag)
317
+ if (r === null || r.etag !== first2.etag)
318
318
  throw fail("value_changed", "The object changed while it was read: read it again");
319
319
  }
320
- return { first, body: at === total ? out : out.subarray(0, at) };
320
+ return { first: first2, body: at === total ? out : out.subarray(0, at) };
321
321
  };
322
322
  const get = async (key, range) => {
323
323
  keyOf(key);
@@ -427,6 +427,65 @@
427
427
  long: storageArea("long"),
428
428
  durable: storageArea("durable")
429
429
  });
430
+ const computeAreas = ["short", "long", "durable"];
431
+ let outputTurn = PromiseCtor.resolve();
432
+ const compute = async (plan, options) => {
433
+ if (plan === null || typeof plan !== "object")
434
+ throw fail("invalid_plan", "Expected a plan object (or an array of plans)");
435
+ let output;
436
+ if (options !== void 0 && options !== null) {
437
+ if (typeOf(options) !== "object") throw fail("invalid_argument", "Expected options { output }");
438
+ for (const k of keys(options))
439
+ if (k !== "output") throw fail("invalid_argument", "Unknown compute option: " + k);
440
+ output = options.output;
441
+ if (output !== void 0 && (typeOf(output) !== "object" || !computeAreas.includes(output.area) || typeof output.key !== "string"))
442
+ throw fail("invalid_argument", 'output is { area: "short" | "long" | "durable", key }');
443
+ }
444
+ let text;
445
+ try {
446
+ text = stringify(plan);
447
+ } catch {
448
+ throw fail("invalid_plan", "The plan is not JSON (write integers beyond 2^53 as decimal strings)");
449
+ }
450
+ await N.computeAsk(text);
451
+ if (output === void 0) return N.compute(text, false);
452
+ const before = outputTurn;
453
+ let release;
454
+ outputTurn = new PromiseCtor((resolve) => {
455
+ release = resolve;
456
+ });
457
+ try {
458
+ await before;
459
+ return await computeOutput(text, output);
460
+ } finally {
461
+ release();
462
+ }
463
+ };
464
+ const computeOutput = async (text, output) => {
465
+ const r = N.compute(text, true);
466
+ let w;
467
+ try {
468
+ w = await storage[output.area].writer(output.key, { contentType: "application/x-ndjson" });
469
+ let bytes = 0;
470
+ for (; ; ) {
471
+ const chunk = N.computeChunk(r.handle);
472
+ if (chunk === null) break;
473
+ bytes += chunk.length;
474
+ await w.write(chunk);
475
+ }
476
+ const head2 = await w.close();
477
+ return freeze({
478
+ ...r.summary,
479
+ output: freeze({ area: output.area, key: output.key, rows: r.summary.length, bytes, etag: head2.etag })
480
+ });
481
+ } catch (e) {
482
+ if (w) await w.abort().catch(() => {
483
+ });
484
+ throw e;
485
+ } finally {
486
+ N.computeDrop(r.handle);
487
+ }
488
+ };
430
489
  const start = (functionName, argsText, executionText) => {
431
490
  const p = program();
432
491
  let depth = 0;
@@ -449,7 +508,7 @@
449
508
  version: execution.version,
450
509
  attempt: execution.attempt
451
510
  }),
452
- data: freeze({
511
+ data: batching ? batchedData() : freeze({
453
512
  walletTrades: trades("wallet"),
454
513
  tokenTrades: trades("token"),
455
514
  tokenTotals: async (mint, o) => {
@@ -487,6 +546,7 @@
487
546
  continue: (args) => call({ Continue: { args } })
488
547
  }),
489
548
  storage,
549
+ compute,
490
550
  subscriptions: freeze({
491
551
  set: (name, subscription) => call({ Subscribe: { name, subscription } }),
492
552
  remove: (name) => call({ Unsubscribe: { name } }),
@@ -515,5 +575,167 @@
515
575
  return text === void 0 ? "null" : text;
516
576
  })();
517
577
  };
578
+ const batching = N.batchReads === true;
579
+ const BATCH = 64;
580
+ const MAX_REENTRY = 1e3;
581
+ const reflectApply = Reflect.apply;
582
+ const promiseThen = PromiseCtor.prototype.then;
583
+ const ready = PromiseCtor.resolve();
584
+ Object.defineProperty(ready, "constructor", { value: void 0 });
585
+ let open = false;
586
+ let direct = false;
587
+ let reentry = 0;
588
+ let first = null;
589
+ let last = null;
590
+ let epoch = 0;
591
+ const wait = (run, hint) => new PromiseCtor((resolve, reject) => {
592
+ const q = { run, hint, next: null, resolve, reject };
593
+ if (last === null) first = q;
594
+ else last.next = q;
595
+ last = q;
596
+ });
597
+ const checkpoint = () => {
598
+ let q = first;
599
+ first = null;
600
+ last = null;
601
+ direct = reentry >= MAX_REENTRY;
602
+ while (q !== null) {
603
+ epoch++;
604
+ let n = 0;
605
+ let hinted = 0;
606
+ for (let r = q; r !== null && n < BATCH; r = r.next) {
607
+ n++;
608
+ if (r.hint !== null && r.hint(epoch)) hinted++;
609
+ }
610
+ if (hinted > 0) N.hint();
611
+ for (; n > 0; n--) {
612
+ const c = q;
613
+ q = c.next;
614
+ let value;
615
+ try {
616
+ value = c.run();
617
+ } catch (e) {
618
+ c.reject(e);
619
+ continue;
620
+ }
621
+ c.resolve(value);
622
+ }
623
+ }
624
+ direct = false;
625
+ if (first === null) {
626
+ reentry = 0;
627
+ open = false;
628
+ } else {
629
+ reentry++;
630
+ reflectApply(promiseThen, ready, [checkpoint]);
631
+ }
632
+ };
633
+ const openBatch = () => {
634
+ open = true;
635
+ reflectApply(promiseThen, ready, [checkpoint]);
636
+ };
637
+ const waiting = () => open && !direct;
638
+ const tradesBatched = (kind) => (key, w) => {
639
+ if (typeof key !== "string") throw fail("invalid_argument", "Expected an address string");
640
+ const { since, until, side, allowPartial, withSignatures, withMarket, withEvents, withOrder, withTx } = checkWindow(w);
641
+ return freeze({
642
+ [asyncIterator]() {
643
+ let after = null;
644
+ let done = false;
645
+ let closed = false;
646
+ let mark = 0;
647
+ const end = () => ({ done: true, value: void 0 });
648
+ const read = () => {
649
+ if (done) return end();
650
+ const batch = N.read(kind, key, since, until, side, allowPartial, withSignatures, after, withMarket, withEvents, withOrder, withTx);
651
+ if (batch === null) {
652
+ done = true;
653
+ return end();
654
+ }
655
+ after = batch.hour;
656
+ return { done: false, value: batch };
657
+ };
658
+ const hint = (e) => {
659
+ if (done || mark === e) return false;
660
+ mark = e;
661
+ N.hint(kind, key, since, until, side, allowPartial, withSignatures, after, withMarket, withEvents, withOrder, withTx);
662
+ return true;
663
+ };
664
+ const nextNow = async () => {
665
+ if (done || closed) return end();
666
+ if (!direct) openBatch();
667
+ return read();
668
+ };
669
+ const returnNow = async (value) => {
670
+ closed = true;
671
+ return { done: true, value };
672
+ };
673
+ return {
674
+ next() {
675
+ if (!waiting()) return nextNow();
676
+ if (closed) return wait(end, null);
677
+ return wait(read, hint);
678
+ },
679
+ return(value) {
680
+ if (!waiting()) return returnNow(value);
681
+ closed = true;
682
+ return wait(() => ({ done: true, value }), null);
683
+ },
684
+ [asyncIterator]() {
685
+ return this;
686
+ }
687
+ };
688
+ }
689
+ });
690
+ };
691
+ const totalsNow = async (mint, o) => {
692
+ if (typeof mint !== "string") throw fail("invalid_argument", "Expected a mint address");
693
+ const rawQuote = o ? o.quote : void 0;
694
+ const quote = rawQuote !== void 0 ? StringCtor(rawQuote) : null;
695
+ return N.totals(mint, quote);
696
+ };
697
+ const infoNow = async (mint) => {
698
+ if (typeof mint !== "string") throw fail("invalid_argument", "Expected a mint address");
699
+ return N.info(mint);
700
+ };
701
+ const batchedData = () => freeze({
702
+ walletTrades: tradesBatched("wallet"),
703
+ tokenTrades: tradesBatched("token"),
704
+ tokenTotals: (mint, o) => {
705
+ if (!waiting()) return totalsNow(mint, o);
706
+ let quote;
707
+ try {
708
+ if (typeof mint !== "string") throw fail("invalid_argument", "Expected a mint address");
709
+ const rawQuote = o ? o.quote : void 0;
710
+ quote = rawQuote !== void 0 ? StringCtor(rawQuote) : null;
711
+ } catch (e) {
712
+ return PromiseCtor.reject(e);
713
+ }
714
+ return wait(() => N.totals(mint, quote), null);
715
+ },
716
+ tokenInfo: (mint) => {
717
+ if (!waiting()) return infoNow(mint);
718
+ if (typeof mint !== "string") return PromiseCtor.reject(fail("invalid_argument", "Expected a mint address"));
719
+ return wait(() => N.info(mint), null);
720
+ },
721
+ volumeSearch: async function* (params) {
722
+ let p = { ...params };
723
+ for (; ; ) {
724
+ let page;
725
+ if (waiting()) {
726
+ const text = flatParams(p);
727
+ page = await wait(() => N.volume(text), null);
728
+ } else page = N.volume(flatParams(p));
729
+ yield page;
730
+ const next = page === null || typeof page !== "object" ? null : page.nextCursor;
731
+ if (next === null || next === void 0) return;
732
+ if (typeof next !== "string" || next === p.cursor)
733
+ throw fail("history_unavailable", "Invalid next cursor");
734
+ const w = page.window;
735
+ if (w && typeof w === "object" && "since" in w) p = { ...p, since: w.since, until: w.until };
736
+ p = { ...p, cursor: next };
737
+ }
738
+ }
739
+ });
518
740
  return freeze({ start, describe });
519
741
  });