abysshub 0.0.1-dev.4 → 0.0.1-dev.5
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 +20 -0
- package/dist/client.d.ts +26 -0
- package/dist/client.js +136 -0
- package/dist/error.d.ts +24 -0
- package/dist/error.js +23 -0
- package/dist/http.d.ts +18 -0
- package/dist/http.js +46 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/types.d.ts +29 -0
- package/dist/types.js +1 -0
- package/package.json +21 -2
- package/test/package.test.js +0 -9
package/README.md
CHANGED
|
@@ -3,3 +3,23 @@
|
|
|
3
3
|
The official JavaScript library for the [Abyss](https://abysshub.com) API.
|
|
4
4
|
|
|
5
5
|
Pre-release: the Abyss API opens soon.
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { Abyss, AbyssError } from "abysshub";
|
|
9
|
+
|
|
10
|
+
const abyss = new Abyss(); // reads ABYSS_API_KEY
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const run = await abyss.run("widget_131", { amount: 250000 });
|
|
14
|
+
console.log(run.result);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error instanceof AbyssError) console.error(error.code, error.param, error.request_id, error.run);
|
|
17
|
+
else throw error;
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`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`.
|
|
22
|
+
|
|
23
|
+
- **`new Abyss({ apiKey?, baseURL?, maxRetries? })`:** the key defaults to `ABYSS_API_KEY`. A key starting `abyss_sk_dev_` goes to `https://api.dev.abysshub.com`. `ABYSS_BASE_URL` or `baseURL` overrides the address.
|
|
24
|
+
- **`abyss.run(widget, input, { maxPrice?, idempotencyKey? })`:** each call presses with a fresh UUID `Idempotency-Key` unless you give one.
|
|
25
|
+
- **Names:** returned data keeps the API's names (`output_files`, `request_id`). Options are camelCase. Field names are never converted.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Input, Run } from "./types.js";
|
|
2
|
+
export interface AbyssOptions {
|
|
3
|
+
/** Defaults to `ABYSS_API_KEY`. */
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
/** Defaults to `ABYSS_BASE_URL`, else the address the key belongs to. */
|
|
6
|
+
baseURL?: string;
|
|
7
|
+
maxRetries?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface RunOptions {
|
|
10
|
+
/** The most this run may cost, in Byssium. */
|
|
11
|
+
maxPrice?: number;
|
|
12
|
+
/** Defaults to a fresh UUID for each call. */
|
|
13
|
+
idempotencyKey?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare class Abyss {
|
|
16
|
+
#private;
|
|
17
|
+
readonly baseURL: string;
|
|
18
|
+
readonly maxRetries: number;
|
|
19
|
+
constructor(options?: AbyssOptions);
|
|
20
|
+
/**
|
|
21
|
+
* Presses a Widget and waits for the run's ending, re-attaching through `Location`
|
|
22
|
+
* whenever the hold ends early. Returns the succeeded run; raises `AbyssError` on a
|
|
23
|
+
* refusal or a failed run.
|
|
24
|
+
*/
|
|
25
|
+
run(widget: string, input: Input, options?: RunOptions): Promise<Run>;
|
|
26
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { AbyssError } from "./error.js";
|
|
2
|
+
import { request } from "./http.js";
|
|
3
|
+
const API = "https://api.abysshub.com";
|
|
4
|
+
const DEV_API = "https://api.dev.abysshub.com";
|
|
5
|
+
const DEV_KEY_PREFIX = "abyss_sk_dev_";
|
|
6
|
+
/** The pause between two re-attaches that both ended without the run's ending. */
|
|
7
|
+
const REATTACH_PAUSE_MS = 1_000;
|
|
8
|
+
export class Abyss {
|
|
9
|
+
baseURL;
|
|
10
|
+
maxRetries;
|
|
11
|
+
#apiKey;
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
const apiKey = options.apiKey ?? env("ABYSS_API_KEY");
|
|
14
|
+
if (!apiKey) {
|
|
15
|
+
throw new AbyssError({
|
|
16
|
+
code: "invalid_api_key",
|
|
17
|
+
message: "No Abyss API key: pass apiKey, or set ABYSS_API_KEY.",
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
this.#apiKey = apiKey;
|
|
21
|
+
const baseURL = options.baseURL ?? env("ABYSS_BASE_URL") ?? (apiKey.startsWith(DEV_KEY_PREFIX) ? DEV_API : API);
|
|
22
|
+
this.baseURL = baseURL.replace(/\/+$/, "");
|
|
23
|
+
this.maxRetries = options.maxRetries ?? 2;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Presses a Widget and waits for the run's ending, re-attaching through `Location`
|
|
27
|
+
* whenever the hold ends early. Returns the succeeded run; raises `AbyssError` on a
|
|
28
|
+
* refusal or a failed run.
|
|
29
|
+
*/
|
|
30
|
+
async run(widget, input, options = {}) {
|
|
31
|
+
const body = { input };
|
|
32
|
+
if (options.maxPrice !== undefined)
|
|
33
|
+
body.max_price = options.maxPrice;
|
|
34
|
+
const press = await this.#send("POST", `${this.baseURL}/v1/widgets/${encodeURIComponent(widget)}/runs`, {
|
|
35
|
+
"Content-Type": "application/json",
|
|
36
|
+
"Idempotency-Key": options.idempotencyKey ?? crypto.randomUUID(),
|
|
37
|
+
}, JSON.stringify(body));
|
|
38
|
+
const reply = hasEnded(press) ? press : await this.#reattach(press);
|
|
39
|
+
if (reply.run.status === "failed") {
|
|
40
|
+
throw new AbyssError({
|
|
41
|
+
code: reply.run.error?.code ?? "platform_fault",
|
|
42
|
+
message: reply.run.error?.message ?? "The run failed.",
|
|
43
|
+
request_id: reply.requestId,
|
|
44
|
+
run: reply.run,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return reply.run;
|
|
48
|
+
}
|
|
49
|
+
/** Reads the run with `GET <Location>?wait=true` until it ends. Re-attaching has no limit and is not a retry. */
|
|
50
|
+
async #reattach(press) {
|
|
51
|
+
const url = this.#waitURL(press);
|
|
52
|
+
let reply = await this.#send("GET", url);
|
|
53
|
+
while (!hasEnded(reply)) {
|
|
54
|
+
await sleep(REATTACH_PAUSE_MS);
|
|
55
|
+
reply = await this.#send("GET", url);
|
|
56
|
+
}
|
|
57
|
+
return reply;
|
|
58
|
+
}
|
|
59
|
+
#waitURL(reply) {
|
|
60
|
+
const location = reply.location ?? (reply.run ? `/v1/runs/${reply.run.id}` : null);
|
|
61
|
+
if (!location) {
|
|
62
|
+
throw new AbyssError({
|
|
63
|
+
code: "unexpected_response",
|
|
64
|
+
message: "The press answered without a Location to read the run at.",
|
|
65
|
+
request_id: reply.requestId,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const url = new URL(location, `${this.baseURL}/`);
|
|
69
|
+
url.searchParams.set("wait", "true");
|
|
70
|
+
return url.href;
|
|
71
|
+
}
|
|
72
|
+
async #send(method, url, headers = {}, body) {
|
|
73
|
+
const answer = await request(url, {
|
|
74
|
+
method,
|
|
75
|
+
headers: { Authorization: `Bearer ${this.#apiKey}`, Accept: "application/json", ...headers },
|
|
76
|
+
...(body === undefined ? {} : { body }),
|
|
77
|
+
});
|
|
78
|
+
const requestId = answer.headers.get("request-id");
|
|
79
|
+
const parsed = parse(answer.text);
|
|
80
|
+
if (answer.status === 200 || answer.status === 202) {
|
|
81
|
+
return { run: isRun(parsed) ? parsed : null, location: answer.headers.get("location"), requestId };
|
|
82
|
+
}
|
|
83
|
+
throw refusal(answer.status, parsed, requestId);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function refusal(status, body, requestId) {
|
|
87
|
+
const error = isObject(body) && isObject(body.error) ? body.error : null;
|
|
88
|
+
if (!error || typeof error.code !== "string") {
|
|
89
|
+
return new AbyssError({
|
|
90
|
+
code: "unexpected_response",
|
|
91
|
+
message: `The API answered ${status} without an error body.`,
|
|
92
|
+
status,
|
|
93
|
+
request_id: requestId,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return new AbyssError({
|
|
97
|
+
code: error.code,
|
|
98
|
+
message: typeof error.message === "string" ? error.message : error.code,
|
|
99
|
+
status,
|
|
100
|
+
param: typeof error.param === "string" ? error.param : null,
|
|
101
|
+
doc_url: typeof error.doc_url === "string" ? error.doc_url : null,
|
|
102
|
+
request_id: isObject(body) && typeof body.request_id === "string" ? body.request_id : requestId,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function parse(text) {
|
|
106
|
+
if (text === null)
|
|
107
|
+
return undefined;
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(text);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function isObject(value) {
|
|
116
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
117
|
+
}
|
|
118
|
+
function isRun(value) {
|
|
119
|
+
return isObject(value) && typeof value.id === "string" && typeof value.status === "string";
|
|
120
|
+
}
|
|
121
|
+
/** Whether the reply carries a run that has succeeded or failed. */
|
|
122
|
+
function hasEnded(reply) {
|
|
123
|
+
return reply.run !== null && (reply.run.status === "succeeded" || reply.run.status === "failed");
|
|
124
|
+
}
|
|
125
|
+
function env(name) {
|
|
126
|
+
try {
|
|
127
|
+
const { process } = globalThis;
|
|
128
|
+
return process?.env?.[name] || undefined;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function sleep(ms) {
|
|
135
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
136
|
+
}
|
package/dist/error.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Run } from "./types.js";
|
|
2
|
+
export interface AbyssErrorFields {
|
|
3
|
+
code: string;
|
|
4
|
+
message: string;
|
|
5
|
+
status?: number | null;
|
|
6
|
+
param?: string | null;
|
|
7
|
+
doc_url?: string | null;
|
|
8
|
+
request_id?: string | null;
|
|
9
|
+
run?: Run | null;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The one error the library raises: a refusal from `/v1`, or a run that failed.
|
|
13
|
+
* Branch on `code`. A failed run carries the run as `run`.
|
|
14
|
+
*/
|
|
15
|
+
export declare class AbyssError extends Error {
|
|
16
|
+
readonly code: string;
|
|
17
|
+
/** The HTTP status of a refusal; null for a failed run. */
|
|
18
|
+
readonly status: number | null;
|
|
19
|
+
readonly param: string | null;
|
|
20
|
+
readonly doc_url: string | null;
|
|
21
|
+
readonly request_id: string | null;
|
|
22
|
+
readonly run: Run | null;
|
|
23
|
+
constructor(fields: AbyssErrorFields);
|
|
24
|
+
}
|
package/dist/error.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one error the library raises: a refusal from `/v1`, or a run that failed.
|
|
3
|
+
* Branch on `code`. A failed run carries the run as `run`.
|
|
4
|
+
*/
|
|
5
|
+
export class AbyssError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
/** The HTTP status of a refusal; null for a failed run. */
|
|
8
|
+
status;
|
|
9
|
+
param;
|
|
10
|
+
doc_url;
|
|
11
|
+
request_id;
|
|
12
|
+
run;
|
|
13
|
+
constructor(fields) {
|
|
14
|
+
super(fields.message);
|
|
15
|
+
this.name = "AbyssError";
|
|
16
|
+
this.code = fields.code;
|
|
17
|
+
this.status = fields.status ?? null;
|
|
18
|
+
this.param = fields.param ?? null;
|
|
19
|
+
this.doc_url = fields.doc_url ?? null;
|
|
20
|
+
this.request_id = fields.request_id ?? null;
|
|
21
|
+
this.run = fields.run ?? null;
|
|
22
|
+
}
|
|
23
|
+
}
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface Limits {
|
|
2
|
+
/** How long to wait for the status and headers. */
|
|
3
|
+
connectMs: number;
|
|
4
|
+
/** How long the body may stay silent before it counts as a cut. */
|
|
5
|
+
silenceMs: number;
|
|
6
|
+
}
|
|
7
|
+
export declare const LIMITS: Limits;
|
|
8
|
+
export interface Answer {
|
|
9
|
+
status: number;
|
|
10
|
+
headers: Headers;
|
|
11
|
+
/** The whole body, or null when the connection was cut or went silent after the headers. */
|
|
12
|
+
text: string | null;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Sends one request and reads its whole body. A failure before the headers rejects;
|
|
16
|
+
* a cut after them answers `text: null`, so the caller can re-attach.
|
|
17
|
+
*/
|
|
18
|
+
export declare function request(url: string, init: RequestInit, limits?: Limits): Promise<Answer>;
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const LIMITS = { connectMs: 10_000, silenceMs: 30_000 };
|
|
2
|
+
/**
|
|
3
|
+
* Sends one request and reads its whole body. A failure before the headers rejects;
|
|
4
|
+
* a cut after them answers `text: null`, so the caller can re-attach.
|
|
5
|
+
*/
|
|
6
|
+
export async function request(url, init, limits = LIMITS) {
|
|
7
|
+
const controller = new AbortController();
|
|
8
|
+
const connect = setTimeout(() => controller.abort(), limits.connectMs);
|
|
9
|
+
let response;
|
|
10
|
+
try {
|
|
11
|
+
response = await fetch(url, { ...init, signal: controller.signal });
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
clearTimeout(connect);
|
|
15
|
+
}
|
|
16
|
+
const text = await readBody(response, controller, limits.silenceMs);
|
|
17
|
+
return { status: response.status, headers: response.headers, text };
|
|
18
|
+
}
|
|
19
|
+
async function readBody(response, controller, silenceMs) {
|
|
20
|
+
if (!response.body)
|
|
21
|
+
return "";
|
|
22
|
+
const reader = response.body.getReader();
|
|
23
|
+
const decoder = new TextDecoder();
|
|
24
|
+
let text = "";
|
|
25
|
+
let silence;
|
|
26
|
+
const listen = () => {
|
|
27
|
+
clearTimeout(silence);
|
|
28
|
+
silence = setTimeout(() => controller.abort(), silenceMs);
|
|
29
|
+
};
|
|
30
|
+
try {
|
|
31
|
+
listen();
|
|
32
|
+
for (;;) {
|
|
33
|
+
const { done, value } = await reader.read();
|
|
34
|
+
if (done)
|
|
35
|
+
return text + decoder.decode();
|
|
36
|
+
listen();
|
|
37
|
+
text += decoder.decode(value, { stream: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
clearTimeout(silence);
|
|
45
|
+
}
|
|
46
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** A run's place: `succeeded` and `failed` are final. */
|
|
2
|
+
export type RunStatus = "queued" | "running" | "succeeded" | "failed";
|
|
3
|
+
/** One file a run wrote. Its `url` is signed fresh on every read, and expires. */
|
|
4
|
+
export interface RunFile {
|
|
5
|
+
path: string;
|
|
6
|
+
size: number;
|
|
7
|
+
content_type: string;
|
|
8
|
+
url: string;
|
|
9
|
+
}
|
|
10
|
+
/** Why a run failed: `widget_fault`, `platform_fault` or `budget_exceeded`. */
|
|
11
|
+
export interface RunError {
|
|
12
|
+
code: string;
|
|
13
|
+
message: string;
|
|
14
|
+
}
|
|
15
|
+
/** A run, with `/v1`'s names. */
|
|
16
|
+
export interface Run {
|
|
17
|
+
id: string;
|
|
18
|
+
widget: string;
|
|
19
|
+
status: RunStatus;
|
|
20
|
+
price: number;
|
|
21
|
+
result: unknown;
|
|
22
|
+
output_files: RunFile[];
|
|
23
|
+
error: RunError | null;
|
|
24
|
+
created_at: string;
|
|
25
|
+
started_at: string | null;
|
|
26
|
+
ended_at: string | null;
|
|
27
|
+
}
|
|
28
|
+
/** A Widget's input: its Fields by name, never converted. */
|
|
29
|
+
export type Input = Record<string, unknown>;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "abysshub",
|
|
3
|
-
"version": "0.0.1-dev.
|
|
3
|
+
"version": "0.0.1-dev.5",
|
|
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",
|
|
@@ -11,10 +11,29 @@
|
|
|
11
11
|
},
|
|
12
12
|
"bugs": "https://github.com/Abysshub/abysshub-sdk/issues",
|
|
13
13
|
"type": "module",
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"sideEffects": false,
|
|
14
26
|
"engines": {
|
|
15
27
|
"node": ">=20"
|
|
16
28
|
},
|
|
17
29
|
"scripts": {
|
|
18
|
-
"
|
|
30
|
+
"build": "tsc",
|
|
31
|
+
"prepack": "npm run build",
|
|
32
|
+
"test": "npm run build && node --test test/*.test.js"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"ajv": "^8.20.0",
|
|
36
|
+
"ajv-formats": "^3.0.1",
|
|
37
|
+
"typescript": "^5.9.3"
|
|
19
38
|
}
|
|
20
39
|
}
|
package/test/package.test.js
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { test } from "node:test";
|
|
4
|
-
|
|
5
|
-
test("abysshub has no runtime dependencies", async () => {
|
|
6
|
-
const pkg = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
|
|
7
|
-
assert.equal(pkg.name, "abysshub");
|
|
8
|
-
assert.deepEqual(Object.keys(pkg.dependencies ?? {}), []);
|
|
9
|
-
});
|