abysshub 0.1.1-dev.1 → 0.1.1-dev.3

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 CHANGED
@@ -109,6 +109,9 @@ A refusal, a failed run, a timeout and a lost connection all raise one class, `A
109
109
  | `param` | the Field, body key, query key or header at fault, else `null` |
110
110
  | `doc_url` | the code on the API access page, else `null` |
111
111
  | `request_id` | quote it to support, else `null` |
112
+ | `shortfall` | with `insufficient_funds`: how much Byssium the wallet is short, else `null` |
113
+ | `price` | with `price_above_max`: what the run costs now, else `null` |
114
+ | `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` |
112
115
  | `run` | the run, for a failed run or a timeout, else `null` |
113
116
 
114
117
  - **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).
@@ -135,13 +138,14 @@ There is no timeout by default: `run()` waits as long as the run takes. `timeout
135
138
  try {
136
139
  const run = await abyss.run("widget_131", input, { timeout: 60, signal: AbortSignal.timeout(90_000) });
137
140
  } catch (error) {
138
- if (error instanceof AbyssError && error.code === "timeout") {
139
- // The run goes on. error.run is the run as last seen, or null when none was seen yet.
141
+ if (error instanceof AbyssError && error.code === "timeout" && error.run_id) {
142
+ // The run goes on: wait for it again, without pressing (and paying) twice.
143
+ const run = await abyss.runs.get(error.run_id, { wait: true });
140
144
  }
141
145
  }
142
146
  ```
143
147
 
144
- Either one raises `AbyssError` with `code: "timeout"` and `run`, the run as last seen. During the press's first hold the run has not been seen yet, so `run` is `null`; to keep its `id` whatever happens, press with `runs.create()` and wait with `runs.get(id, { wait: true, timeout })`.
148
+ 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.
145
149
 
146
150
  ## Names
147
151
 
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"` and the run as last seen.
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 = { signal: stop.signal, seen: null };
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({ code: "timeout", message, run: call.seen && this.#run(call.seen) });
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({ signal: null, seen: null }, data.id, false));
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
- return { body: parsed, run, cut: answer.text === null, location: answer.headers.get("location"), requestId };
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({
@@ -283,7 +309,10 @@ function refusal(status, body, requestId) {
283
309
  status,
284
310
  param: typeof error.param === "string" ? error.param : null,
285
311
  doc_url: typeof error.doc_url === "string" ? error.doc_url : null,
312
+ shortfall: typeof error.shortfall === "number" ? error.shortfall : null,
313
+ price: typeof error.price === "number" ? error.price : null,
286
314
  request_id: isObject(body) && typeof body.request_id === "string" ? body.request_id : requestId,
315
+ run_id: runId,
287
316
  });
288
317
  }
289
318
  function parse(text) {
package/dist/error.d.ts CHANGED
@@ -6,6 +6,12 @@ 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;
11
+ /** With `insufficient_funds`: how much Byssium the wallet is short. */
12
+ shortfall?: number | null;
13
+ /** With `price_above_max`: what the run costs now. */
14
+ price?: number | null;
9
15
  run?: Run | null;
10
16
  /** What a `connection_error` was caused by. */
11
17
  cause?: unknown;
@@ -21,6 +27,16 @@ export declare class AbyssError extends Error {
21
27
  readonly param: string | null;
22
28
  readonly doc_url: string | null;
23
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;
36
+ /** With `insufficient_funds`: how much Byssium the wallet is short; else null. */
37
+ readonly shortfall: number | null;
38
+ /** With `price_above_max`: what the run costs now; else null. */
39
+ readonly price: number | null;
24
40
  readonly run: Run | null;
25
41
  constructor(fields: AbyssErrorFields);
26
42
  }
package/dist/error.js CHANGED
@@ -9,6 +9,16 @@ 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;
18
+ /** With `insufficient_funds`: how much Byssium the wallet is short; else null. */
19
+ shortfall;
20
+ /** With `price_above_max`: what the run costs now; else null. */
21
+ price;
12
22
  run;
13
23
  constructor(fields) {
14
24
  super(fields.message, fields.cause === undefined ? undefined : { cause: fields.cause });
@@ -18,6 +28,9 @@ export class AbyssError extends Error {
18
28
  this.param = fields.param ?? null;
19
29
  this.doc_url = fields.doc_url ?? null;
20
30
  this.request_id = fields.request_id ?? null;
31
+ this.run_id = fields.run_id ?? null;
32
+ this.shortfall = fields.shortfall ?? null;
33
+ this.price = fields.price ?? null;
21
34
  this.run = fields.run ?? null;
22
35
  }
23
36
  }
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.1";
2
+ export declare const VERSION = "0.1.1-dev.3";
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.1";
2
+ export const VERSION = "0.1.1-dev.3";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abysshub",
3
- "version": "0.1.1-dev.1",
3
+ "version": "0.1.1-dev.3",
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",