@worldlabsai/client 0.3.0 → 0.5.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 +18 -8
- package/dist/core/assets.d.ts +4 -0
- package/dist/core/assets.d.ts.map +1 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/network-error.d.ts +6 -0
- package/dist/core/network-error.d.ts.map +1 -0
- package/dist/core/network-error.js +8 -0
- package/dist/core/network-error.js.map +1 -0
- package/dist/core/operations.d.ts +3 -0
- package/dist/core/operations.d.ts.map +1 -1
- package/dist/core/operations.js +5 -0
- package/dist/core/operations.js.map +1 -1
- package/dist/core/tasks.d.ts +6 -6
- package/dist/core/tasks.d.ts.map +1 -1
- package/dist/core/tasks.js +5 -5
- package/dist/core/tasks.js.map +1 -1
- package/dist/core/transport.d.ts +1 -1
- package/dist/core/transport.d.ts.map +1 -1
- package/dist/core/transport.js +44 -21
- package/dist/core/transport.js.map +1 -1
- package/dist/generated/developer-api.gen.d.ts +328 -78
- package/dist/generated/developer-api.gen.d.ts.map +1 -1
- package/dist/generated/tasks.gen.d.ts +30 -6
- package/dist/generated/tasks.gen.d.ts.map +1 -1
- package/dist/generated/tasks.gen.js +12 -0
- package/dist/generated/tasks.gen.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/index.ts +1 -0
- package/src/core/network-error.ts +8 -0
- package/src/core/operations.ts +6 -0
- package/src/core/tasks.ts +7 -7
- package/src/core/transport.ts +61 -24
- package/src/generated/developer-api.gen.ts +328 -78
- package/src/generated/tasks.gen.ts +16 -0
- package/src/index.ts +1 -0
package/src/core/transport.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { NetworkError } from "./network-error.ts";
|
|
1
2
|
import { sleep } from "./sleep.ts";
|
|
2
3
|
|
|
3
4
|
/** Handles a `Request` for API calls and signed uploads; standard `fetch` is compatible. */
|
|
@@ -8,7 +9,11 @@ export type Fetch = (request: Request) => Promise<Response>;
|
|
|
8
9
|
* without calling `next`, call `next` more than once for retries, or inspect the response.
|
|
9
10
|
* Clone requests before sending them if a retry may need to reuse the body.
|
|
10
11
|
*/
|
|
11
|
-
export type Middleware = (
|
|
12
|
+
export type Middleware = (
|
|
13
|
+
request: Request,
|
|
14
|
+
next: Fetch,
|
|
15
|
+
options?: Pick<RequestOptions, "retry">,
|
|
16
|
+
) => Promise<Response>;
|
|
12
17
|
|
|
13
18
|
/** Values accepted in a request query. `null` and `undefined` are omitted. */
|
|
14
19
|
export type QueryValue = string | number | boolean | null | undefined;
|
|
@@ -40,7 +45,19 @@ export interface TransportOptions {
|
|
|
40
45
|
pathParams?: Record<string, string>;
|
|
41
46
|
}
|
|
42
47
|
|
|
43
|
-
/** FastAPI
|
|
48
|
+
/** One FastAPI validation issue, `{ loc: ["body", "frames"], msg: "Field required" }`. */
|
|
49
|
+
const validationIssue = (issue: unknown): string => {
|
|
50
|
+
const location: unknown =
|
|
51
|
+
typeof issue === "object" && issue !== null ? Reflect.get(issue, "loc") : undefined;
|
|
52
|
+
const message: unknown =
|
|
53
|
+
typeof issue === "object" && issue !== null ? Reflect.get(issue, "msg") : issue;
|
|
54
|
+
return Array.isArray(location) ? `${location.join(".")}: ${String(message)}` : String(message);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* FastAPI puts its reason under `detail`, as a list of validation issues for a
|
|
59
|
+
* 422; operation and storage errors use `message`.
|
|
60
|
+
*/
|
|
44
61
|
const errorDetail = (body: unknown): string | undefined => {
|
|
45
62
|
if (typeof body !== "object" || body === null) {
|
|
46
63
|
return undefined;
|
|
@@ -50,6 +67,9 @@ const errorDetail = (body: unknown): string | undefined => {
|
|
|
50
67
|
if (typeof value === "string" && value !== "") {
|
|
51
68
|
return value;
|
|
52
69
|
}
|
|
70
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
71
|
+
return value.map((issue) => validationIssue(issue)).join("; ");
|
|
72
|
+
}
|
|
53
73
|
}
|
|
54
74
|
return undefined;
|
|
55
75
|
};
|
|
@@ -82,11 +102,15 @@ const fillPath = (path: string, params: Record<string, string>): string =>
|
|
|
82
102
|
return encodeURIComponent(value);
|
|
83
103
|
});
|
|
84
104
|
|
|
85
|
-
const compose = (
|
|
105
|
+
const compose = (
|
|
106
|
+
middleware: Middleware[],
|
|
107
|
+
fetch: Fetch,
|
|
108
|
+
options?: Pick<RequestOptions, "retry">,
|
|
109
|
+
): Fetch => {
|
|
86
110
|
let next = fetch;
|
|
87
111
|
for (const layer of middleware.toReversed()) {
|
|
88
112
|
const inner = next;
|
|
89
|
-
next = async (request) => await layer(request, inner);
|
|
113
|
+
next = async (request) => await layer(request, inner, options);
|
|
90
114
|
}
|
|
91
115
|
return next;
|
|
92
116
|
};
|
|
@@ -105,17 +129,27 @@ const parseBody = async (response: Response): Promise<unknown> => {
|
|
|
105
129
|
|
|
106
130
|
const RETRYABLE_STATUS_CODES = new Set([429, 502, 503, 504]);
|
|
107
131
|
|
|
108
|
-
|
|
109
|
-
|
|
132
|
+
/**
|
|
133
|
+
* Longest `Retry-After` a retry waits out. A concurrency cap, an hourly quota,
|
|
134
|
+
* or a closed task names minutes to hours, which the caller has to surface
|
|
135
|
+
* rather than sit through.
|
|
136
|
+
*/
|
|
137
|
+
const MAX_RETRY_AFTER_MS = 10_000;
|
|
138
|
+
|
|
139
|
+
/** The wait a response names in `Retry-After`, as seconds or an HTTP date. */
|
|
140
|
+
const retryAfterMs = (error: unknown): number | undefined => {
|
|
110
141
|
const value =
|
|
111
142
|
error instanceof ApiError ? error.response.headers.get("retry-after")?.trim() : undefined;
|
|
112
143
|
if (!value) {
|
|
113
|
-
return
|
|
144
|
+
return undefined;
|
|
114
145
|
}
|
|
115
146
|
const delay = /^\d+$/u.test(value) ? Number(value) * 1000 : Date.parse(value) - Date.now();
|
|
116
|
-
return
|
|
147
|
+
return Number.isFinite(delay) ? Math.max(0, delay) : undefined;
|
|
117
148
|
};
|
|
118
149
|
|
|
150
|
+
const retryDelay = (error: unknown, attempt: number): number =>
|
|
151
|
+
Math.max(1000 * 2 ** attempt * (0.5 + Math.random() / 2), retryAfterMs(error) ?? 0);
|
|
152
|
+
|
|
119
153
|
export interface Transport {
|
|
120
154
|
/**
|
|
121
155
|
* Send a request and parse its response as JSON, falling back to text.
|
|
@@ -150,10 +184,8 @@ export const createTransport = (options: TransportOptions): Transport => {
|
|
|
150
184
|
return {
|
|
151
185
|
fetch,
|
|
152
186
|
send,
|
|
153
|
-
request: async <T>(
|
|
154
|
-
|
|
155
|
-
{ body, params, query, retry = false, ...init }: RequestOptions = {},
|
|
156
|
-
): Promise<T> => {
|
|
187
|
+
request: async <T>(path: string, requestOptions: RequestOptions = {}): Promise<T> => {
|
|
188
|
+
const { body, params, query, retry = false, ...init } = requestOptions;
|
|
157
189
|
const url = new URL(baseUrl + fillPath(path, { ...boundParams, ...params }));
|
|
158
190
|
for (const [key, value] of Object.entries(query ?? {})) {
|
|
159
191
|
if (value !== undefined && value !== null) {
|
|
@@ -172,16 +204,18 @@ export const createTransport = (options: TransportOptions): Transport => {
|
|
|
172
204
|
|
|
173
205
|
// Provider and middleware TypeErrors must not be mistaken for network failures.
|
|
174
206
|
let networkError: unknown;
|
|
175
|
-
const sendRequest =
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
|
|
207
|
+
const sendRequest = compose(
|
|
208
|
+
options.middleware ?? [],
|
|
209
|
+
async (attempt) => {
|
|
210
|
+
try {
|
|
211
|
+
return await fetch(attempt);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
networkError = error;
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
requestOptions,
|
|
218
|
+
);
|
|
185
219
|
|
|
186
220
|
// oxlint-disable no-await-in-loop -- retries are sequential and preserve the original request
|
|
187
221
|
for (let attempt = 0; ; attempt += 1) {
|
|
@@ -206,11 +240,14 @@ export const createTransport = (options: TransportOptions): Transport => {
|
|
|
206
240
|
return parsed as T;
|
|
207
241
|
} catch (error) {
|
|
208
242
|
request.signal.throwIfAborted();
|
|
243
|
+
const networkFailure = error instanceof TypeError && error === networkError;
|
|
209
244
|
const transient =
|
|
210
245
|
error instanceof ApiError
|
|
211
|
-
? RETRYABLE_STATUS_CODES.has(error.response.status)
|
|
212
|
-
|
|
246
|
+
? RETRYABLE_STATUS_CODES.has(error.response.status) &&
|
|
247
|
+
(retryAfterMs(error) ?? 0) <= MAX_RETRY_AFTER_MS
|
|
248
|
+
: networkFailure;
|
|
213
249
|
if (!retry || attempt >= 3 || !transient) {
|
|
250
|
+
if (networkFailure) throw new NetworkError(error);
|
|
214
251
|
throw error;
|
|
215
252
|
}
|
|
216
253
|
await sleep(retryDelay(error, attempt), request.signal);
|