@johannc-dev/zyrk 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -2
- package/dist/build.d.ts +48 -0
- package/dist/build.js +338 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +295 -0
- package/dist/client.d.ts +208 -0
- package/dist/client.js +416 -0
- package/dist/contracts.d.ts +312 -0
- package/dist/contracts.js +154 -0
- package/dist/errors.d.ts +91 -0
- package/dist/errors.js +58 -0
- package/dist/export.d.ts +87 -0
- package/dist/export.js +338 -0
- package/dist/globals.d.ts +59 -0
- package/dist/hourfile.d.ts +57 -0
- package/dist/hourfile.js +338 -0
- package/dist/limits.d.ts +91 -0
- package/dist/limits.js +54 -0
- package/dist/local-data.d.ts +106 -0
- package/dist/local-data.js +345 -0
- package/dist/local-replay.d.ts +40 -0
- package/dist/local-replay.js +342 -0
- package/dist/local-storage.d.ts +76 -0
- package/dist/local-storage.js +499 -0
- package/dist/local.d.ts +235 -0
- package/dist/local.js +1133 -0
- package/dist/sandbox-bootstrap.js +519 -0
- package/dist/sdk.d.ts +1129 -0
- package/dist/sdk.js +208 -0
- package/dist/stream.d.ts +36 -0
- package/dist/stream.js +69 -0
- package/dist/webhook.d.ts +43 -0
- package/dist/webhook.js +54 -0
- package/package.json +56 -4
package/dist/cli.js
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Cause, Console, Effect, Exit, FileSystem, Layer, Option, Path, Stdio, Terminal, } from "effect";
|
|
3
|
+
import { Argument, CliError, Command, Flag } from "effect/cli";
|
|
4
|
+
import { ChildProcessSpawner } from "effect/process";
|
|
5
|
+
import { BUNDLE_PATH, writeBundleEffect } from "./build.js";
|
|
6
|
+
import { ZyrkClient } from "./client.js";
|
|
7
|
+
import { decode, Json } from "./contracts.js";
|
|
8
|
+
import { exportHistory, exportTotals, exportVolumeSearch, } from "./export.js";
|
|
9
|
+
import { LocalRunner } from "./local.js";
|
|
10
|
+
import { DecodeError, messageOf, } from "./errors.js";
|
|
11
|
+
export const VERSION = "0.2.0";
|
|
12
|
+
const print = (value) => Console.log(JSON.stringify(value, null, 2));
|
|
13
|
+
const entry = Argument.String("entry.ts").pipe(Argument.withDescription("TypeScript or JavaScript entry point of the program"));
|
|
14
|
+
const program = Argument.String("program").pipe(Argument.withDescription("Program name"));
|
|
15
|
+
const execution = Argument.String("execution").pipe(Argument.withDescription("Execution id"));
|
|
16
|
+
const fn = Argument.String("function").pipe(Argument.withDescription("Function to invoke"));
|
|
17
|
+
const args = Argument.String("json").pipe(Argument.withDescription("JSON arguments (default null)"), Argument.optional);
|
|
18
|
+
const minify = Flag.Boolean("minify").pipe(Flag.withDescription("Minify the bundle (smaller, harder to read in errors)"), Flag.withDefault(false));
|
|
19
|
+
const noTypecheck = Flag.Boolean("no-typecheck").pipe(Flag.withDescription("Skip type-checking (the static checks still run)"), Flag.withDefault(false));
|
|
20
|
+
const timeout = Flag.Int("timeout").pipe(Flag.withDescription("Stop waiting after this many milliseconds"), Flag.optional);
|
|
21
|
+
const parseArgs = (raw) => Effect.try({
|
|
22
|
+
try: () => JSON.parse(Option.getOrElse(raw, () => "null")),
|
|
23
|
+
catch: (cause) => new DecodeError({ what: "arguments", message: messageOf(cause) }),
|
|
24
|
+
}).pipe(Effect.flatMap(decode(Json, "arguments")));
|
|
25
|
+
const bundleWith = (input) => writeBundleEffect(input.entry, {
|
|
26
|
+
minify: input.minify,
|
|
27
|
+
typecheck: !input["no-typecheck"],
|
|
28
|
+
}).pipe(Effect.tap((result) => Effect.forEach(result.warnings, (w) => Console.error(`warning: ${w}`))));
|
|
29
|
+
const start = Effect.fnUntraced(function* (input) {
|
|
30
|
+
const client = yield* ZyrkClient;
|
|
31
|
+
const id = yield* client.start(input.program, {
|
|
32
|
+
function: input.function,
|
|
33
|
+
args: yield* parseArgs(input.json),
|
|
34
|
+
});
|
|
35
|
+
yield* Console.error(JSON.stringify({ executionId: id }));
|
|
36
|
+
return { client, id };
|
|
37
|
+
});
|
|
38
|
+
const withClient = (f) => ZyrkClient.use(f).pipe(Effect.flatMap(print), Effect.provide(ZyrkClient.layerConfig));
|
|
39
|
+
const onArgument = (name, description, argument, f) => Command.make(name, { value: argument }, (input) => withClient((client) => f(client, input.value))).pipe(Command.withDescription(description));
|
|
40
|
+
const timeoutOf = (value) => Option.match(value, {
|
|
41
|
+
onNone: () => ({}),
|
|
42
|
+
onSome: (timeoutMs) => ({ timeoutMs }),
|
|
43
|
+
});
|
|
44
|
+
export function timeOf(text) {
|
|
45
|
+
const ms = /^\d+$/.test(text) ? Number(text) : Date.parse(text);
|
|
46
|
+
if (!Number.isFinite(ms))
|
|
47
|
+
throw new Error(`Not a date: ${text}`);
|
|
48
|
+
return ms;
|
|
49
|
+
}
|
|
50
|
+
const local = (name, body) => Effect.promise(() => body().catch((error) => {
|
|
51
|
+
process.stderr.write(`error[${name}]: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
}));
|
|
54
|
+
const optionalString = (name, description) => Flag.String(name).pipe(Flag.withDescription(description), Flag.optional);
|
|
55
|
+
const flag = (name, description) => Flag.Boolean(name).pipe(Flag.withDescription(description), Flag.withDefault(false));
|
|
56
|
+
const value = (o) => Option.getOrUndefined(o);
|
|
57
|
+
const exportCommand = Command.make("export", {
|
|
58
|
+
token: optionalString("token", "Mint whose trades to export"),
|
|
59
|
+
wallet: optionalString("wallet", "Wallet whose trades to export"),
|
|
60
|
+
since: optionalString("since", "Start (included): 2026-11-07, ISO time or ms"),
|
|
61
|
+
until: optionalString("until", "End (excluded): 2026-11-08, ISO time or ms"),
|
|
62
|
+
side: optionalString("side", "buy or sell (default: both)"),
|
|
63
|
+
"with-signatures": flag("with-signatures", "Export batch.signatures"),
|
|
64
|
+
"with-market": flag("with-market", "Export batch.market (reserves, fees)"),
|
|
65
|
+
"with-events": flag("with-events", "Export pool events too (implies --with-market)"),
|
|
66
|
+
"with-order": flag("with-order", "Export batch.order (on-chain order)"),
|
|
67
|
+
"with-tx": flag("with-tx", "Export batch.tx (transaction of each trade, place in it)"),
|
|
68
|
+
refresh: flag("refresh", "Download every hour again (a finished export only re-downloads the hours that were open or not sealed)"),
|
|
69
|
+
totals: optionalString("totals", "Also save /v1/tokens/{mint}/totals of this mint"),
|
|
70
|
+
quote: optionalString("quote", "Quote mint of --totals"),
|
|
71
|
+
"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),
|
|
73
|
+
out: Flag.String("out").pipe(Flag.withAlias("o"), Flag.withDescription("Data directory (created, resumed if it exists)"), Flag.withDefault("data")),
|
|
74
|
+
}, (input) => local("export", async () => {
|
|
75
|
+
const url = process.env.TH_API_URL;
|
|
76
|
+
const apiKey = process.env.TH_API_KEY;
|
|
77
|
+
if (!url || !apiKey)
|
|
78
|
+
throw new Error("Set TH_API_URL and TH_API_KEY");
|
|
79
|
+
const connection = {
|
|
80
|
+
url,
|
|
81
|
+
apiKey,
|
|
82
|
+
...(Option.isSome(input.rpm) ? { rpm: input.rpm.value } : {}),
|
|
83
|
+
onWarning: (m) => process.stderr.write(`warning: ${m}\n`),
|
|
84
|
+
onProgress: (p) => process.stderr.write(p.done
|
|
85
|
+
? `${p.read}: done, ${p.hours} hour files, ${p.bytes} bytes\n`
|
|
86
|
+
: `${p.read}: hour ${p.hour} (${p.hours})\r`),
|
|
87
|
+
};
|
|
88
|
+
const token = value(input.token);
|
|
89
|
+
const wallet = value(input.wallet);
|
|
90
|
+
let work = false;
|
|
91
|
+
if (token || wallet) {
|
|
92
|
+
if (token && wallet)
|
|
93
|
+
throw new Error("--token or --wallet, not both");
|
|
94
|
+
const since = value(input.since);
|
|
95
|
+
const until = value(input.until);
|
|
96
|
+
if (!since || !until)
|
|
97
|
+
throw new Error("--since and --until are required");
|
|
98
|
+
const side = value(input.side);
|
|
99
|
+
if (side !== undefined && side !== "buy" && side !== "sell")
|
|
100
|
+
throw new Error("--side is buy or sell");
|
|
101
|
+
const summary = await exportHistory(connection, input.out, {
|
|
102
|
+
scope: token ? "token" : "wallet",
|
|
103
|
+
key: (token ?? wallet),
|
|
104
|
+
sinceMs: timeOf(since),
|
|
105
|
+
untilMs: timeOf(until),
|
|
106
|
+
...(side ? { side } : {}),
|
|
107
|
+
withSignatures: input["with-signatures"],
|
|
108
|
+
withMarket: input["with-market"],
|
|
109
|
+
withEvents: input["with-events"],
|
|
110
|
+
withOrder: input["with-order"],
|
|
111
|
+
withTx: input["with-tx"],
|
|
112
|
+
refresh: input.refresh,
|
|
113
|
+
});
|
|
114
|
+
process.stderr.write(`${summary.reads.length} part(s), ${summary.requests} requests, ${summary.bytes} bytes written to ${input.out}\n`);
|
|
115
|
+
work = true;
|
|
116
|
+
}
|
|
117
|
+
const totals = value(input.totals);
|
|
118
|
+
if (totals) {
|
|
119
|
+
await exportTotals(connection, input.out, totals, value(input.quote) ?? null);
|
|
120
|
+
work = true;
|
|
121
|
+
}
|
|
122
|
+
const volume = value(input["volume-search"]);
|
|
123
|
+
if (volume) {
|
|
124
|
+
const pages = await exportVolumeSearch(connection, input.out, JSON.parse(volume));
|
|
125
|
+
process.stderr.write(`volumeSearch: ${pages} page(s)\n`);
|
|
126
|
+
work = true;
|
|
127
|
+
}
|
|
128
|
+
if (!work)
|
|
129
|
+
throw new Error("Nothing to export: --token/--wallet, --totals or --volume-search");
|
|
130
|
+
})).pipe(Command.withDescription("Download history (one file per hour, as hosted programs see it) for zyrk dev. Resumes an interrupted export."));
|
|
131
|
+
const devCommand = Command.make("dev", {
|
|
132
|
+
entry,
|
|
133
|
+
function: fn,
|
|
134
|
+
json: args,
|
|
135
|
+
data: optionalString("data", "Directory written by zyrk export"),
|
|
136
|
+
state: optionalString("state", "Keep the program state in this folder between runs"),
|
|
137
|
+
storage: optionalString("storage", "Keep ctx.storage in this folder (default: memory)"),
|
|
138
|
+
bundle: flag("bundle", "entry is a built bundle (.functions/program.js)"),
|
|
139
|
+
replay: optionalString("replay", "Replay this subscription over --since/--until: <function> is ignored, json is unused"),
|
|
140
|
+
since: optionalString("since", "Replay start"),
|
|
141
|
+
until: optionalString("until", "Replay end"),
|
|
142
|
+
"events-per-delivery": Flag.Int("events-per-delivery").pipe(Flag.withDescription("Events per replayed delivery (1 to 32, default 1)"), Flag.optional),
|
|
143
|
+
quiet: flag("quiet", "Do not print logs and emits as they happen"),
|
|
144
|
+
"no-typecheck": noTypecheck,
|
|
145
|
+
}, (input) => local("dev", async () => {
|
|
146
|
+
const source = input.bundle
|
|
147
|
+
? { code: (await import("node:fs")).readFileSync(input.entry, "utf8") }
|
|
148
|
+
: input.entry;
|
|
149
|
+
const runner = await LocalRunner.load(source, {
|
|
150
|
+
...(Option.isSome(input.data) ? { data: input.data.value } : {}),
|
|
151
|
+
...(Option.isSome(input.state) ? { state: input.state.value } : {}),
|
|
152
|
+
...(Option.isSome(input.storage)
|
|
153
|
+
? { storage: input.storage.value }
|
|
154
|
+
: {}),
|
|
155
|
+
console: !input.quiet,
|
|
156
|
+
typecheck: !input["no-typecheck"],
|
|
157
|
+
});
|
|
158
|
+
const replay = value(input.replay);
|
|
159
|
+
if (replay) {
|
|
160
|
+
const since = value(input.since);
|
|
161
|
+
const until = value(input.until);
|
|
162
|
+
if (!since || !until)
|
|
163
|
+
throw new Error("--replay needs --since and --until");
|
|
164
|
+
const r = await runner.replay({
|
|
165
|
+
subscription: replay,
|
|
166
|
+
sinceMs: timeOf(since),
|
|
167
|
+
untilMs: timeOf(until),
|
|
168
|
+
...(Option.isSome(input["events-per-delivery"])
|
|
169
|
+
? { eventsPerDelivery: input["events-per-delivery"].value }
|
|
170
|
+
: {}),
|
|
171
|
+
});
|
|
172
|
+
for (const w of [...r.warnings, ...runner.warnings])
|
|
173
|
+
process.stderr.write(`warning: ${w}\n`);
|
|
174
|
+
console.log(JSON.stringify({
|
|
175
|
+
id: r.id,
|
|
176
|
+
status: r.status,
|
|
177
|
+
...(r.error ? { error: r.error } : {}),
|
|
178
|
+
progress: r.progress,
|
|
179
|
+
deliveries: r.runs.map((x) => ({
|
|
180
|
+
id: x.id,
|
|
181
|
+
status: x.status,
|
|
182
|
+
...(x.error ? { error: x.error } : { result: x.result }),
|
|
183
|
+
})),
|
|
184
|
+
state: Object.fromEntries(r.state),
|
|
185
|
+
}, null, 2));
|
|
186
|
+
if (r.status !== "succeeded")
|
|
187
|
+
process.exitCode = 1;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const parsed = JSON.parse(Option.getOrElse(input.json, () => "null"));
|
|
191
|
+
const run = await runner.run(input.function, parsed);
|
|
192
|
+
await runner.idle();
|
|
193
|
+
for (const w of runner.warnings)
|
|
194
|
+
process.stderr.write(`warning: ${w}\n`);
|
|
195
|
+
const last = run.executions[run.executions.length - 1];
|
|
196
|
+
if (last)
|
|
197
|
+
process.stderr.write(`usage: ${JSON.stringify(last.usage)}\n`);
|
|
198
|
+
if (run.status === "succeeded")
|
|
199
|
+
console.log(JSON.stringify(run.result, null, 2));
|
|
200
|
+
else {
|
|
201
|
+
process.stderr.write(`error[ExecutionFailed ${run.error?.kind}/${run.error?.code}]: ${run.error?.message}\n`);
|
|
202
|
+
process.exitCode = 1;
|
|
203
|
+
}
|
|
204
|
+
})).pipe(Command.withDescription("Run a function on your machine with the same ctx as the service, on data from zyrk export"));
|
|
205
|
+
const webhook = Command.make("webhook").pipe(Command.withDescription("Outbound webhook of the account: configuration, test, delivery log"), Command.withSubcommands([
|
|
206
|
+
Command.make("get", {}, () => withClient((c) => c.webhook())).pipe(Command.withDescription("Show the configuration (secrets masked)")),
|
|
207
|
+
Command.make("set", {
|
|
208
|
+
url: Argument.String("url").pipe(Argument.withDescription("https:// URL of your receiver")),
|
|
209
|
+
events: Flag.String("events").pipe(Flag.withDescription("Automatic alerts, comma separated: failed, limit, cpu_limit…"), Flag.withDefault("")),
|
|
210
|
+
disabled: Flag.Boolean("disabled").pipe(Flag.withDescription("Keep the configuration but deliver nothing"), Flag.withDefault(false)),
|
|
211
|
+
}, (input) => withClient((c) => c.setWebhook({
|
|
212
|
+
url: input.url,
|
|
213
|
+
events: input.events
|
|
214
|
+
.split(",")
|
|
215
|
+
.map((e) => e.trim())
|
|
216
|
+
.filter((e) => e !== ""),
|
|
217
|
+
enabled: !input.disabled,
|
|
218
|
+
}))).pipe(Command.withDescription("Set the URL and alerts; the first time prints the secret (keep it)")),
|
|
219
|
+
Command.make("rotate", {
|
|
220
|
+
force: Flag.Boolean("force").pipe(Flag.withDescription("Rotate again although the previous secret is still valid (it stops working now)"), Flag.withDefault(false)),
|
|
221
|
+
}, (input) => withClient((c) => c.rotateWebhookSecret({ force: input.force }))).pipe(Command.withDescription("New secret (printed); the previous one stays valid for 24 h")),
|
|
222
|
+
Command.make("test", {}, () => withClient((c) => c.testWebhook())).pipe(Command.withDescription("Send a test message")),
|
|
223
|
+
Command.make("deliveries", {
|
|
224
|
+
limit: Flag.Int("limit").pipe(Flag.withDescription("Attempts to show (1 to 100)"), Flag.optional),
|
|
225
|
+
}, (input) => withClient((c) => c.webhookDeliveries(Option.getOrUndefined(input.limit)))).pipe(Command.withDescription("Last delivery attempts (status, latency, error) and pending count")),
|
|
226
|
+
Command.make("delete", {}, () => withClient((c) => c.deleteWebhook())).pipe(Command.withDescription("Remove the webhook and its queued messages")),
|
|
227
|
+
]));
|
|
228
|
+
const zyrk = Command.make("zyrk").pipe(Command.withDescription("Zyrk hosted functions (SDK 0.2). Environment: TH_API_URL, TH_API_KEY. Install npm dependencies locally; the service receives only the bundle."), Command.withSubcommands([
|
|
229
|
+
Command.make("build", { entry, minify, "no-typecheck": noTypecheck }, (input) => bundleWith(input).pipe(Effect.flatMap((r) => Console.log(`${BUNDLE_PATH} (${r.bytes} bytes)`)))).pipe(Command.withDescription(`Check and bundle into ${BUNDLE_PATH}. Refuses Node modules, fetch and the 0.1 API.`)),
|
|
230
|
+
Command.make("deploy", {
|
|
231
|
+
program,
|
|
232
|
+
entry,
|
|
233
|
+
minify,
|
|
234
|
+
"no-typecheck": noTypecheck,
|
|
235
|
+
force: Flag.Boolean("force").pipe(Flag.withDescription("Deploy even if executions in flight are incompatible (they are cancelled)"), Flag.withDefault(false)),
|
|
236
|
+
}, (input) => Effect.gen(function* () {
|
|
237
|
+
const { code } = yield* bundleWith(input);
|
|
238
|
+
const client = yield* ZyrkClient;
|
|
239
|
+
yield* print(yield* client.deploy(input.program, code, { force: input.force }));
|
|
240
|
+
}).pipe(Effect.provide(ZyrkClient.layerConfig))).pipe(Command.withDescription("Build and deploy a program")),
|
|
241
|
+
Command.make("run", { program, function: fn, json: args, timeout }, (input) => start(input).pipe(Effect.flatMap(({ client, id }) => client.wait(id, timeoutOf(input.timeout))), Effect.flatMap(print), Effect.provide(ZyrkClient.layerConfig))).pipe(Command.withDescription("Start a function and wait for its result")),
|
|
242
|
+
Command.make("start", { program, function: fn, json: args }, (input) => start(input).pipe(Effect.flatMap(({ id }) => print({ id })), Effect.provide(ZyrkClient.layerConfig))).pipe(Command.withDescription("Start a function without waiting")),
|
|
243
|
+
Command.make("wait", { execution, timeout }, (input) => withClient((c) => c.wait(input.execution, timeoutOf(input.timeout)))).pipe(Command.withDescription("Wait for the result of an execution")),
|
|
244
|
+
onArgument("result", "Status and result of an execution (continuations followed)", execution, (c, id) => c.result(id)),
|
|
245
|
+
onArgument("execution", "Full record of one execution (logs, usage)", execution, (c, id) => c.execution(id)),
|
|
246
|
+
onArgument("ack", "Acknowledge an execution", execution, (c, id) => c.acknowledge(id)),
|
|
247
|
+
onArgument("cancel", "Cancel an execution", execution, (c, id) => c.cancel(id)),
|
|
248
|
+
Command.make("programs", {}, () => withClient((c) => c.programs())).pipe(Command.withDescription("List the programs of the account")),
|
|
249
|
+
onArgument("inspect", "Show a program", program, (c, p) => c.inspect(p)),
|
|
250
|
+
onArgument("executions", "Recent executions of a program", program, (c, p) => c.executions(p)),
|
|
251
|
+
onArgument("pause", "Pause a program", program, (c, p) => c.pause(p, true)),
|
|
252
|
+
onArgument("resume", "Resume a program", program, (c, p) => c.pause(p, false)),
|
|
253
|
+
exportCommand,
|
|
254
|
+
devCommand,
|
|
255
|
+
Command.make("measurements", {}, () => withClient((c) => c.measurements())).pipe(Command.withDescription("Daily usage of the account")),
|
|
256
|
+
webhook,
|
|
257
|
+
]));
|
|
258
|
+
const describe = (error) => {
|
|
259
|
+
switch (error._tag) {
|
|
260
|
+
case "ApiError":
|
|
261
|
+
return `error[ApiError ${error.status} ${error.code}]: ${error.message}`;
|
|
262
|
+
case "BuildError":
|
|
263
|
+
return `error[BuildError ${error.stage}]:\n${error.message}`;
|
|
264
|
+
case "DecodeError":
|
|
265
|
+
return `error[DecodeError ${error.what}]: ${error.message}`;
|
|
266
|
+
case "ExecutionFailed":
|
|
267
|
+
return `error[ExecutionFailed ${error.failure.kind}/${error.failure.code}]: ${error.message}`;
|
|
268
|
+
case "ExecutionEnded":
|
|
269
|
+
return `error[ExecutionEnded ${error.status}]: ${error.message}`;
|
|
270
|
+
case "WaitTimeout":
|
|
271
|
+
case "ConfigError":
|
|
272
|
+
case "NetworkError":
|
|
273
|
+
return `error[${error._tag}]: ${error.message}`;
|
|
274
|
+
default:
|
|
275
|
+
return error;
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
const CliEnvironment = Layer.mergeAll(FileSystem.layerNoop({}), Path.layer, Stdio.layerTest({}), Layer.succeed(Terminal.Terminal, Terminal.make({
|
|
279
|
+
columns: Effect.sync(() => process.stdout.columns ?? 80),
|
|
280
|
+
rows: Effect.sync(() => process.stdout.rows ?? 24),
|
|
281
|
+
size: Effect.die("unsupported"),
|
|
282
|
+
readInput: Effect.die("unsupported"),
|
|
283
|
+
readLine: Effect.die("unsupported"),
|
|
284
|
+
display: (text) => Effect.sync(() => void process.stdout.write(text)),
|
|
285
|
+
})), Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make(() => Effect.die("unsupported"))));
|
|
286
|
+
Effect.runPromiseExit(Command.runWith(zyrk, { version: VERSION })(process.argv.slice(2)).pipe(Effect.provide(CliEnvironment))).then((exit) => {
|
|
287
|
+
if (Exit.isSuccess(exit))
|
|
288
|
+
return;
|
|
289
|
+
const error = Cause.findErrorOption(exit.cause);
|
|
290
|
+
if (Option.isNone(error))
|
|
291
|
+
process.stderr.write(Cause.pretty(exit.cause) + "\n");
|
|
292
|
+
else if (!CliError.isCliError(error.value))
|
|
293
|
+
process.stderr.write(describe(error.value) + "\n");
|
|
294
|
+
process.exitCode = 1;
|
|
295
|
+
});
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect";
|
|
2
|
+
import { Job, Json, ProgramSummary, type JobResult } from "./contracts.js";
|
|
3
|
+
import { ApiError, ConfigError, DecodeError, ExecutionEnded, ExecutionFailed, NetworkError, WaitTimeout } from "./errors.js";
|
|
4
|
+
export * from "./errors.js";
|
|
5
|
+
export type { Job, JobResult, ProgramSummary } from "./contracts.js";
|
|
6
|
+
import type { ReplayFeedItem, ReplayRecord, ReplayRequest } from "./sdk.js";
|
|
7
|
+
export type { ReplayFeedItem, ReplayRecord, ReplayRequest } from "./sdk.js";
|
|
8
|
+
export interface ClientConfig {
|
|
9
|
+
/** Gateway URL, e.g. `https://api.example.com`. */
|
|
10
|
+
readonly url: string;
|
|
11
|
+
readonly apiKey: string;
|
|
12
|
+
}
|
|
13
|
+
export interface StartInput {
|
|
14
|
+
readonly function: string;
|
|
15
|
+
readonly args: Json;
|
|
16
|
+
readonly onComplete?: string;
|
|
17
|
+
/** Retry key: the same id within 24 h returns the same execution. Default: random. */
|
|
18
|
+
readonly requestId?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface WaitOptions {
|
|
21
|
+
/** Delay between two status checks, in milliseconds (default 1000). */
|
|
22
|
+
readonly pollMs?: number;
|
|
23
|
+
/** Give up after this many milliseconds (default: never). The execution keeps running. */
|
|
24
|
+
readonly timeoutMs?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface DeployOptions {
|
|
27
|
+
/** Deploy even if incompatible with executions in flight (they are cancelled). */
|
|
28
|
+
readonly force?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** Input of {@link ZyrkClientService.run}: a start without `onComplete`. */
|
|
31
|
+
export type RunInput = Omit<StartInput, "onComplete">;
|
|
32
|
+
export interface AbortOptions {
|
|
33
|
+
readonly signal?: AbortSignal;
|
|
34
|
+
}
|
|
35
|
+
/** Errors of a single HTTP call to the service. */
|
|
36
|
+
export type RequestError = NetworkError | ApiError | DecodeError;
|
|
37
|
+
/** Errors of waiting for one execution result. */
|
|
38
|
+
export type WaitError = RequestError | ExecutionFailed | ExecutionEnded | WaitTimeout;
|
|
39
|
+
export interface ZyrkClientService {
|
|
40
|
+
request(method: string, path: string, body?: unknown): Effect.Effect<Json, RequestError>;
|
|
41
|
+
deploy(program: string, code: string, options?: DeployOptions): Effect.Effect<Json, RequestError>;
|
|
42
|
+
programs(): Effect.Effect<readonly ProgramSummary[], RequestError>;
|
|
43
|
+
inspect(program: string): Effect.Effect<Json, RequestError>;
|
|
44
|
+
pause(program: string, paused?: boolean): Effect.Effect<Json, RequestError>;
|
|
45
|
+
/** Recent executions of a program (summaries). */
|
|
46
|
+
executions(program: string): Effect.Effect<Json, RequestError>;
|
|
47
|
+
/** Starts an execution and returns its id. */
|
|
48
|
+
start(program: string, input: StartInput): Effect.Effect<string, RequestError>;
|
|
49
|
+
/** The raw execution record (continuations not followed). */
|
|
50
|
+
execution(id: string): Effect.Effect<Job, RequestError>;
|
|
51
|
+
/** Where the job stands now, continuations followed (same shape as `ctx.jobs.result`). */
|
|
52
|
+
result(id: string): Effect.Effect<JobResult, RequestError>;
|
|
53
|
+
/** Polls until the job succeeds and returns its result; fails otherwise. */
|
|
54
|
+
wait(id: string, options?: WaitOptions): Effect.Effect<Json, WaitError>;
|
|
55
|
+
/**
|
|
56
|
+
* Polls until every job is final or `timeoutMs` elapses (same contract as
|
|
57
|
+
* `ctx.jobs.wait`): never fails because a job failed.
|
|
58
|
+
*/
|
|
59
|
+
waitAll(ids: readonly string[], options?: WaitOptions): Effect.Effect<JobResult[], RequestError>;
|
|
60
|
+
run(program: string, input: RunInput, options?: WaitOptions): Effect.Effect<Json, WaitError>;
|
|
61
|
+
acknowledge(id: string): Effect.Effect<Json, RequestError>;
|
|
62
|
+
cancel(id: string): Effect.Effect<Json, RequestError>;
|
|
63
|
+
/** Buffered account events after `afterSeq` (the WebSocket stream is in `./stream`). */
|
|
64
|
+
events(afterSeq?: number): Effect.Effect<Json, RequestError>;
|
|
65
|
+
/** Daily usage of the account. */
|
|
66
|
+
measurements(): Effect.Effect<Json, RequestError>;
|
|
67
|
+
/** The webhook configuration (secrets masked) and its queue counts. */
|
|
68
|
+
webhook(): Effect.Effect<Json, RequestError>;
|
|
69
|
+
/** Sets the webhook; the first call returns the secret in clear (keep it). */
|
|
70
|
+
setWebhook(config: WebhookConfig): Effect.Effect<Json, RequestError>;
|
|
71
|
+
deleteWebhook(): Effect.Effect<Json, RequestError>;
|
|
72
|
+
/** New secret (returned in clear); the previous one also signs for 24 h. */
|
|
73
|
+
/** Refused (`rotation_in_progress`) while the previous secret is valid, unless `force`. */
|
|
74
|
+
rotateWebhookSecret(options?: RotateOptions): Effect.Effect<Json, RequestError>;
|
|
75
|
+
/** Queues a `test` message. */
|
|
76
|
+
testWebhook(): Effect.Effect<Json, RequestError>;
|
|
77
|
+
/** Last delivery attempts, pending count, failed messages. */
|
|
78
|
+
webhookDeliveries(limit?: number): Effect.Effect<Json, RequestError>;
|
|
79
|
+
}
|
|
80
|
+
export interface RotateOptions {
|
|
81
|
+
/** How long the previous secret keeps signing (default and maximum 24 h). */
|
|
82
|
+
keepPreviousMs?: number;
|
|
83
|
+
/** Rotate even though the previous secret is still valid (it ends now). */
|
|
84
|
+
force?: boolean;
|
|
85
|
+
}
|
|
86
|
+
export interface WebhookConfig {
|
|
87
|
+
/** `https://` only. */
|
|
88
|
+
url: string;
|
|
89
|
+
/** Automatic alerts: `failed`, `limit` or failure codes like `cpu_limit`. */
|
|
90
|
+
events?: readonly string[];
|
|
91
|
+
enabled?: boolean;
|
|
92
|
+
}
|
|
93
|
+
/** Builds the client service for a gateway URL and API key. */
|
|
94
|
+
export declare const make: (config: ClientConfig) => ZyrkClientService;
|
|
95
|
+
/** Reads `TH_API_URL` and `TH_API_KEY` from the environment. */
|
|
96
|
+
export declare const clientConfig: Effect.Effect<ClientConfig, ConfigError>;
|
|
97
|
+
declare const ZyrkClient_base: Context.ServiceClass<ZyrkClient, "@johannc-dev/zyrk/ZyrkClient", ZyrkClientService>;
|
|
98
|
+
/** The Zyrk functions HTTP client as an Effect service. */
|
|
99
|
+
export declare class ZyrkClient extends ZyrkClient_base {
|
|
100
|
+
static readonly layer: (config: ClientConfig) => Layer.Layer<ZyrkClient>;
|
|
101
|
+
/** Layer configured from `TH_API_URL` / `TH_API_KEY`. */
|
|
102
|
+
static readonly layerConfig: Layer.Layer<ZyrkClient, ConfigError>;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Promise façade over {@link ZyrkClient}. Methods reject with the tagged
|
|
106
|
+
* error classes of `./errors` (`ApiError`, `NetworkError`…). Every method
|
|
107
|
+
* takes an `AbortSignal`: in its options object when it has one, else as its
|
|
108
|
+
* last argument.
|
|
109
|
+
*/
|
|
110
|
+
export declare class FunctionsClient {
|
|
111
|
+
private readonly client;
|
|
112
|
+
private readonly config;
|
|
113
|
+
constructor(config: ClientConfig);
|
|
114
|
+
/**
|
|
115
|
+
* What functions stored in `ctx.storage.short` / `ctx.storage.long`, over
|
|
116
|
+
* HTTP (no WebSocket needed). Same shapes as in functions.
|
|
117
|
+
*/
|
|
118
|
+
storage(area: "short" | "long" | "durable"): HttpStorageArea;
|
|
119
|
+
request(method: string, path: string, body?: unknown, signal?: AbortSignal): Promise<Json>;
|
|
120
|
+
deploy(program: string, code: string, options?: DeployOptions & AbortOptions): Promise<Json>;
|
|
121
|
+
programs(signal?: AbortSignal): Promise<readonly ProgramSummary[]>;
|
|
122
|
+
inspect(program: string, signal?: AbortSignal): Promise<Json>;
|
|
123
|
+
pause(program: string, paused?: boolean, signal?: AbortSignal): Promise<Json>;
|
|
124
|
+
executions(program: string, signal?: AbortSignal): Promise<Json>;
|
|
125
|
+
start(program: string, input: StartInput, signal?: AbortSignal): Promise<string>;
|
|
126
|
+
execution(id: string, signal?: AbortSignal): Promise<Job>;
|
|
127
|
+
result(id: string, signal?: AbortSignal): Promise<JobResult>;
|
|
128
|
+
wait(id: string, options?: WaitOptions & AbortOptions): Promise<Json>;
|
|
129
|
+
waitAll(ids: readonly string[], options?: WaitOptions & AbortOptions): Promise<JobResult[]>;
|
|
130
|
+
run(program: string, input: RunInput, options?: WaitOptions & AbortOptions): Promise<Json>;
|
|
131
|
+
acknowledge(id: string, signal?: AbortSignal): Promise<Json>;
|
|
132
|
+
cancel(id: string, signal?: AbortSignal): Promise<Json>;
|
|
133
|
+
events(afterSeq?: number, signal?: AbortSignal): Promise<Json>;
|
|
134
|
+
measurements(signal?: AbortSignal): Promise<Json>;
|
|
135
|
+
webhook(signal?: AbortSignal): Promise<Json>;
|
|
136
|
+
setWebhook(config: WebhookConfig, signal?: AbortSignal): Promise<Json>;
|
|
137
|
+
deleteWebhook(signal?: AbortSignal): Promise<Json>;
|
|
138
|
+
rotateWebhookSecret(options?: RotateOptions, signal?: AbortSignal): Promise<Json>;
|
|
139
|
+
testWebhook(signal?: AbortSignal): Promise<Json>;
|
|
140
|
+
webhookDeliveries(limit?: number, signal?: AbortSignal): Promise<Json>;
|
|
141
|
+
/** Starts a history replay of a subscription (`POST programs/{p}/replay`). */
|
|
142
|
+
replay(program: string, request: ReplayRequest, signal?: AbortSignal): Promise<ReplayRecord>;
|
|
143
|
+
/** Status and progress of a replay. */
|
|
144
|
+
replayStatus(id: string, signal?: AbortSignal): Promise<ReplayRecord>;
|
|
145
|
+
cancelReplay(id: string, signal?: AbortSignal): Promise<ReplayRecord>;
|
|
146
|
+
/** Executions of a replay still queued or running (`ctx.jobs` mode); finished ones are in {@link replayEmits}. */
|
|
147
|
+
replayExecutions(id: string, after?: number, signal?: AbortSignal): Promise<Json>;
|
|
148
|
+
/**
|
|
149
|
+
* The replay's live feed (never on the account stream, never stored):
|
|
150
|
+
* its `ctx.emit` values and each execution's final record, in order,
|
|
151
|
+
* after `afterSeq` (200 at most). Reading frees everything up to
|
|
152
|
+
* `afterSeq`: pass the last `seq` received, while the replay runs (it
|
|
153
|
+
* pauses when 16 MiB wait, and fails after 2 min without a read).
|
|
154
|
+
*/
|
|
155
|
+
replayEmits(id: string, afterSeq?: number, signal?: AbortSignal): Promise<ReplayFeedItem[]>;
|
|
156
|
+
}
|
|
157
|
+
/** Lakebed's `StorageObjectHead` as the HTTP headers describe it. */
|
|
158
|
+
export interface HttpStorageHead {
|
|
159
|
+
readonly key: string;
|
|
160
|
+
readonly size: number;
|
|
161
|
+
readonly contentType: string;
|
|
162
|
+
readonly metadata: Readonly<Record<string, string>>;
|
|
163
|
+
readonly lastModified: string;
|
|
164
|
+
/** ISO 8601 (`x-expires-at`); `null` in `durable`. */
|
|
165
|
+
readonly expiresAt: string | null;
|
|
166
|
+
readonly etag: string;
|
|
167
|
+
}
|
|
168
|
+
export interface HttpStorageObject extends HttpStorageHead {
|
|
169
|
+
readonly body: Uint8Array;
|
|
170
|
+
}
|
|
171
|
+
/** `ctx.storage` of the account, seen from outside over HTTP. */
|
|
172
|
+
export interface HttpStorageArea {
|
|
173
|
+
/** `null` when absent or expired; `range` sends `Range: bytes=…`. */
|
|
174
|
+
get(key: string, options?: {
|
|
175
|
+
offset?: number;
|
|
176
|
+
length?: number;
|
|
177
|
+
signal?: AbortSignal;
|
|
178
|
+
}): Promise<HttpStorageObject | null>;
|
|
179
|
+
head(key: string, signal?: AbortSignal): Promise<HttpStorageHead | null>;
|
|
180
|
+
/** Uploads a dataset for functions to read (`ifAbsent`: `If-None-Match: *`). */
|
|
181
|
+
put(key: string, body: Uint8Array | ArrayBuffer | string | Blob, options?: {
|
|
182
|
+
contentType?: string;
|
|
183
|
+
metadata?: Record<string, string>;
|
|
184
|
+
ifAbsent?: boolean;
|
|
185
|
+
/** `If-Match`: only if the object still has this etag (412 `precondition_failed`). */
|
|
186
|
+
ifMatch?: string;
|
|
187
|
+
signal?: AbortSignal;
|
|
188
|
+
}): Promise<Json>;
|
|
189
|
+
delete(key: string, options?: {
|
|
190
|
+
ifMatch?: string;
|
|
191
|
+
signal?: AbortSignal;
|
|
192
|
+
}): Promise<void>;
|
|
193
|
+
list(prefix?: string, options?: {
|
|
194
|
+
cursor?: string | null;
|
|
195
|
+
limit?: number;
|
|
196
|
+
signal?: AbortSignal;
|
|
197
|
+
}): Promise<{
|
|
198
|
+
objects: Json[];
|
|
199
|
+
cursor: string | null;
|
|
200
|
+
}>;
|
|
201
|
+
usage(signal?: AbortSignal): Promise<Json>;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The path of a key: each `/`-separated part percent-encoded. Empty, `.`
|
|
205
|
+
* and `..` parts cannot travel in a URL, so such keys are refused here
|
|
206
|
+
* (they stay usable from functions).
|
|
207
|
+
*/
|
|
208
|
+
export declare const storageKeyPath: (key: string) => string;
|