@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/export.d.ts CHANGED
@@ -5,16 +5,19 @@ export interface ExportConnection {
5
5
  readonly url: string;
6
6
  /** API key (`TH_API_KEY`) of an account with hosted functions. */
7
7
  readonly apiKey: string;
8
- /** At most this many requests per minute (default: as fast as the server allows). */
8
+ /** At most this many requests per minute (default: as fast as the server allows, 30 a second per account). Optional pacing only. */
9
9
  readonly rpm?: number;
10
- /** Network or 5xx failures tolerated in a row before giving up (default 8). */
10
+ /** Network failures, 502, 503 and 504 tolerated in a row before giving up (default 8); another 5xx is retried once. */
11
11
  readonly maxRetries?: number;
12
- /** Longest wait accepted on a 429 before stopping (default 5 minutes): a daily quota stops the export. */
12
+ /** Longest wait accepted on 429s in a row before stopping (default 5 minutes): a longer one (an older server's daily quota) stops the export. */
13
13
  readonly maxWaitMs?: number;
14
14
  readonly fetch?: typeof fetch;
15
15
  readonly signal?: AbortSignal;
16
16
  readonly onProgress?: (progress: ExportProgress) => void;
17
- /** Hours that may still change (open, not sealed), files downloaded again. */
17
+ /**
18
+ * Hours that may still change (open, not sealed), files downloaded again,
19
+ * errors saved as the answer of a call.
20
+ */
18
21
  readonly onWarning?: (message: string) => void;
19
22
  }
20
23
  export interface ExportProgress {
@@ -52,7 +55,7 @@ export interface ExportSummary {
52
55
  }
53
56
  /** `[since, until)` in parts of at most 30 days, ending on UTC day boundaries. */
54
57
  export declare function parts(sinceMs: number, untilMs: number): [number, number][];
55
- /** Meta of an export answer (`history_columns::export::hour_response`). */
58
+ /** Meta of an export answer. */
56
59
  interface HourMeta {
57
60
  readonly format?: string;
58
61
  readonly coverage?: ReadMeta["coverage"];
@@ -76,12 +79,28 @@ export declare function parseFrame(bytes: Uint8Array): {
76
79
  export declare function staleFrom(meta: ReadMeta, dir: string): number | null;
77
80
  /** Exports one read (cut into parts of at most 30 days). Resumes an interrupted export. */
78
81
  export declare function exportHistory(connection: ExportConnection, out: string, read: ExportRead): Promise<ExportSummary>;
79
- /** Saves `/v1/tokens/{mint}/totals` for `ctx.data.tokenTotals`. */
82
+ /**
83
+ * Saves the totals of a mint for `ctx.data.tokenTotals`, and its info for
84
+ * `ctx.data.tokenInfo`, as hosted programs receive them. Returns the totals,
85
+ * or the error answer saved for them (`{ error: { kind, code, message } }`).
86
+ */
80
87
  export declare function exportTotals(connection: ExportConnection, out: string, mint: string, quote?: string | null): Promise<Json>;
81
88
  /**
82
- * Saves every page of `/v1/volume/search` for `params`, following the
89
+ * Saves the answer of `ctx.compute(plan)` (a plan or an array of plans), as
90
+ * the service computes it for a function (`POST /v1/functions/compute`, the
91
+ * same engine), so that the same call in a local run finds it. Relative
92
+ * windows (`last`, an `until` left out) are resolved now, at export. An
93
+ * error answer (an invalid plan, a limit) is saved as that error.
94
+ */
95
+ export declare function exportCompute(connection: ExportConnection, out: string, plan: Json): Promise<{
96
+ rows: number | null;
97
+ }>;
98
+ /**
99
+ * Saves every page of a volume search for `params`, following the
83
100
  * cursor exactly as `ctx.data.volumeSearch` does, so that the same call in
84
- * a local run finds each page.
101
+ * a local run finds each page. A page answered by an error (an invalid
102
+ * parameter...) is saved as that error, and ends the search, as it does in a
103
+ * hosted call.
85
104
  */
86
105
  export declare function exportVolumeSearch(connection: ExportConnection, out: string, params: VolumeSearchParams): Promise<number>;
87
106
  export {};
package/dist/export.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
2
- import { join } from "node:path";
2
+ import { dirname, join } from "node:path";
3
3
  import { decompressHour } from "./hourfile.js";
4
- import { HOUR_MS, READ_FORMAT, sha256, readId, spec as readSpec, totalsName, volumeName, } from "./local-data.js";
4
+ import { HOUR_MS, READ_FORMAT, infoName, sha256, readId, spec as readSpec, totalsName, volumeName, computeName, } from "./local-data.js";
5
5
  import { MAX_WINDOW_MS } from "./limits.js";
6
6
  const DAY_MS = 86_400_000;
7
7
  const PART_MS = 30 * DAY_MS;
@@ -40,6 +40,8 @@ const writeAtomic = (file, data) => {
40
40
  writeFileSync(tmp, data);
41
41
  renameSync(tmp, file);
42
42
  };
43
+ const MAX_BUSY_ATTEMPTS = 12;
44
+ const MAX_SERVER_ERRORS = 1;
43
45
  class Http {
44
46
  c;
45
47
  requests = 0;
@@ -47,12 +49,15 @@ class Http {
47
49
  constructor(c) {
48
50
  this.c = c;
49
51
  }
50
- async raw(path, query) {
52
+ async raw(path, query, keep, post) {
51
53
  const f = this.c.fetch ?? fetch;
52
54
  const url = new URL(this.c.url.replace(/\/+$/, "") + path);
53
55
  for (const [k, v] of Object.entries(query))
54
56
  url.searchParams.set(k, v);
55
57
  let failures = 0;
58
+ let serverErrors = 0;
59
+ let busy = 0;
60
+ let waited = 0;
56
61
  for (;;) {
57
62
  if (this.c.rpm) {
58
63
  const gap = 60_000 / this.c.rpm - (Date.now() - this.#last);
@@ -65,7 +70,13 @@ class Http {
65
70
  let bytes;
66
71
  try {
67
72
  response = await f(url, {
68
- headers: { authorization: `Bearer ${this.c.apiKey}` },
73
+ headers: {
74
+ authorization: `Bearer ${this.c.apiKey}`,
75
+ ...(post !== undefined
76
+ ? { "content-type": "application/json" }
77
+ : {}),
78
+ },
79
+ ...(post !== undefined ? { method: "POST", body: post } : {}),
69
80
  ...(this.c.signal ? { signal: this.c.signal } : {}),
70
81
  });
71
82
  bytes = new Uint8Array(await response.arrayBuffer());
@@ -88,6 +99,8 @@ class Http {
88
99
  catch {
89
100
  body = null;
90
101
  }
102
+ if (keep?.(response.status, body))
103
+ return { status: response.status, contentType, bytes };
91
104
  const err = body?.error;
92
105
  const code = typeof err === "string"
93
106
  ? err
@@ -105,12 +118,26 @@ class Http {
105
118
  ? header * 1000
106
119
  : undefined;
107
120
  if (response.status === 429) {
108
- const wait = retryMs ?? 60_000;
109
- if (wait > (this.c.maxWaitMs ?? 300_000))
110
- throw new ExportError(`${code}: ${message} Retry after ${Math.round(wait / 1000)} s: run the same command again then, the export resumes where it stopped.`, code, 429);
121
+ const maxWait = this.c.maxWaitMs ?? 300_000;
122
+ const asked = retryMs ?? 60_000;
123
+ if (asked > maxWait)
124
+ throw new ExportError(`${code}: ${message} Retry after ${Math.round(asked / 1000)} s: run the same command again then, the export resumes where it stopped.`, code, 429);
125
+ const wait = Math.max(asked, Math.min(30_000, 1000 * 2 ** busy));
126
+ if (++busy >= MAX_BUSY_ATTEMPTS || waited + wait > maxWait)
127
+ throw new ExportError(`${code}: ${message} The server answered 429 ${busy} times in a row (${Math.round(waited / 1000)} s): run the same command again later, the export resumes where it stopped.`, code, 429);
128
+ waited += wait;
111
129
  await sleep(wait, this.c.signal);
112
130
  continue;
113
131
  }
132
+ if (response.status >= 500 &&
133
+ response.status !== 502 &&
134
+ response.status !== 503 &&
135
+ response.status !== 504) {
136
+ if (++serverErrors > MAX_SERVER_ERRORS)
137
+ throw new ExportError(`${response.status} ${code}: ${message}`, code, response.status);
138
+ await sleep(Math.min(30_000, 500 * 2 ** serverErrors), this.c.signal);
139
+ continue;
140
+ }
114
141
  if (response.status >= 500 && code !== "history_timeout") {
115
142
  if (++failures > (this.c.maxRetries ?? 8))
116
143
  throw new ExportError(`${response.status} ${code}: ${message}`, code, response.status);
@@ -120,11 +147,14 @@ class Http {
120
147
  throw new ExportError(`${response.status} ${code}: ${message}`, code, response.status);
121
148
  }
122
149
  }
123
- async get(path, query) {
124
- const answer = await this.raw(path, query);
150
+ async get(path, query, keep, post) {
151
+ const answer = await this.raw(path, query, keep, post);
125
152
  const text = new TextDecoder().decode(answer.bytes);
126
153
  try {
127
- return text ? JSON.parse(text) : null;
154
+ return {
155
+ status: answer.status,
156
+ body: text ? JSON.parse(text) : null,
157
+ };
128
158
  }
129
159
  catch {
130
160
  throw new ExportError(`${path}: the answer is not JSON`, "decode");
@@ -171,9 +201,9 @@ export function staleFrom(meta, dir) {
171
201
  break;
172
202
  let live = false;
173
203
  try {
204
+ const { header } = decompressHour(readFileSync(join(dir, `${hour}.zhb`)));
174
205
  live =
175
- decompressHour(readFileSync(join(dir, `${hour}.zhb`))).header.source ===
176
- "live";
206
+ header.source === "live" || (header.market && !header.virtualSides);
177
207
  }
178
208
  catch {
179
209
  live = true;
@@ -207,7 +237,7 @@ async function exportPart(http, out, s, sinceMs, untilMs, progress, refresh, onP
207
237
  if (meta.done) {
208
238
  const from = staleFrom(meta, dir);
209
239
  if (from !== null) {
210
- onWarning?.(`${id}: hours from ${new Date(from * HOUR_MS).toISOString()} were open or not sealed when exported; downloading them again`);
240
+ onWarning?.(`${id}: hours from ${new Date(from * HOUR_MS).toISOString()} were open or not sealed when exported, or exported by an older version; downloading them again`);
211
241
  const hours = meta.hours.filter((h) => h < from);
212
242
  meta = {
213
243
  ...meta,
@@ -307,12 +337,107 @@ export async function exportHistory(connection, out, read) {
307
337
  }
308
338
  return { reads, requests: http.requests, bytes: progress.bytes };
309
339
  }
340
+ const RECORDED = new Set([
341
+ "invalid_window",
342
+ "invalid_parameter",
343
+ "invalid_limit",
344
+ "invalid_amount",
345
+ "invalid_cursor",
346
+ "usd_volume_unavailable",
347
+ "invalid_quote",
348
+ "partial_coverage",
349
+ "query_work_limit",
350
+ ]);
351
+ const recordable = (status, body) => {
352
+ const e = body?.error;
353
+ return (status === 400 &&
354
+ typeof e === "object" &&
355
+ e !== null &&
356
+ typeof e.kind === "string" &&
357
+ typeof e.code === "string" &&
358
+ typeof e.message === "string" &&
359
+ RECORDED.has(e.code));
360
+ };
361
+ async function getNew(http, path, query, what) {
362
+ try {
363
+ const { status, body } = await http.get(path, query, recordable);
364
+ if (status !== 400)
365
+ return { ok: body };
366
+ const e = body.error;
367
+ const retry = body.retryAfterMs;
368
+ return {
369
+ code: e.code,
370
+ error: {
371
+ error: { kind: e.kind, code: e.code, message: e.message },
372
+ ...(typeof retry === "number" ? { retryAfterMs: retry } : {}),
373
+ },
374
+ };
375
+ }
376
+ catch (error) {
377
+ if (error instanceof ExportError && error.status === 404)
378
+ throw new ExportError(`This server does not export ${what} yet (it answered 404): it must be upgraded first. Trades (--token, --wallet) export as before.`, "unsupported_server", 404);
379
+ throw error;
380
+ }
381
+ }
382
+ function save(connection, out, [dir, name], answer, content, call) {
383
+ const ok = join(out, dir, name);
384
+ const failed = join(out, "errors", dir, name);
385
+ const [file, other] = "ok" in answer ? [ok, failed] : [failed, ok];
386
+ mkdirSync(dirname(file), { recursive: true });
387
+ writeAtomic(file, JSON.stringify(content));
388
+ rmSync(other, { force: true });
389
+ if (!("ok" in answer))
390
+ connection.onWarning?.(`${call} answered ${answer.code}: saved as its answer; the same call in a local run throws this error, as a hosted call does now`);
391
+ }
310
392
  export async function exportTotals(connection, out, mint, quote = null) {
311
393
  const http = new Http(connection);
312
- const body = await http.get(`/v1/tokens/${encodeURIComponent(mint)}/totals`, quote ? { quote } : {});
313
- mkdirSync(join(out, "totals"), { recursive: true });
314
- writeAtomic(join(out, "totals", totalsName(mint, quote)), JSON.stringify(body));
315
- return body;
394
+ const key = encodeURIComponent(mint);
395
+ const body = await getNew(http, `/v1/functions/totals/${key}`, quote ? { quote } : {}, "totals (--totals)");
396
+ const info = await getNew(http, `/v1/functions/info/${key}`, {}, "totals (--totals)");
397
+ const content = (a) => ("ok" in a ? a.ok : a.error);
398
+ const totals = content(body);
399
+ save(connection, out, ["totals", totalsName(mint, quote)], body, totals, "tokenTotals");
400
+ save(connection, out, ["info", infoName(mint)], info, content(info), "tokenInfo");
401
+ return totals;
402
+ }
403
+ export async function exportCompute(connection, out, plan) {
404
+ const http = new Http(connection);
405
+ mkdirSync(join(out, "compute"), { recursive: true });
406
+ let answer;
407
+ try {
408
+ const { status, body } = await http.get("/v1/functions/compute", {}, recordable, JSON.stringify({ plan }));
409
+ if (status === 200)
410
+ answer = { ok: body };
411
+ else {
412
+ const e = body.error;
413
+ const retry = body.retryAfterMs;
414
+ answer = {
415
+ code: e.code,
416
+ error: {
417
+ error: { kind: e.kind, code: e.code, message: e.message },
418
+ ...(typeof retry === "number" ? { retryAfterMs: retry } : {}),
419
+ },
420
+ };
421
+ }
422
+ }
423
+ catch (error) {
424
+ if (error instanceof ExportError && error.status === 404)
425
+ throw new ExportError("This server does not run ctx.compute for local runs yet (it answered 404): it must be upgraded first.", "unsupported_server", 404);
426
+ throw error;
427
+ }
428
+ save(connection, out, ["compute", `${computeName(plan)}.json`], answer, "ok" in answer
429
+ ? { plan, result: answer.ok }
430
+ : { plan, ...answer.error }, "compute");
431
+ const ok = "ok" in answer ? answer.ok : null;
432
+ return {
433
+ rows: ok === null
434
+ ? null
435
+ : Array.isArray(ok)
436
+ ? ok.length
437
+ : typeof ok.length === "number"
438
+ ? ok.length
439
+ : null,
440
+ };
316
441
  }
317
442
  export async function exportVolumeSearch(connection, out, params) {
318
443
  const http = new Http(connection);
@@ -322,8 +447,13 @@ export async function exportVolumeSearch(connection, out, params) {
322
447
  const flat = Object.entries(p)
323
448
  .filter(([, v]) => v !== undefined && v !== null)
324
449
  .map(([k, v]) => [k, String(v)]);
325
- const page = await http.get("/v1/volume/search", Object.fromEntries(flat));
326
- writeAtomic(join(out, "volume", `${volumeName(flat)}.json`), JSON.stringify({ params: flat, page }));
450
+ const answer = await getNew(http, "/v1/functions/volume/search", Object.fromEntries(flat), "volume searches (--volume-search)");
451
+ save(connection, out, ["volume", `${volumeName(flat)}.json`], answer, "ok" in answer
452
+ ? { params: flat, page: answer.ok }
453
+ : { params: flat, ...answer.error }, `volumeSearch (page ${pages})`);
454
+ if (!("ok" in answer))
455
+ return pages;
456
+ const page = answer.ok;
327
457
  const next = page?.nextCursor;
328
458
  if (next === null || next === undefined)
329
459
  return pages;
@@ -1,4 +1,4 @@
1
- import type { HourBatch } from "./sdk.js";
1
+ import { type HourBatch } from "./sdk.js";
2
2
  export declare const HOUR_MAGIC = "ZHB1";
3
3
  export type ColumnType = "u8" | "u16" | "u32" | "i32" | "u64" | "f64";
4
4
  export interface HourHeader {
@@ -12,6 +12,8 @@ export interface HourHeader {
12
12
  /** Exports: timestamp of each operation counted in `skipped` (`null`: unknown). */
13
13
  readonly skippedTimes?: readonly (number | null)[];
14
14
  readonly market: boolean;
15
+ /** Market flags with one bit per virtual side; absent from older files. */
16
+ readonly virtualSides?: boolean;
15
17
  readonly order: boolean;
16
18
  /** Absent from files written before transaction ids. */
17
19
  readonly tx?: boolean;
package/dist/hourfile.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { constants, zstdCompressSync, zstdDecompressSync } from "node:zlib";
2
+ import { MarketFlag } from "./sdk.js";
2
3
  export const HOUR_MAGIC = "ZHB1";
3
4
  export const nodeRealm = {
4
5
  Uint8Array,
@@ -237,6 +238,12 @@ export function batchOf(raw, realm = nodeRealm) {
237
238
  const m = new realm.Object();
238
239
  for (const [name, type] of MARKET_COLUMNS)
239
240
  m[name] = column(`market.${name}`, type);
241
+ const flags = m.flags;
242
+ if (!header.virtualSides)
243
+ for (let i = 0; i < flags.length; i++)
244
+ if ((flags[i] & MarketFlag.virtualReserves) !== 0)
245
+ flags[i] |=
246
+ MarketFlag.virtualTokenReserve | MarketFlag.virtualQuoteReserve;
240
247
  batch.market = m;
241
248
  }
242
249
  if (header.order) {
package/dist/limits.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Limits of funrun v2, mirrored from the server defaults. The server is the
2
+ * Limits of hosted functions, mirrored from the server defaults. The server is the
3
3
  * authority; these constants let tools and programs plan around them.
4
4
  *
5
5
  * No dependency: safe to import from guest code.
@@ -11,7 +11,11 @@ export declare const execLimits: {
11
11
  * reads excluded: they count in `readMs`.
12
12
  */
13
13
  readonly cpuMs: 10000;
14
- /** Wall time, waits (`ctx.jobs.wait`, host operations) included. */
14
+ /**
15
+ * Wall time, waits (`ctx.jobs.wait`, host operations, timers) included.
16
+ * Time spent waiting for history reads (`ctx.data.*`) does not count, up
17
+ * to `readWaitMs`.
18
+ */
15
19
  readonly wallMs: 60000;
16
20
  readonly heapBytes: number;
17
21
  /** JSON arguments of a direct call or a job. */
@@ -34,6 +38,16 @@ export declare const execLimits: {
34
38
  readonly readMs: 30000;
35
39
  /** User storage bytes read plus written by one execution (`io_limit`). */
36
40
  readonly storageBytes: number;
41
+ /**
42
+ * History read wait not counted in `wallMs`, at most: an execution lasts
43
+ * `wallMs + readWaitMs` (15 minutes) at most.
44
+ */
45
+ readonly readWaitMs: number;
46
+ /**
47
+ * Of one history read call, at most this much is given back to `wallMs`:
48
+ * a read that hangs still ends at the wall limit.
49
+ */
50
+ readonly readCallWaitMs: 120000;
37
51
  };
38
52
  /** User storage (`ctx.storage`), per account. Server defaults; operators may change them. */
39
53
  export declare const storageLimits: {
package/dist/limits.js CHANGED
@@ -11,6 +11,8 @@ export const execLimits = {
11
11
  hostOps: 10_000,
12
12
  readMs: 30_000,
13
13
  storageBytes: 1024 * MIB,
14
+ readWaitMs: 14 * 60_000,
15
+ readCallWaitMs: 120_000,
14
16
  };
15
17
  export const storageLimits = {
16
18
  shortQuotaBytes: 1024 * MIB,
@@ -69,7 +69,7 @@ export declare class ReadFailed extends Error {
69
69
  export declare const sha256: (bytes: Uint8Array) => string;
70
70
  export declare function windowComplete(c: Coverage, since: number, until: number): boolean;
71
71
  export declare function hourComplete(c: Coverage, hour: number, since: number, until: number): boolean;
72
- /** `funrun_sandbox::natives::batch_bytes`: what a batch costs against `readBytes`. */
72
+ /** What a batch costs against `readBytes`, as the service counts it. */
73
73
  export declare function batchBytes(h: HourHeader): number;
74
74
  export interface HourRead {
75
75
  readonly batch: HourBatch;
@@ -93,14 +93,31 @@ export declare class LocalData {
93
93
  find(s: ReadSpec, since: number, until: number): ReadMeta | undefined;
94
94
  /**
95
95
  * The next batch of a read after `afterHour` (`null`: none left), as
96
- * `ColumnsReader::read_hour` answers it. Throws {@link ReadFailed}.
96
+ * the service's reader answers it. Throws {@link ReadFailed}.
97
97
  */
98
98
  readHour(s: ReadSpec, since: number, until: number, allowPartial: boolean, afterHour: number | null, realm?: Realm): HourRead | null;
99
- /** `/v1/tokens/{mint}/totals` saved by the export, or `undefined`. */
99
+ /** Totals of a mint saved by the export, or `undefined`. */
100
100
  totals(mint: string, quote: string | null): Json | undefined;
101
- /** A `/v1/volume/search` page saved by the export for exactly these parameters. */
101
+ /** Info of a mint saved by the export (`--totals`), or `undefined`. */
102
+ info(mint: string): Json | undefined;
103
+ /** The error `tokenTotals` answered at export, or `undefined`. */
104
+ totalsFailure(mint: string, quote: string | null): ReadFailure | undefined;
105
+ /** The error `tokenInfo` answered at export, or `undefined`. */
106
+ infoFailure(mint: string): ReadFailure | undefined;
107
+ /** The error a volume search page answered at export, or `undefined`. */
108
+ volumeFailure(params: readonly (readonly [string, string])[]): ReadFailure | undefined;
109
+ /** The answer `ctx.compute(plan)` gave at export (`zyrk export --compute`). */
110
+ computeResult(plan: unknown): Json | undefined;
111
+ /** The error `ctx.compute(plan)` answered at export, or `undefined`. */
112
+ computeFailure(plan: unknown): ReadFailure | undefined;
113
+ /** A volume search page saved by the export for exactly these parameters. */
102
114
  volumePage(params: readonly (readonly [string, string])[]): Json | undefined;
103
115
  }
116
+ /** JSON with object keys sorted at every level (the name of a plan). */
117
+ export declare function canonical(v: unknown): string;
118
+ /** File name of a saved compute answer. */
119
+ export declare function computeName(plan: unknown): string;
104
120
  export declare const totalsName: (mint: string, quote: string | null) => string;
121
+ export declare const infoName: (mint: string) => string;
105
122
  /** Parameters as the sandbox passes them (`[key, string value]`), sorted. */
106
123
  export declare function volumeName(params: readonly (readonly [string, string])[]): string;
@@ -315,7 +315,8 @@ export class LocalData {
315
315
  batch: batchOf(raw, realm),
316
316
  bytes: batchBytes(raw.header),
317
317
  recut: recuts,
318
- stale: stale(raw.header.source, hour),
318
+ stale: stale(raw.header.source, hour) ||
319
+ (raw.header.market && !raw.header.virtualSides),
319
320
  };
320
321
  }
321
322
  return null;
@@ -330,15 +331,65 @@ export class LocalData {
330
331
  throw corrupt(file, e, again);
331
332
  }
332
333
  }
334
+ #failure(file, again) {
335
+ const saved = this.#json(join(this.dir, "errors", file), again);
336
+ if (saved === undefined)
337
+ return undefined;
338
+ const { code, message } = saved?.error ?? {};
339
+ if (typeof code !== "string" || typeof message !== "string")
340
+ throw corrupt(join(this.dir, "errors", file), new Error("not an error answer"), again);
341
+ const retry = saved.retryAfterMs;
342
+ return {
343
+ code,
344
+ message,
345
+ ...(typeof retry === "number" ? { retryAfterMs: retry } : {}),
346
+ };
347
+ }
333
348
  totals(mint, quote) {
334
349
  return this.#json(join(this.dir, "totals", totalsName(mint, quote)), `zyrk export --totals ${mint}${quote ? ` --quote ${quote}` : ""}`);
335
350
  }
351
+ info(mint) {
352
+ return this.#json(join(this.dir, "info", infoName(mint)), `zyrk export --totals ${mint}`);
353
+ }
354
+ totalsFailure(mint, quote) {
355
+ return this.#failure(join("totals", totalsName(mint, quote)), `zyrk export --totals ${mint}${quote ? ` --quote ${quote}` : ""}`);
356
+ }
357
+ infoFailure(mint) {
358
+ return this.#failure(join("info", infoName(mint)), `zyrk export --totals ${mint}`);
359
+ }
360
+ volumeFailure(params) {
361
+ return this.#failure(join("volume", `${volumeName(params)}.json`), "zyrk export --volume-search '<params>'");
362
+ }
363
+ computeResult(plan) {
364
+ const saved = this.#json(join(this.dir, "compute", `${computeName(plan)}.json`), "zyrk export --compute '<plan>'");
365
+ return saved?.result;
366
+ }
367
+ computeFailure(plan) {
368
+ return this.#failure(join("compute", `${computeName(plan)}.json`), "zyrk export --compute '<plan>'");
369
+ }
336
370
  volumePage(params) {
337
371
  const saved = this.#json(join(this.dir, "volume", `${volumeName(params)}.json`), "zyrk export --volume-search '<params>'");
338
372
  return saved?.page;
339
373
  }
340
374
  }
375
+ export function canonical(v) {
376
+ if (Array.isArray(v))
377
+ return `[${v.map(canonical).join(",")}]`;
378
+ if (v !== null && typeof v === "object")
379
+ return `{${Object.keys(v)
380
+ .sort()
381
+ .map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`)
382
+ .join(",")}}`;
383
+ return JSON.stringify(v) ?? "null";
384
+ }
385
+ export function computeName(plan) {
386
+ return createHash("sha256")
387
+ .update(canonical(plan))
388
+ .digest("hex")
389
+ .slice(0, 32);
390
+ }
341
391
  export const totalsName = (mint, quote) => quote ? `${mint}~${quote}.json` : `${mint}.json`;
392
+ export const infoName = (mint) => `${mint}.json`;
342
393
  export function volumeName(params) {
343
394
  const canonical = JSON.stringify([...params].sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)));
344
395
  return createHash("sha256").update(canonical).digest("hex").slice(0, 32);
@@ -1,15 +1,15 @@
1
1
  import { type LocalData, type ReadMeta } from "./local-data.js";
2
2
  import { type HourBatch, type Subscription } from "./sdk.js";
3
- /** `windows::LAG_MS`: the first 30 s after coverage starts are not trusted. */
3
+ /** The first 30 s after coverage starts are not trusted (as the service). */
4
4
  export declare const LAG_MS = 30000;
5
5
  /** Events per delivery, as `delivery_batch` of the service. */
6
6
  export declare const DELIVERY_BATCH = 32;
7
- /** Largest `batchSize` of a replay (`replay::MAX_BATCH` of the service). */
7
+ /** Largest `batchSize` of a replay of the service. */
8
8
  export declare const REPLAY_MAX_BATCH = 1000;
9
- /** History items per source page of the service (`ReplayLimits::page`). */
9
+ /** History items per source page of the service's replays. */
10
10
  export declare const SOURCE_PAGE = 500;
11
11
  /**
12
- * Fields of the live operation JSON (`query::operation`) that exported
12
+ * Fields of the live operation JSON that exported
13
13
  * columns do not hold: absent from replayed operations.
14
14
  */
15
15
  export declare const MISSING_OPERATION_FIELDS: readonly ["prices (provider strings: read batch.price… instead)", "usdValue", "finality", "provenance", "market (the provider's raw object)"];
@@ -26,7 +26,7 @@ export interface ReplayProgress {
26
26
  gaps: number;
27
27
  orderFallbacks: number;
28
28
  }
29
- /** Decimal string of a raw amount (`query::dec_text`). */
29
+ /** Decimal string of a raw amount, as the live JSON writes it. */
30
30
  export declare function formatRaw(raw: bigint, decimals: number): string;
31
31
  /** Row `i` as the live publication shapes it (without {@link MISSING_OPERATION_FIELDS}). */
32
32
  export declare function operationOf(b: HourBatch, i: number): Record<string, unknown>;
@@ -82,7 +82,8 @@ function poolEventOf(b, i, kind) {
82
82
  const m = b.market;
83
83
  const flags = m.flags[i];
84
84
  const reserves = (flags & MarketFlag.reserves) !== 0;
85
- const virtual = (flags & MarketFlag.virtualReserves) !== 0;
85
+ const vToken = (flags & MarketFlag.virtualTokenReserve) !== 0;
86
+ const vQuote = (flags & MarketFlag.virtualQuoteReserve) !== 0;
86
87
  const op = operationOf(b, i);
87
88
  return {
88
89
  ...op,
@@ -93,10 +94,10 @@ function poolEventOf(b, i, kind) {
93
94
  market: {
94
95
  tokenReserveRaw: reserves ? m.tokenReserve[i].toString() : null,
95
96
  quoteReserveRaw: reserves ? m.quoteReserve[i].toString() : null,
96
- virtualTokenReserveRaw: virtual
97
+ virtualTokenReserveRaw: vToken
97
98
  ? m.virtualTokenReserve[i].toString()
98
99
  : null,
99
- virtualQuoteReserveRaw: virtual
100
+ virtualQuoteReserveRaw: vQuote
100
101
  ? m.virtualQuoteReserve[i].toString()
101
102
  : null,
102
103
  signer: addressAt(b, m.signer[i]),
@@ -9,9 +9,9 @@ export declare const localStorageLimits: {
9
9
  readonly maxUploads: 4;
10
10
  /** Bytes read plus written by one execution (`io_limit`). */
11
11
  readonly ioBytes: number;
12
- /** Quota cost floor of one object (`StorageOptions::min_object_bytes`). */
12
+ /** Quota cost floor of one object. */
13
13
  readonly minObjectBytes: 4096;
14
- /** Objects per area (`AreaOptions::max_objects`). */
14
+ /** Objects per area. */
15
15
  readonly maxObjects: 100000;
16
16
  };
17
17
  export interface AreaConfig {
package/dist/local.d.ts CHANGED
@@ -199,9 +199,9 @@ export interface KeepStorage {
199
199
  readonly from?: "short" | "long" | "durable";
200
200
  readonly keys?: readonly string[];
201
201
  }
202
- /** The service's `ReplayLimits::max_duration_ms` (30 min). */
202
+ /** Longest replay of the service (30 min). */
203
203
  export declare const REPLAY_MAX_DURATION_MS: number;
204
- /** The service's `FeedLimits::replay_bytes` (16 MiB). */
204
+ /** Unread feed of one replay on the service (16 MiB). */
205
205
  export declare const REPLAY_FEED_BYTES: number;
206
206
  /** What {@link LocalRunner.replay} answers (the service's replay record, locally). */
207
207
  export interface LocalReplay {