@johannc-dev/zyrk 0.2.1 → 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/README.md +7 -0
- package/dist/cli.d.ts +6 -1
- package/dist/cli.js +41 -5
- package/dist/client.d.ts +11 -2
- package/dist/client.js +5 -1
- package/dist/contracts.d.ts +16 -0
- package/dist/contracts.js +14 -0
- package/dist/export.d.ts +12 -2
- package/dist/export.js +51 -5
- package/dist/limits.d.ts +15 -1
- package/dist/limits.js +2 -0
- package/dist/local-data.d.ts +8 -0
- package/dist/local-data.js +23 -0
- package/dist/local.js +121 -4
- package/dist/sandbox-bootstrap.js +60 -0
- package/dist/sdk.d.ts +210 -2
- package/dist/sdk.js +12 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,6 +53,13 @@ npx zyrk run wallet countBuys '{"wallet":"…","sinceMs":0,"untilMs":3600000}'
|
|
|
53
53
|
autres API réseau dans votre code, et l'ancienne API 0.1 (`ctx.data.query`,
|
|
54
54
|
`ctx.data.pages`) avec un guide de migration.
|
|
55
55
|
|
|
56
|
+
Supprimer un programme : `npx zyrk delete wallet` (demande confirmation ;
|
|
57
|
+
`--yes` dans un script) ou `client.delete("wallet")`. Partent ses versions,
|
|
58
|
+
son état, ses abonnements, ses exécutions (celles en cours sont annulées) et
|
|
59
|
+
ses replays ; `ctx.storage` reste (il appartient au compte). La place est
|
|
60
|
+
libérée tout de suite et le même nom redéployé repart de zéro. Un programme
|
|
61
|
+
absent répond `404 program_not_found`.
|
|
62
|
+
|
|
56
63
|
Modules :
|
|
57
64
|
|
|
58
65
|
| Import | Contenu |
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export declare const VERSION = "0.2.
|
|
2
|
+
export declare const VERSION = "0.2.2";
|
|
3
3
|
/** `2026-11-07`, `2026-11-07T13:00:00Z` or milliseconds since the epoch. */
|
|
4
4
|
export declare function timeOf(text: string): number;
|
|
5
|
+
/**
|
|
6
|
+
* Whether `zyrk delete` may go on: `--yes`, or a `y` typed at the prompt.
|
|
7
|
+
* Without a terminal (a script, CI) and without `--yes` it refuses.
|
|
8
|
+
*/
|
|
9
|
+
export declare function confirmDeletion(name: string, yes: boolean, interactive?: boolean): Promise<boolean>;
|
package/dist/cli.js
CHANGED
|
@@ -5,10 +5,10 @@ import { ChildProcessSpawner } from "effect/process";
|
|
|
5
5
|
import { BUNDLE_PATH, writeBundleEffect } from "./build.js";
|
|
6
6
|
import { ZyrkClient } from "./client.js";
|
|
7
7
|
import { decode, Json } from "./contracts.js";
|
|
8
|
-
import { exportHistory, exportTotals, exportVolumeSearch, } from "./export.js";
|
|
8
|
+
import { exportHistory, exportTotals, exportVolumeSearch, exportCompute, } from "./export.js";
|
|
9
9
|
import { LocalRunner } from "./local.js";
|
|
10
10
|
import { DecodeError, messageOf, } from "./errors.js";
|
|
11
|
-
export const VERSION = "0.2.
|
|
11
|
+
export const VERSION = "0.2.2";
|
|
12
12
|
const print = (value) => Console.log(JSON.stringify(value, null, 2));
|
|
13
13
|
const entry = Argument.String("entry.ts").pipe(Argument.withDescription("TypeScript or JavaScript entry point of the program"));
|
|
14
14
|
const program = Argument.String("program").pipe(Argument.withDescription("Program name"));
|
|
@@ -47,6 +47,26 @@ export function timeOf(text) {
|
|
|
47
47
|
throw new Error(`Not a date: ${text}`);
|
|
48
48
|
return ms;
|
|
49
49
|
}
|
|
50
|
+
export async function confirmDeletion(name, yes, interactive = process.stdin.isTTY === true) {
|
|
51
|
+
if (yes)
|
|
52
|
+
return true;
|
|
53
|
+
if (!interactive) {
|
|
54
|
+
process.stderr.write(`error[NotConfirmed]: zyrk delete ${name} deletes the program, its state, subscriptions, executions and replays: add --yes to confirm without a terminal\n`);
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
const { createInterface } = await import("node:readline/promises");
|
|
58
|
+
const prompt = createInterface({
|
|
59
|
+
input: process.stdin,
|
|
60
|
+
output: process.stderr,
|
|
61
|
+
});
|
|
62
|
+
try {
|
|
63
|
+
const answer = await prompt.question(`Delete program ${name} with its state, subscriptions, executions and replays (ctx.storage stays)? [y/N] `);
|
|
64
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
prompt.close();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
50
70
|
const local = (name, body) => Effect.promise(() => body().catch((error) => {
|
|
51
71
|
process.stderr.write(`error[${name}]: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
52
72
|
process.exitCode = 1;
|
|
@@ -67,9 +87,10 @@ const exportCommand = Command.make("export", {
|
|
|
67
87
|
"with-tx": flag("with-tx", "Export batch.tx (transaction of each trade, place in it)"),
|
|
68
88
|
refresh: flag("refresh", "Download every hour again (a finished export only re-downloads the hours that were open or not sealed, or exported by an older version)"),
|
|
69
89
|
totals: optionalString("totals", "Also save the totals of this mint (ctx.data.tokenTotals, tokenInfo)"),
|
|
70
|
-
quote: optionalString("quote", "Quote mint of --totals"),
|
|
90
|
+
quote: optionalString("quote", "Quote mint of --totals, or auto"),
|
|
91
|
+
compute: optionalString("compute", "Also save the answer of ctx.compute(<json plan or array of plans>), computed by the service"),
|
|
71
92
|
"volume-search": optionalString("volume-search", "Also save every page of ctx.data.volumeSearch(<json params>)"),
|
|
72
|
-
rpm: Flag.Int("rpm").pipe(Flag.withDescription("At most this many requests per minute"), Flag.optional),
|
|
93
|
+
rpm: Flag.Int("rpm").pipe(Flag.withDescription("At most this many requests per minute (optional: the server allows 30 a second per account)"), Flag.optional),
|
|
73
94
|
out: Flag.String("out").pipe(Flag.withAlias("o"), Flag.withDescription("Data directory (created, resumed if it exists)"), Flag.withDefault("data")),
|
|
74
95
|
}, (input) => local("export", async () => {
|
|
75
96
|
const url = process.env.TH_API_URL;
|
|
@@ -119,6 +140,12 @@ const exportCommand = Command.make("export", {
|
|
|
119
140
|
await exportTotals(connection, input.out, totals, value(input.quote) ?? null);
|
|
120
141
|
work = true;
|
|
121
142
|
}
|
|
143
|
+
const computeText = value(input.compute);
|
|
144
|
+
if (computeText) {
|
|
145
|
+
const r = await exportCompute(connection, input.out, JSON.parse(computeText));
|
|
146
|
+
process.stderr.write(`compute: ${r.rows === null ? "an error answer (saved: the local run throws it)" : `${r.rows} row(s)`}\n`);
|
|
147
|
+
work = true;
|
|
148
|
+
}
|
|
122
149
|
const volume = value(input["volume-search"]);
|
|
123
150
|
if (volume) {
|
|
124
151
|
const pages = await exportVolumeSearch(connection, input.out, JSON.parse(volume));
|
|
@@ -126,7 +153,7 @@ const exportCommand = Command.make("export", {
|
|
|
126
153
|
work = true;
|
|
127
154
|
}
|
|
128
155
|
if (!work)
|
|
129
|
-
throw new Error("Nothing to export: --token/--wallet, --totals
|
|
156
|
+
throw new Error("Nothing to export: --token/--wallet, --totals, --volume-search or --compute");
|
|
130
157
|
})).pipe(Command.withDescription("Download history (one file per hour, as hosted programs see it) for zyrk dev. Resumes an interrupted export."));
|
|
131
158
|
const devCommand = Command.make("dev", {
|
|
132
159
|
entry,
|
|
@@ -248,6 +275,15 @@ const zyrk = Command.make("zyrk").pipe(Command.withDescription("Zyrk hosted func
|
|
|
248
275
|
Command.make("programs", {}, () => withClient((c) => c.programs())).pipe(Command.withDescription("List the programs of the account")),
|
|
249
276
|
onArgument("inspect", "Show a program", program, (c, p) => c.inspect(p)),
|
|
250
277
|
onArgument("executions", "Recent executions of a program", program, (c, p) => c.executions(p)),
|
|
278
|
+
Command.make("delete", {
|
|
279
|
+
program,
|
|
280
|
+
yes: flag("yes", "Do not ask for confirmation"),
|
|
281
|
+
}, (input) => Effect.promise(() => confirmDeletion(input.program, input.yes)).pipe(Effect.flatMap((confirmed) => {
|
|
282
|
+
if (confirmed)
|
|
283
|
+
return withClient((c) => c.delete(input.program));
|
|
284
|
+
process.exitCode = 1;
|
|
285
|
+
return Effect.void;
|
|
286
|
+
}))).pipe(Command.withDescription("Delete a program and everything bound to it (state, subscriptions, executions, replays); ctx.storage stays. Asks first unless --yes")),
|
|
251
287
|
onArgument("pause", "Pause a program", program, (c, p) => c.pause(p, true)),
|
|
252
288
|
onArgument("resume", "Resume a program", program, (c, p) => c.pause(p, false)),
|
|
253
289
|
exportCommand,
|
package/dist/client.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Context, Effect, Layer } from "effect";
|
|
2
|
-
import { Job, Json, ProgramSummary, type JobResult } from "./contracts.js";
|
|
2
|
+
import { Job, Json, ProgramDeleted, ProgramSummary, type JobResult } from "./contracts.js";
|
|
3
3
|
import { ApiError, ConfigError, DecodeError, ExecutionEnded, ExecutionFailed, NetworkError, WaitTimeout } from "./errors.js";
|
|
4
4
|
export * from "./errors.js";
|
|
5
|
-
export type { Job, JobResult, ProgramSummary } from "./contracts.js";
|
|
5
|
+
export type { Job, JobResult, ProgramDeleted, ProgramSummary, } from "./contracts.js";
|
|
6
6
|
import type { ReplayFeedItem, ReplayRecord, ReplayRequest } from "./sdk.js";
|
|
7
7
|
export type { ReplayFeedItem, ReplayRecord, ReplayRequest } from "./sdk.js";
|
|
8
8
|
export interface ClientConfig {
|
|
@@ -46,6 +46,13 @@ export interface ZyrkClientService {
|
|
|
46
46
|
programs(): Effect.Effect<readonly ProgramSummary[], RequestError>;
|
|
47
47
|
inspect(program: string): Effect.Effect<Json, RequestError>;
|
|
48
48
|
pause(program: string, paused?: boolean): Effect.Effect<Json, RequestError>;
|
|
49
|
+
/**
|
|
50
|
+
* Deletes the program and everything bound to it (versions, state,
|
|
51
|
+
* subscriptions, executions, replays); what runs of it is cancelled.
|
|
52
|
+
* The account's storage stays. Fails with `program_not_found` (404) when
|
|
53
|
+
* there is no such program, also when a retried call already deleted it.
|
|
54
|
+
*/
|
|
55
|
+
delete(program: string): Effect.Effect<ProgramDeleted, RequestError>;
|
|
49
56
|
/** Recent executions of a program (summaries). */
|
|
50
57
|
executions(program: string): Effect.Effect<Json, RequestError>;
|
|
51
58
|
/** Starts an execution and returns its id. */
|
|
@@ -125,6 +132,8 @@ export declare class FunctionsClient {
|
|
|
125
132
|
programs(signal?: AbortSignal): Promise<readonly ProgramSummary[]>;
|
|
126
133
|
inspect(program: string, signal?: AbortSignal): Promise<Json>;
|
|
127
134
|
pause(program: string, paused?: boolean, signal?: AbortSignal): Promise<Json>;
|
|
135
|
+
/** See {@link ZyrkClientService.delete}. */
|
|
136
|
+
delete(program: string, signal?: AbortSignal): Promise<ProgramDeleted>;
|
|
128
137
|
executions(program: string, signal?: AbortSignal): Promise<Json>;
|
|
129
138
|
start(program: string, input: StartInput, signal?: AbortSignal): Promise<string>;
|
|
130
139
|
execution(id: string, signal?: AbortSignal): Promise<Job>;
|
package/dist/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Cause, Config, Context, Effect, Exit, Layer, Option, Redacted, Result, Schema, } from "effect";
|
|
2
|
-
import { decode, Job, Json, ProgramSummary, Started, toJobResult, } from "./contracts.js";
|
|
2
|
+
import { decode, Job, Json, ProgramDeleted, ProgramSummary, Started, toJobResult, } from "./contracts.js";
|
|
3
3
|
import { ApiError, ConfigError, DecodeError, ExecutionEnded, ExecutionFailed, Failure, messageOf, NetworkError, WaitTimeout, } from "./errors.js";
|
|
4
4
|
export * from "./errors.js";
|
|
5
5
|
const ServerError = Schema.Struct({
|
|
@@ -630,6 +630,7 @@ export const make = (config) => {
|
|
|
630
630
|
programs: () => request("GET", "programs").pipe(Effect.flatMap(decode(Schema.Array(ProgramSummary), "programs"))),
|
|
631
631
|
inspect: (program) => request("GET", `programs/${segment(program)}`),
|
|
632
632
|
pause: (program, paused = true) => request("POST", `programs/${segment(program)}/pause`, { paused }),
|
|
633
|
+
delete: (program) => request("DELETE", `programs/${segment(program)}`).pipe(Effect.flatMap(decode(ProgramDeleted, "delete response"))),
|
|
633
634
|
executions: (program) => request("GET", `programs/${segment(program)}/executions`),
|
|
634
635
|
start,
|
|
635
636
|
execution,
|
|
@@ -695,6 +696,9 @@ export class FunctionsClient {
|
|
|
695
696
|
pause(program, paused = true, signal) {
|
|
696
697
|
return runPromise(this.client.pause(program, paused), signal);
|
|
697
698
|
}
|
|
699
|
+
delete(program, signal) {
|
|
700
|
+
return runPromise(this.client.delete(program), signal);
|
|
701
|
+
}
|
|
698
702
|
executions(program, signal) {
|
|
699
703
|
return runPromise(this.client.executions(program), signal);
|
|
700
704
|
}
|
package/dist/contracts.d.ts
CHANGED
|
@@ -277,6 +277,22 @@ export declare const ProgramSummary: Schema.Struct<{
|
|
|
277
277
|
readonly paused: Schema.Boolean;
|
|
278
278
|
}>;
|
|
279
279
|
export type ProgramSummary = typeof ProgramSummary.Type;
|
|
280
|
+
/** What `DELETE programs/{program}` removed (counts). */
|
|
281
|
+
export declare const ProgramDeleted: Schema.Struct<{
|
|
282
|
+
readonly deleted: Schema.Boolean;
|
|
283
|
+
readonly program: Schema.String;
|
|
284
|
+
readonly removed: Schema.Struct<{
|
|
285
|
+
readonly versions: Schema.Number;
|
|
286
|
+
readonly stateKeys: Schema.Number;
|
|
287
|
+
readonly subscriptions: Schema.Number;
|
|
288
|
+
readonly executions: Schema.Number;
|
|
289
|
+
readonly cancelledExecutions: Schema.Number;
|
|
290
|
+
readonly replays: Schema.Number;
|
|
291
|
+
readonly cancelledReplays: Schema.Number;
|
|
292
|
+
readonly streamEvents: Schema.Number;
|
|
293
|
+
}>;
|
|
294
|
+
}>;
|
|
295
|
+
export type ProgramDeleted = typeof ProgramDeleted.Type;
|
|
280
296
|
/** A schema that decodes without Effect services, so decoding is synchronous. */
|
|
281
297
|
export type Decodable = Schema.Top & {
|
|
282
298
|
readonly DecodingServices: never;
|
package/dist/contracts.js
CHANGED
|
@@ -111,6 +111,20 @@ export const ProgramSummary = Schema.Struct({
|
|
|
111
111
|
stateVersion: Schema.Number,
|
|
112
112
|
paused: Schema.Boolean,
|
|
113
113
|
});
|
|
114
|
+
export const ProgramDeleted = Schema.Struct({
|
|
115
|
+
deleted: Schema.Boolean,
|
|
116
|
+
program: Schema.String,
|
|
117
|
+
removed: Schema.Struct({
|
|
118
|
+
versions: Schema.Number,
|
|
119
|
+
stateKeys: Schema.Number,
|
|
120
|
+
subscriptions: Schema.Number,
|
|
121
|
+
executions: Schema.Number,
|
|
122
|
+
cancelledExecutions: Schema.Number,
|
|
123
|
+
replays: Schema.Number,
|
|
124
|
+
cancelledReplays: Schema.Number,
|
|
125
|
+
streamEvents: Schema.Number,
|
|
126
|
+
}),
|
|
127
|
+
});
|
|
114
128
|
export const NonNegativeInt = Schema.Number.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0));
|
|
115
129
|
export const decodeWith = (schema, options, onError) => {
|
|
116
130
|
const parse = Schema.decodeUnknownResult(schema);
|
package/dist/export.d.ts
CHANGED
|
@@ -5,11 +5,11 @@ 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
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 429s in a row 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;
|
|
@@ -85,6 +85,16 @@ export declare function exportHistory(connection: ExportConnection, out: string,
|
|
|
85
85
|
* or the error answer saved for them (`{ error: { kind, code, message } }`).
|
|
86
86
|
*/
|
|
87
87
|
export declare function exportTotals(connection: ExportConnection, out: string, mint: string, quote?: string | null): Promise<Json>;
|
|
88
|
+
/**
|
|
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
|
+
}>;
|
|
88
98
|
/**
|
|
89
99
|
* Saves every page of a volume search for `params`, following the
|
|
90
100
|
* cursor exactly as `ctx.data.volumeSearch` does, so that the same call in
|
package/dist/export.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { decompressHour } from "./hourfile.js";
|
|
4
|
-
import { HOUR_MS, READ_FORMAT, infoName, 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;
|
|
@@ -49,7 +49,7 @@ class Http {
|
|
|
49
49
|
constructor(c) {
|
|
50
50
|
this.c = c;
|
|
51
51
|
}
|
|
52
|
-
async raw(path, query, keep) {
|
|
52
|
+
async raw(path, query, keep, post) {
|
|
53
53
|
const f = this.c.fetch ?? fetch;
|
|
54
54
|
const url = new URL(this.c.url.replace(/\/+$/, "") + path);
|
|
55
55
|
for (const [k, v] of Object.entries(query))
|
|
@@ -70,7 +70,13 @@ class Http {
|
|
|
70
70
|
let bytes;
|
|
71
71
|
try {
|
|
72
72
|
response = await f(url, {
|
|
73
|
-
headers: {
|
|
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 } : {}),
|
|
74
80
|
...(this.c.signal ? { signal: this.c.signal } : {}),
|
|
75
81
|
});
|
|
76
82
|
bytes = new Uint8Array(await response.arrayBuffer());
|
|
@@ -141,8 +147,8 @@ class Http {
|
|
|
141
147
|
throw new ExportError(`${response.status} ${code}: ${message}`, code, response.status);
|
|
142
148
|
}
|
|
143
149
|
}
|
|
144
|
-
async get(path, query, keep) {
|
|
145
|
-
const answer = await this.raw(path, query, keep);
|
|
150
|
+
async get(path, query, keep, post) {
|
|
151
|
+
const answer = await this.raw(path, query, keep, post);
|
|
146
152
|
const text = new TextDecoder().decode(answer.bytes);
|
|
147
153
|
try {
|
|
148
154
|
return {
|
|
@@ -338,6 +344,7 @@ const RECORDED = new Set([
|
|
|
338
344
|
"invalid_amount",
|
|
339
345
|
"invalid_cursor",
|
|
340
346
|
"usd_volume_unavailable",
|
|
347
|
+
"invalid_quote",
|
|
341
348
|
"partial_coverage",
|
|
342
349
|
"query_work_limit",
|
|
343
350
|
]);
|
|
@@ -393,6 +400,45 @@ export async function exportTotals(connection, out, mint, quote = null) {
|
|
|
393
400
|
save(connection, out, ["info", infoName(mint)], info, content(info), "tokenInfo");
|
|
394
401
|
return totals;
|
|
395
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
|
+
};
|
|
441
|
+
}
|
|
396
442
|
export async function exportVolumeSearch(connection, out, params) {
|
|
397
443
|
const http = new Http(connection);
|
|
398
444
|
mkdirSync(join(out, "volume"), { recursive: true });
|
package/dist/limits.d.ts
CHANGED
|
@@ -11,7 +11,11 @@ export declare const execLimits: {
|
|
|
11
11
|
* reads excluded: they count in `readMs`.
|
|
12
12
|
*/
|
|
13
13
|
readonly cpuMs: 10000;
|
|
14
|
-
/**
|
|
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
package/dist/local-data.d.ts
CHANGED
|
@@ -106,9 +106,17 @@ export declare class LocalData {
|
|
|
106
106
|
infoFailure(mint: string): ReadFailure | undefined;
|
|
107
107
|
/** The error a volume search page answered at export, or `undefined`. */
|
|
108
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;
|
|
109
113
|
/** A volume search page saved by the export for exactly these parameters. */
|
|
110
114
|
volumePage(params: readonly (readonly [string, string])[]): Json | undefined;
|
|
111
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;
|
|
112
120
|
export declare const totalsName: (mint: string, quote: string | null) => string;
|
|
113
121
|
export declare const infoName: (mint: string) => string;
|
|
114
122
|
/** Parameters as the sandbox passes them (`[key, string value]`), sorted. */
|
package/dist/local-data.js
CHANGED
|
@@ -360,11 +360,34 @@ export class LocalData {
|
|
|
360
360
|
volumeFailure(params) {
|
|
361
361
|
return this.#failure(join("volume", `${volumeName(params)}.json`), "zyrk export --volume-search '<params>'");
|
|
362
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
|
+
}
|
|
363
370
|
volumePage(params) {
|
|
364
371
|
const saved = this.#json(join(this.dir, "volume", `${volumeName(params)}.json`), "zyrk export --volume-search '<params>'");
|
|
365
372
|
return saved?.page;
|
|
366
373
|
}
|
|
367
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
|
+
}
|
|
368
391
|
export const totalsName = (mint, quote) => quote ? `${mint}~${quote}.json` : `${mint}.json`;
|
|
369
392
|
export const infoName = (mint) => `${mint}.json`;
|
|
370
393
|
export function volumeName(params) {
|
package/dist/local.js
CHANGED
|
@@ -6,7 +6,7 @@ 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));
|
|
@@ -21,6 +21,20 @@ function batchReadsFromEnv() {
|
|
|
21
21
|
}
|
|
22
22
|
return on;
|
|
23
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
|
+
};
|
|
24
38
|
let bootstrapText;
|
|
25
39
|
export function sandboxBootstrap() {
|
|
26
40
|
return (bootstrapText ??= readFileSync(join(here, "sandbox-bootstrap.js"), "utf8"));
|
|
@@ -230,6 +244,7 @@ export class LocalRunner {
|
|
|
230
244
|
pending: 0,
|
|
231
245
|
io: { bytes: 0 },
|
|
232
246
|
startedAt: 0,
|
|
247
|
+
readWaitMs: 0,
|
|
233
248
|
done,
|
|
234
249
|
finish,
|
|
235
250
|
};
|
|
@@ -370,7 +385,7 @@ export class LocalRunner {
|
|
|
370
385
|
if (e.usage.outputBytes + size > this.limits.exec.outputBytes * e.scale)
|
|
371
386
|
throw limit("output_limit", `Result, logs and emits went over ${this.limits.exec.outputBytes * e.scale} bytes`);
|
|
372
387
|
e.usage.outputBytes += size;
|
|
373
|
-
if (
|
|
388
|
+
if (this.#wallUsed(e) > this.limits.exec.wallMs)
|
|
374
389
|
throw limit("wall_limit", `The execution ran over ${this.limits.exec.wallMs} ms (measured locally, not preempted)`);
|
|
375
390
|
if (e.cancelled)
|
|
376
391
|
throw new HostError("permission", "cancelled", "Its parent failed: the job was cancelled");
|
|
@@ -446,6 +461,38 @@ export class LocalRunner {
|
|
|
446
461
|
};
|
|
447
462
|
const fromRead = (f) => new HostError(kindOfSdkCode(f.code), f.code, f.message, f.retryAfterMs);
|
|
448
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
|
+
};
|
|
449
496
|
const ms = (v) => typeof v === "number" && Number.isFinite(v) && Math.abs(v) < 9e15
|
|
450
497
|
? Math.trunc(v)
|
|
451
498
|
: undefined;
|
|
@@ -472,6 +519,9 @@ export class LocalRunner {
|
|
|
472
519
|
wf.readBytes > this.limits.account.workflowReadBytes)
|
|
473
520
|
throw readLimit();
|
|
474
521
|
};
|
|
522
|
+
const waited = (t0) => {
|
|
523
|
+
e.readWaitMs += Math.min(performance.now() - t0, this.limits.exec.readCallWaitMs);
|
|
524
|
+
};
|
|
475
525
|
const data = () => {
|
|
476
526
|
if (!this.data)
|
|
477
527
|
throw sdkError(program("local_data_missing", "No data directory: export history with zyrk export, then pass --data (or { data })."));
|
|
@@ -480,6 +530,7 @@ export class LocalRunner {
|
|
|
480
530
|
const saved = (answer, failure, missing) => {
|
|
481
531
|
let value;
|
|
482
532
|
let failed;
|
|
533
|
+
const t0 = performance.now();
|
|
483
534
|
try {
|
|
484
535
|
value = answer();
|
|
485
536
|
failed = value === undefined ? failure() : undefined;
|
|
@@ -489,6 +540,9 @@ export class LocalRunner {
|
|
|
489
540
|
throw sdkError(fromRead(error.failure));
|
|
490
541
|
throw error;
|
|
491
542
|
}
|
|
543
|
+
finally {
|
|
544
|
+
waited(t0);
|
|
545
|
+
}
|
|
492
546
|
if (failed)
|
|
493
547
|
throw sdkError(fromRead(failed));
|
|
494
548
|
if (value === undefined)
|
|
@@ -541,6 +595,7 @@ export class LocalRunner {
|
|
|
541
595
|
}
|
|
542
596
|
finally {
|
|
543
597
|
e.usage.readMs += performance.now() - t0;
|
|
598
|
+
waited(t0);
|
|
544
599
|
sample();
|
|
545
600
|
}
|
|
546
601
|
if (hour === null)
|
|
@@ -567,6 +622,63 @@ export class LocalRunner {
|
|
|
567
622
|
throw sdkError(program("invalid_argument", "Expected a mint address"));
|
|
568
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}`);
|
|
569
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));
|
|
681
|
+
},
|
|
570
682
|
volume: (text) => {
|
|
571
683
|
readAllowed();
|
|
572
684
|
e.usage.readCalls++;
|
|
@@ -602,8 +714,13 @@ export class LocalRunner {
|
|
|
602
714
|
},
|
|
603
715
|
};
|
|
604
716
|
}
|
|
717
|
+
#wallUsed(e) {
|
|
718
|
+
return (performance.now() -
|
|
719
|
+
e.startedAt -
|
|
720
|
+
Math.min(e.readWaitMs, this.limits.exec.readWaitMs));
|
|
721
|
+
}
|
|
605
722
|
#checkWall(e) {
|
|
606
|
-
if (
|
|
723
|
+
if (this.#wallUsed(e) > this.limits.exec.wallMs) {
|
|
607
724
|
e.fatal ??= limit("wall_limit", `The execution ran over ${this.limits.exec.wallMs} ms`);
|
|
608
725
|
throw e.fatal;
|
|
609
726
|
}
|
|
@@ -795,7 +912,7 @@ export class LocalRunner {
|
|
|
795
912
|
});
|
|
796
913
|
if (this.#waiting >= account.parallelExecutions - 1)
|
|
797
914
|
throw limit("job_wait_limit", `At most ${account.parallelExecutions - 1} executions wait at once`);
|
|
798
|
-
const left = this.limits.exec.wallMs - (
|
|
915
|
+
const left = this.limits.exec.wallMs - this.#wallUsed(e);
|
|
799
916
|
const timeout = Math.max(0, Math.min(typeof a.timeout_ms === "number" ? a.timeout_ms : left, left));
|
|
800
917
|
this.#waiting++;
|
|
801
918
|
let timer;
|
|
@@ -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;
|
|
@@ -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 } }),
|
package/dist/sdk.d.ts
CHANGED
|
@@ -388,6 +388,12 @@ export type TokenMetadata = {
|
|
|
388
388
|
readonly quoteName: string | null;
|
|
389
389
|
readonly quoteSymbol: string | null;
|
|
390
390
|
readonly quoteUri: string | null;
|
|
391
|
+
/**
|
|
392
|
+
* Decimals of `quoteMint`: the row's, else 9 for SOL and 6 for USDC, else
|
|
393
|
+
* those of the quote table (rows sent before 2026-10-29 18:15 UTC lack
|
|
394
|
+
* them), else `null` (absent from metadata saved by older exports).
|
|
395
|
+
*/
|
|
396
|
+
readonly quoteDecimals?: number | null;
|
|
391
397
|
/** Time, slot and signature of the row the values come from. */
|
|
392
398
|
readonly asOfMs: number | null;
|
|
393
399
|
readonly slot: number | null;
|
|
@@ -930,6 +936,189 @@ export interface StorageArea {
|
|
|
930
936
|
putJSON(key: string, value: JsonInput, options?: Omit<StoragePutOptions, "contentType">): Promise<StorageObjectHead>;
|
|
931
937
|
writer(key: string, options?: StoragePutOptions): Promise<StorageWriter>;
|
|
932
938
|
}
|
|
939
|
+
/** Milliseconds, or text such as `"500ms"`, `"30s"`, `"15m"`, `"6h"`, `"30d"`. */
|
|
940
|
+
export type ComputeDuration = number | `${number}${"ms" | "s" | "m" | "h" | "d"}`;
|
|
941
|
+
/** `[since, until)` in ms; `until` defaults to the call's time. */
|
|
942
|
+
export type ComputeWindow = {
|
|
943
|
+
readonly since: number;
|
|
944
|
+
readonly until?: number;
|
|
945
|
+
} | {
|
|
946
|
+
readonly last: ComputeDuration;
|
|
947
|
+
readonly until?: number;
|
|
948
|
+
};
|
|
949
|
+
/** Fields of a trade a filter or a measure reads. */
|
|
950
|
+
export type ComputeField = "side" | "protocol" | "token" | "trader" | "quoteMint" | "timeMs" | "ageMs" | "quoteRaw" | "tokenRaw" | "quoteAmount" | "tokenAmount" | "execPrice" | "isCreator" | "marketFlags" | "orderFlags";
|
|
951
|
+
/** Fields measured by `sum`, `min`, `max`, `avg`, `first`, `last`, `anchor`, `asOf`. */
|
|
952
|
+
export type ComputeNumericField = "timeMs" | "ageMs" | "quoteRaw" | "tokenRaw" | "quoteAmount" | "tokenAmount" | "execPrice";
|
|
953
|
+
type ComputeValue = string | number | boolean;
|
|
954
|
+
/** A comparison of one field: a value (equality), or operators (all must hold). */
|
|
955
|
+
export type ComputeCondition = ComputeValue | {
|
|
956
|
+
readonly eq?: ComputeValue;
|
|
957
|
+
readonly ne?: ComputeValue;
|
|
958
|
+
readonly gt?: number | string;
|
|
959
|
+
readonly gte?: number | string;
|
|
960
|
+
readonly lt?: number | string;
|
|
961
|
+
readonly lte?: number | string;
|
|
962
|
+
readonly in?: readonly ComputeValue[];
|
|
963
|
+
readonly notIn?: readonly ComputeValue[];
|
|
964
|
+
/** Flag fields (`marketFlags`, `orderFlags`): bits of the mask set. */
|
|
965
|
+
readonly hasAll?: number;
|
|
966
|
+
readonly hasAny?: number;
|
|
967
|
+
readonly hasNone?: number;
|
|
968
|
+
};
|
|
969
|
+
/**
|
|
970
|
+
* A filter: every field listed must pass (`and`), with `and`, `or`, `not`
|
|
971
|
+
* to combine. In `where`, the fields of a trade (and `"<stage>.<measure>"`);
|
|
972
|
+
* in `having`, the measures and keys of a group. A comparison with an
|
|
973
|
+
* unknown value (no trader, a creation not seen, a missing stage value) is
|
|
974
|
+
* false, `ne` and `notIn` included.
|
|
975
|
+
*/
|
|
976
|
+
export interface ComputeFilter {
|
|
977
|
+
readonly and?: readonly ComputeFilter[];
|
|
978
|
+
readonly or?: readonly ComputeFilter[];
|
|
979
|
+
readonly not?: ComputeFilter;
|
|
980
|
+
readonly [field: string]: ComputeCondition | ComputeFilter | readonly ComputeFilter[] | undefined;
|
|
981
|
+
}
|
|
982
|
+
export type ComputeKey = "token" | "trader" | "quoteMint" | "side" | "protocol"
|
|
983
|
+
/** `floor(timeMs / width) * width`. */
|
|
984
|
+
| {
|
|
985
|
+
readonly time: ComputeDuration;
|
|
986
|
+
}
|
|
987
|
+
/** `floor(ageMs / width) * width` (tokens whose creation is known). */
|
|
988
|
+
| {
|
|
989
|
+
readonly age: ComputeDuration;
|
|
990
|
+
};
|
|
991
|
+
type MeasureOptions = {
|
|
992
|
+
/** Only the trades that pass it. */
|
|
993
|
+
readonly where?: ComputeFilter;
|
|
994
|
+
/** Plans with `anchors`: the token's trades in `[anchor + from, anchor + to)` ms. */
|
|
995
|
+
readonly window?: readonly [number, number];
|
|
996
|
+
};
|
|
997
|
+
export type ComputeMeasure = ({
|
|
998
|
+
readonly count: true;
|
|
999
|
+
} & MeasureOptions) | ({
|
|
1000
|
+
readonly countDistinct: "token" | "trader" | "quoteMint";
|
|
1001
|
+
} & MeasureOptions) | ({
|
|
1002
|
+
readonly sum: ComputeNumericField;
|
|
1003
|
+
} & MeasureOptions) | ({
|
|
1004
|
+
readonly min: ComputeNumericField;
|
|
1005
|
+
} & MeasureOptions) | ({
|
|
1006
|
+
readonly max: ComputeNumericField;
|
|
1007
|
+
} & MeasureOptions) | ({
|
|
1008
|
+
readonly avg: ComputeNumericField;
|
|
1009
|
+
} & MeasureOptions)
|
|
1010
|
+
/** In the token's trade sequence (see the docs). */
|
|
1011
|
+
| ({
|
|
1012
|
+
readonly first: ComputeNumericField;
|
|
1013
|
+
} & MeasureOptions) | ({
|
|
1014
|
+
readonly last: ComputeNumericField;
|
|
1015
|
+
} & MeasureOptions)
|
|
1016
|
+
/** `Number(a) / Number(b)` of two other measures. */
|
|
1017
|
+
| {
|
|
1018
|
+
readonly ratio: readonly [string, string];
|
|
1019
|
+
}
|
|
1020
|
+
/** Plans with `anchors`: a field of the anchor trade. */
|
|
1021
|
+
| {
|
|
1022
|
+
readonly anchor: ComputeNumericField;
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Plans with `anchors`: `field` of the token's trade as of `anchor +
|
|
1026
|
+
* offsetMs` (the last at or before it; with `direction: "after"`, the
|
|
1027
|
+
* first at or after it), within `withinMs` (default and at most 1 h). An
|
|
1028
|
+
* array of offsets gives one column each, `"<name>@<offset>"`.
|
|
1029
|
+
*/
|
|
1030
|
+
| ({
|
|
1031
|
+
readonly asOf: ComputeNumericField;
|
|
1032
|
+
readonly offsetMs: number | readonly number[];
|
|
1033
|
+
readonly direction?: "before" | "after";
|
|
1034
|
+
readonly withinMs?: number;
|
|
1035
|
+
} & Omit<MeasureOptions, "window">);
|
|
1036
|
+
export interface ComputePlan {
|
|
1037
|
+
readonly range: ComputeWindow;
|
|
1038
|
+
/** Only these mints (1 to 10 000). */
|
|
1039
|
+
readonly tokens?: readonly Address[];
|
|
1040
|
+
/** Only the tokens created in this window (creations seen in its hours). */
|
|
1041
|
+
readonly created?: ComputeWindow;
|
|
1042
|
+
/** `"SOL"` (default), a quote mint, `"auto"` (per token, as `tokenTotals`) or `"any"`. */
|
|
1043
|
+
readonly quote?: "SOL" | "auto" | "any" | Address;
|
|
1044
|
+
readonly where?: ComputeFilter;
|
|
1045
|
+
readonly groupBy?: readonly ComputeKey[];
|
|
1046
|
+
/** 1 to 16 named measures; results list them in name order. */
|
|
1047
|
+
readonly measures: Readonly<Record<string, ComputeMeasure>>;
|
|
1048
|
+
readonly having?: ComputeFilter;
|
|
1049
|
+
/** The `n` groups with the largest `by`, per partition of the `per` keys. */
|
|
1050
|
+
readonly top?: {
|
|
1051
|
+
readonly by: string;
|
|
1052
|
+
readonly n: number;
|
|
1053
|
+
readonly per?: readonly string[];
|
|
1054
|
+
};
|
|
1055
|
+
readonly orderBy?: readonly (string | {
|
|
1056
|
+
readonly by: string;
|
|
1057
|
+
readonly desc?: boolean;
|
|
1058
|
+
})[];
|
|
1059
|
+
readonly limit?: number;
|
|
1060
|
+
/** Accept a window not fully covered, or rejected operations. */
|
|
1061
|
+
readonly allowPartial?: boolean;
|
|
1062
|
+
/** Stages: measures per token, read in `where` as `"<stage>.<measure>"`. */
|
|
1063
|
+
readonly with?: Readonly<Record<string, {
|
|
1064
|
+
readonly where?: ComputeFilter;
|
|
1065
|
+
readonly measures: Readonly<Record<string, ComputeMeasure>>;
|
|
1066
|
+
readonly having?: ComputeFilter;
|
|
1067
|
+
}>>;
|
|
1068
|
+
/**
|
|
1069
|
+
* One row per anchor: the first trade, in its token's sequence, of each
|
|
1070
|
+
* group of `by` (with `token`) that passes `where`. Measures then read it
|
|
1071
|
+
* (`anchor`) and its token's trades around it (`asOf`, `window`).
|
|
1072
|
+
*/
|
|
1073
|
+
readonly anchors?: {
|
|
1074
|
+
readonly by: readonly ComputeKey[];
|
|
1075
|
+
readonly where?: ComputeFilter;
|
|
1076
|
+
readonly pick?: "first";
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
/** A column of a result: addresses and names, numbers, or exact integers. */
|
|
1080
|
+
export type ComputeColumn = readonly (string | null)[] | Float64Array | readonly (bigint | null)[];
|
|
1081
|
+
export interface ComputeStats {
|
|
1082
|
+
readonly hours: number;
|
|
1083
|
+
readonly derivedHours: number;
|
|
1084
|
+
readonly sealedHours: number;
|
|
1085
|
+
readonly openHours: number;
|
|
1086
|
+
readonly passes: number;
|
|
1087
|
+
readonly trades: number;
|
|
1088
|
+
readonly groups: number;
|
|
1089
|
+
/** Server time of the call, every thread (the history reading; not your CPU). */
|
|
1090
|
+
readonly cpuMs: number;
|
|
1091
|
+
readonly wallMs: number;
|
|
1092
|
+
}
|
|
1093
|
+
interface ComputeSummary {
|
|
1094
|
+
readonly length: number;
|
|
1095
|
+
/** Key columns, in `groupBy` order. */
|
|
1096
|
+
readonly keys: readonly string[];
|
|
1097
|
+
/** Measure columns, in name order. */
|
|
1098
|
+
readonly measures: readonly string[];
|
|
1099
|
+
readonly coverage: Json;
|
|
1100
|
+
/** Covered, and no rejected operation counted. */
|
|
1101
|
+
readonly complete: boolean;
|
|
1102
|
+
/** Operations of the tokens read that normalization rejects, in the range. */
|
|
1103
|
+
readonly skipped: number;
|
|
1104
|
+
readonly stats: ComputeStats;
|
|
1105
|
+
}
|
|
1106
|
+
export interface ComputeResult extends ComputeSummary {
|
|
1107
|
+
/** One column per key and measure, `length` values each. */
|
|
1108
|
+
readonly columns: Readonly<Record<string, ComputeColumn>>;
|
|
1109
|
+
}
|
|
1110
|
+
/** A result written to storage (`output`): JSON lines, one object per row. */
|
|
1111
|
+
export interface ComputeOutput extends ComputeSummary {
|
|
1112
|
+
readonly output: {
|
|
1113
|
+
readonly area: "short" | "long" | "durable";
|
|
1114
|
+
readonly key: string;
|
|
1115
|
+
readonly rows: number;
|
|
1116
|
+
readonly bytes: number;
|
|
1117
|
+
readonly etag: string;
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
/** The rows of a result as objects (keys and measures by name). */
|
|
1121
|
+
export declare function computeRows(r: ComputeResult): Record<string, string | number | bigint | null>[];
|
|
933
1122
|
export interface Context {
|
|
934
1123
|
readonly execution: ExecutionInfo;
|
|
935
1124
|
readonly data: {
|
|
@@ -943,10 +1132,13 @@ export interface Context {
|
|
|
943
1132
|
/**
|
|
944
1133
|
* Volume of a token since its creation (`cumulativeRaw`, `buyRaw`,
|
|
945
1134
|
* `sellRaw`, `buys`, `sells`, `sinceCreation.complete`, `coverage`), its
|
|
946
|
-
* `creation` and `metadata`, as JSON.
|
|
1135
|
+
* `creation` and `metadata`, as JSON. Amounts are in `quoteMint` at
|
|
1136
|
+
* `quoteDecimals`; without `quote`: SOL; `quote: "auto"`: SOL unless the
|
|
1137
|
+
* token trades in other quotes only (then its most traded one; `quotes`
|
|
1138
|
+
* lists them all).
|
|
947
1139
|
*/
|
|
948
1140
|
tokenTotals(mint: Address, options?: {
|
|
949
|
-
quote?: Address;
|
|
1141
|
+
quote?: Address | "auto";
|
|
950
1142
|
}): Promise<Json>;
|
|
951
1143
|
/**
|
|
952
1144
|
* Latest known metadata and the creation, at most two blocks read (no
|
|
@@ -990,6 +1182,22 @@ export interface Context {
|
|
|
990
1182
|
* after the last write), `long` (500 MiB, 7 days after the last write) and
|
|
991
1183
|
* `durable` (50 MiB, never expires).
|
|
992
1184
|
*/
|
|
1185
|
+
/**
|
|
1186
|
+
* Runs a plan (or up to 8 plans over one read) natively over the
|
|
1187
|
+
* history: answers columns, or with `output` writes the rows to storage
|
|
1188
|
+
* as JSON lines and answers a summary. Errors: `invalid_plan`,
|
|
1189
|
+
* `invalid_window`, `partial_coverage`, `compute_result_limit`,
|
|
1190
|
+
* `compute_memory_limit`, `compute_limit`, `compute_overflow`, and those of
|
|
1191
|
+
* history reads.
|
|
1192
|
+
*/
|
|
1193
|
+
compute(plan: ComputePlan): Promise<ComputeResult>;
|
|
1194
|
+
compute(plans: readonly ComputePlan[]): Promise<ComputeResult[]>;
|
|
1195
|
+
compute(plan: ComputePlan, options: {
|
|
1196
|
+
readonly output: {
|
|
1197
|
+
readonly area: "short" | "long" | "durable";
|
|
1198
|
+
readonly key: string;
|
|
1199
|
+
};
|
|
1200
|
+
}): Promise<ComputeOutput>;
|
|
993
1201
|
readonly storage: {
|
|
994
1202
|
readonly short: StorageArea;
|
|
995
1203
|
readonly long: StorageArea;
|
package/dist/sdk.js
CHANGED
|
@@ -73,6 +73,18 @@ export function isCompletion(args) {
|
|
|
73
73
|
args.kind === "completion" &&
|
|
74
74
|
typeof args.executionId === "string");
|
|
75
75
|
}
|
|
76
|
+
export function computeRows(r) {
|
|
77
|
+
const names = [...r.keys, ...r.measures];
|
|
78
|
+
const out = [];
|
|
79
|
+
for (let i = 0; i < r.length; i++) {
|
|
80
|
+
const row = {};
|
|
81
|
+
for (const n of names)
|
|
82
|
+
row[n] =
|
|
83
|
+
r.columns[n][i] ?? null;
|
|
84
|
+
out.push(row);
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
76
88
|
export const SDK_MARKER = "__zyrkSdk";
|
|
77
89
|
export const SDK_VERSION = "0.2";
|
|
78
90
|
export function defineProgram(program) {
|