abysshub 0.1.1-dev.2 → 0.1.1-dev.4
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 +13 -3
- package/dist/client.js +38 -11
- package/dist/error.d.ts +8 -0
- package/dist/error.js +7 -0
- package/dist/http.d.ts +3 -2
- package/dist/http.js +4 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -20,6 +20,14 @@ try {
|
|
|
20
20
|
|
|
21
21
|
`Abyss` is the package's default export, the form each Widget's page shows. The named form, `import { Abyss } from "abysshub"`, works too and is the same class.
|
|
22
22
|
|
|
23
|
+
From CommonJS, `require()` loads the same module on Node 20.19+ and 22.12+:
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
const { Abyss, file } = require("abysshub");
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
A bare `require("abysshub")` returns the module, not the class, so destructure it. On an older Node, `require()` fails with `ERR_REQUIRE_ESM`: use `const { Abyss, file } = await import("abysshub")` there, or upgrade Node.
|
|
30
|
+
|
|
23
31
|
`run()` presses the Widget and waits for the run to end, however long it takes, then returns the succeeded run. A refusal or a failed run raises `AbyssError`.
|
|
24
32
|
|
|
25
33
|
It runs on Node 20+, Bun and Deno, with no dependencies. An API key is a server-side secret, so in a browser `new Abyss()` throws.
|
|
@@ -111,6 +119,7 @@ A refusal, a failed run, a timeout and a lost connection all raise one class, `A
|
|
|
111
119
|
| `request_id` | quote it to support, else `null` |
|
|
112
120
|
| `shortfall` | with `insufficient_funds`: how much Byssium the wallet is short, else `null` |
|
|
113
121
|
| `price` | with `price_above_max`: what the run costs now, else `null` |
|
|
122
|
+
| `run_id` | the id of the run the call started or read, once the press's `Location` or a run body named it, else `null` |
|
|
114
123
|
| `run` | the run, for a failed run or a timeout, else `null` |
|
|
115
124
|
|
|
116
125
|
- **Refusals:** `invalid_api_key` (401), `not_found` (404), `invalid_request`, `invalid_input`, `held_input_*`, `input_unreachable` and `idempotency_key_reused` (422), `insufficient_funds` (402), `spend_cap_reached` and `price_above_max` (400), `idempotency_key_in_use` (409), `rate_limited` (429), `server_error` (500).
|
|
@@ -137,13 +146,14 @@ There is no timeout by default: `run()` waits as long as the run takes. `timeout
|
|
|
137
146
|
try {
|
|
138
147
|
const run = await abyss.run("widget_131", input, { timeout: 60, signal: AbortSignal.timeout(90_000) });
|
|
139
148
|
} catch (error) {
|
|
140
|
-
if (error instanceof AbyssError && error.code === "timeout") {
|
|
141
|
-
// The run goes on
|
|
149
|
+
if (error instanceof AbyssError && error.code === "timeout" && error.run_id) {
|
|
150
|
+
// The run goes on: wait for it again, without pressing (and paying) twice.
|
|
151
|
+
const run = await abyss.runs.get(error.run_id, { wait: true });
|
|
142
152
|
}
|
|
143
153
|
}
|
|
144
154
|
```
|
|
145
155
|
|
|
146
|
-
Either one raises `AbyssError` with `code: "timeout"
|
|
156
|
+
Either one raises `AbyssError` with `code: "timeout"`, `run`, the run as last seen or `null`, and `run_id`, the run's id. The press answers its `Location` at once, so `run_id` is set as soon as the press has answered, even while no run has been seen yet; it is `null` only when the waiting stopped before that. Nothing is sent after the waiting stops.
|
|
147
157
|
|
|
148
158
|
## Names
|
|
149
159
|
|
package/dist/client.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { AbyssError } from "./error.js";
|
|
2
2
|
import { uploadFiles } from "./files.js";
|
|
3
|
-
import { request } from "./http.js";
|
|
3
|
+
import { LIMITS, request } from "./http.js";
|
|
4
4
|
import { Run } from "./run.js";
|
|
5
5
|
import { VERSION } from "./version.js";
|
|
6
6
|
const API = "https://api.abysshub.com";
|
|
7
7
|
const DEV_API = "https://api.dev.abysshub.com";
|
|
8
8
|
const DEV_KEY_PREFIX = "abyss_sk_dev_";
|
|
9
|
+
/** A `Location` naming a run, `…/v1/runs/{id}`, with the id as its group. */
|
|
10
|
+
const RUN_LOCATION = /\/v1\/runs\/([^/?#]+)\/?(?:[?#]|$)/;
|
|
9
11
|
/** The pause between two re-attaches that both ended without the run's ending. */
|
|
10
12
|
const REATTACH_PAUSE_MS = 1_000;
|
|
11
13
|
/** The first pause before a retry after a drop or a 5xx; it doubles with each retry. */
|
|
@@ -67,6 +69,7 @@ export class Abyss {
|
|
|
67
69
|
code: run.error?.code ?? "platform_fault",
|
|
68
70
|
message: run.error?.message ?? "The run failed.",
|
|
69
71
|
request_id: reply.requestId,
|
|
72
|
+
run_id: run.id,
|
|
70
73
|
run,
|
|
71
74
|
});
|
|
72
75
|
}
|
|
@@ -79,7 +82,7 @@ export class Abyss {
|
|
|
79
82
|
}
|
|
80
83
|
/**
|
|
81
84
|
* Runs one call under its `timeout` and `signal`. When either stops the waiting, the
|
|
82
|
-
* call raises `AbyssError` with `code: "timeout"
|
|
85
|
+
* call raises `AbyssError` with `code: "timeout"`, the run as last seen and its id.
|
|
83
86
|
*/
|
|
84
87
|
async #call(options, work) {
|
|
85
88
|
const timeout = options.timeout ?? this.timeout;
|
|
@@ -95,14 +98,20 @@ export class Abyss {
|
|
|
95
98
|
if (options.signal?.aborted)
|
|
96
99
|
abort();
|
|
97
100
|
options.signal?.addEventListener("abort", abort, { once: true });
|
|
98
|
-
const call =
|
|
101
|
+
const call = newCall(stop.signal);
|
|
99
102
|
try {
|
|
100
103
|
return await work(call);
|
|
101
104
|
}
|
|
102
105
|
catch (error) {
|
|
103
106
|
if (!stop.signal.aborted)
|
|
104
107
|
throw error;
|
|
105
|
-
throw new AbyssError({
|
|
108
|
+
throw new AbyssError({
|
|
109
|
+
code: "timeout",
|
|
110
|
+
message,
|
|
111
|
+
request_id: call.requestId,
|
|
112
|
+
run_id: call.runId,
|
|
113
|
+
run: call.seen && this.#run(call.seen),
|
|
114
|
+
});
|
|
106
115
|
}
|
|
107
116
|
finally {
|
|
108
117
|
clearTimeout(timer);
|
|
@@ -187,7 +196,7 @@ export class Abyss {
|
|
|
187
196
|
return reply;
|
|
188
197
|
}
|
|
189
198
|
#run(data) {
|
|
190
|
-
return new Run(data, () => this.#getRun(
|
|
199
|
+
return new Run(data, () => this.#getRun(newCall(null), data.id, false));
|
|
191
200
|
}
|
|
192
201
|
#widgetURL(widget) {
|
|
193
202
|
return `${this.baseURL}/v1/widgets/${encodeURIComponent(widget)}`;
|
|
@@ -199,6 +208,7 @@ export class Abyss {
|
|
|
199
208
|
code: "unexpected_response",
|
|
200
209
|
message: "The press answered without a Location to read the run at.",
|
|
201
210
|
request_id: reply.requestId,
|
|
211
|
+
run_id: reply.runId,
|
|
202
212
|
});
|
|
203
213
|
}
|
|
204
214
|
const url = new URL(location, `${this.baseURL}/`);
|
|
@@ -219,7 +229,7 @@ export class Abyss {
|
|
|
219
229
|
for (let attempt = 0;; attempt++) {
|
|
220
230
|
let answer;
|
|
221
231
|
try {
|
|
222
|
-
answer = await request(url, init);
|
|
232
|
+
answer = await request(url, init, LIMITS, (headers) => heard(call, headers));
|
|
223
233
|
}
|
|
224
234
|
catch (error) {
|
|
225
235
|
if (call.signal?.aborted)
|
|
@@ -228,6 +238,7 @@ export class Abyss {
|
|
|
228
238
|
throw new AbyssError({
|
|
229
239
|
code: "connection_error",
|
|
230
240
|
message: "The connection dropped, or timed out, before the API answered.",
|
|
241
|
+
run_id: call.runId,
|
|
231
242
|
cause: error,
|
|
232
243
|
});
|
|
233
244
|
}
|
|
@@ -238,17 +249,31 @@ export class Abyss {
|
|
|
238
249
|
const parsed = parse(answer.text);
|
|
239
250
|
if (answer.status === 200 || answer.status === 202) {
|
|
240
251
|
const run = isRun(parsed) ? parsed : null;
|
|
241
|
-
if (run)
|
|
252
|
+
if (run) {
|
|
242
253
|
call.seen = run;
|
|
243
|
-
|
|
254
|
+
call.runId = run.id;
|
|
255
|
+
}
|
|
256
|
+
const location = answer.headers.get("location");
|
|
257
|
+
return { body: parsed, run, cut: answer.text === null, location, requestId, runId: call.runId };
|
|
244
258
|
}
|
|
245
259
|
const pause = retryPause(answer, attempt);
|
|
246
260
|
if (pause === null || attempt >= retries)
|
|
247
|
-
throw refusal(answer.status, parsed, requestId);
|
|
261
|
+
throw refusal(answer.status, parsed, requestId, call.runId);
|
|
248
262
|
await sleep(pause, call.signal);
|
|
249
263
|
}
|
|
250
264
|
}
|
|
251
265
|
}
|
|
266
|
+
/** A call's waiting before it has learned anything. */
|
|
267
|
+
function newCall(signal) {
|
|
268
|
+
return { signal, seen: null, runId: null, requestId: null };
|
|
269
|
+
}
|
|
270
|
+
/** Notes a response's headers as they arrive: its `Request-Id`, and the run its `Location` names. */
|
|
271
|
+
function heard(call, headers) {
|
|
272
|
+
call.requestId = headers.get("request-id") ?? call.requestId;
|
|
273
|
+
const runId = headers.get("location")?.match(RUN_LOCATION)?.[1];
|
|
274
|
+
if (runId)
|
|
275
|
+
call.runId = runId;
|
|
276
|
+
}
|
|
252
277
|
/** How long to wait before sending a refused request again, or null when it is not retried. */
|
|
253
278
|
function retryPause(answer, attempt) {
|
|
254
279
|
if (answer.status === 429 || answer.status === 409) {
|
|
@@ -265,9 +290,9 @@ function backoff(attempt) {
|
|
|
265
290
|
function expect(reply, is, message) {
|
|
266
291
|
if (is(reply.body))
|
|
267
292
|
return reply.body;
|
|
268
|
-
throw new AbyssError({ code: "unexpected_response", message, request_id: reply.requestId });
|
|
293
|
+
throw new AbyssError({ code: "unexpected_response", message, request_id: reply.requestId, run_id: reply.runId });
|
|
269
294
|
}
|
|
270
|
-
function refusal(status, body, requestId) {
|
|
295
|
+
function refusal(status, body, requestId, runId) {
|
|
271
296
|
const error = isObject(body) && isObject(body.error) ? body.error : null;
|
|
272
297
|
if (!error || typeof error.code !== "string") {
|
|
273
298
|
return new AbyssError({
|
|
@@ -275,6 +300,7 @@ function refusal(status, body, requestId) {
|
|
|
275
300
|
message: `The API answered ${status} without an error body.`,
|
|
276
301
|
status,
|
|
277
302
|
request_id: requestId,
|
|
303
|
+
run_id: runId,
|
|
278
304
|
});
|
|
279
305
|
}
|
|
280
306
|
return new AbyssError({
|
|
@@ -286,6 +312,7 @@ function refusal(status, body, requestId) {
|
|
|
286
312
|
shortfall: typeof error.shortfall === "number" ? error.shortfall : null,
|
|
287
313
|
price: typeof error.price === "number" ? error.price : null,
|
|
288
314
|
request_id: isObject(body) && typeof body.request_id === "string" ? body.request_id : requestId,
|
|
315
|
+
run_id: runId,
|
|
289
316
|
});
|
|
290
317
|
}
|
|
291
318
|
function parse(text) {
|
package/dist/error.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export interface AbyssErrorFields {
|
|
|
6
6
|
param?: string | null;
|
|
7
7
|
doc_url?: string | null;
|
|
8
8
|
request_id?: string | null;
|
|
9
|
+
/** The id of the run the call started or read, once the call has learned it. */
|
|
10
|
+
run_id?: string | null;
|
|
9
11
|
/** With `insufficient_funds`: how much Byssium the wallet is short. */
|
|
10
12
|
shortfall?: number | null;
|
|
11
13
|
/** With `price_above_max`: what the run costs now. */
|
|
@@ -25,6 +27,12 @@ export declare class AbyssError extends Error {
|
|
|
25
27
|
readonly param: string | null;
|
|
26
28
|
readonly doc_url: string | null;
|
|
27
29
|
readonly request_id: string | null;
|
|
30
|
+
/**
|
|
31
|
+
* The id of the run the call started or read, once the call has learned it from a
|
|
32
|
+
* `Location` header or a run body; else null. A `timeout` carries it even when no run
|
|
33
|
+
* body has arrived, so `runs.get(run_id, { wait: true })` finds the run again.
|
|
34
|
+
*/
|
|
35
|
+
readonly run_id: string | null;
|
|
28
36
|
/** With `insufficient_funds`: how much Byssium the wallet is short; else null. */
|
|
29
37
|
readonly shortfall: number | null;
|
|
30
38
|
/** With `price_above_max`: what the run costs now; else null. */
|
package/dist/error.js
CHANGED
|
@@ -9,6 +9,12 @@ export class AbyssError extends Error {
|
|
|
9
9
|
param;
|
|
10
10
|
doc_url;
|
|
11
11
|
request_id;
|
|
12
|
+
/**
|
|
13
|
+
* The id of the run the call started or read, once the call has learned it from a
|
|
14
|
+
* `Location` header or a run body; else null. A `timeout` carries it even when no run
|
|
15
|
+
* body has arrived, so `runs.get(run_id, { wait: true })` finds the run again.
|
|
16
|
+
*/
|
|
17
|
+
run_id;
|
|
12
18
|
/** With `insufficient_funds`: how much Byssium the wallet is short; else null. */
|
|
13
19
|
shortfall;
|
|
14
20
|
/** With `price_above_max`: what the run costs now; else null. */
|
|
@@ -22,6 +28,7 @@ export class AbyssError extends Error {
|
|
|
22
28
|
this.param = fields.param ?? null;
|
|
23
29
|
this.doc_url = fields.doc_url ?? null;
|
|
24
30
|
this.request_id = fields.request_id ?? null;
|
|
31
|
+
this.run_id = fields.run_id ?? null;
|
|
25
32
|
this.shortfall = fields.shortfall ?? null;
|
|
26
33
|
this.price = fields.price ?? null;
|
|
27
34
|
this.run = fields.run ?? null;
|
package/dist/http.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface Answer {
|
|
|
14
14
|
/**
|
|
15
15
|
* Sends one request and reads its whole body. A failure before the headers rejects;
|
|
16
16
|
* a cut after them answers `text: null`, so the caller can re-attach. `init.signal`
|
|
17
|
-
* stops the request at any point, and then it always rejects.
|
|
17
|
+
* stops the request at any point, and then it always rejects. `heard` is handed the
|
|
18
|
+
* headers as soon as they arrive, so a stop during the body still leaves them known.
|
|
18
19
|
*/
|
|
19
|
-
export declare function request(url: string, init: RequestInit, limits?: Limits): Promise<Answer>;
|
|
20
|
+
export declare function request(url: string, init: RequestInit, limits?: Limits, heard?: (headers: Headers) => void): Promise<Answer>;
|
package/dist/http.js
CHANGED
|
@@ -2,9 +2,10 @@ export const LIMITS = { connectMs: 10_000, silenceMs: 30_000 };
|
|
|
2
2
|
/**
|
|
3
3
|
* Sends one request and reads its whole body. A failure before the headers rejects;
|
|
4
4
|
* a cut after them answers `text: null`, so the caller can re-attach. `init.signal`
|
|
5
|
-
* stops the request at any point, and then it always rejects.
|
|
5
|
+
* stops the request at any point, and then it always rejects. `heard` is handed the
|
|
6
|
+
* headers as soon as they arrive, so a stop during the body still leaves them known.
|
|
6
7
|
*/
|
|
7
|
-
export async function request(url, init, limits = LIMITS) {
|
|
8
|
+
export async function request(url, init, limits = LIMITS, heard) {
|
|
8
9
|
const stop = init.signal;
|
|
9
10
|
stop?.throwIfAborted();
|
|
10
11
|
const controller = new AbortController();
|
|
@@ -19,6 +20,7 @@ export async function request(url, init, limits = LIMITS) {
|
|
|
19
20
|
finally {
|
|
20
21
|
clearTimeout(connect);
|
|
21
22
|
}
|
|
23
|
+
heard?.(response.headers);
|
|
22
24
|
const text = await readBody(response, controller, limits.silenceMs);
|
|
23
25
|
stop?.throwIfAborted();
|
|
24
26
|
return { status: response.status, headers: response.headers, text };
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** This package's version, written from package.json by scripts/version.js at build. */
|
|
2
|
-
export declare const VERSION = "0.1.1-dev.
|
|
2
|
+
export declare const VERSION = "0.1.1-dev.4";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** This package's version, written from package.json by scripts/version.js at build. */
|
|
2
|
-
export const VERSION = "0.1.1-dev.
|
|
2
|
+
export const VERSION = "0.1.1-dev.4";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "abysshub",
|
|
3
|
-
"version": "0.1.1-dev.
|
|
3
|
+
"version": "0.1.1-dev.4",
|
|
4
4
|
"description": "The official JavaScript library for the Abyss API.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/Abysshub/abysshub-sdk/tree/main/js#readme",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"exports": {
|
|
17
17
|
".": {
|
|
18
18
|
"types": "./dist/index.d.ts",
|
|
19
|
-
"import": "./dist/index.js"
|
|
19
|
+
"import": "./dist/index.js",
|
|
20
|
+
"default": "./dist/index.js"
|
|
20
21
|
}
|
|
21
22
|
},
|
|
22
23
|
"files": [
|