@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.
Files changed (42) hide show
  1. package/README.md +18 -8
  2. package/dist/core/assets.d.ts +4 -0
  3. package/dist/core/assets.d.ts.map +1 -1
  4. package/dist/core/index.d.ts +1 -0
  5. package/dist/core/index.d.ts.map +1 -1
  6. package/dist/core/index.js +1 -0
  7. package/dist/core/index.js.map +1 -1
  8. package/dist/core/network-error.d.ts +6 -0
  9. package/dist/core/network-error.d.ts.map +1 -0
  10. package/dist/core/network-error.js +8 -0
  11. package/dist/core/network-error.js.map +1 -0
  12. package/dist/core/operations.d.ts +3 -0
  13. package/dist/core/operations.d.ts.map +1 -1
  14. package/dist/core/operations.js +5 -0
  15. package/dist/core/operations.js.map +1 -1
  16. package/dist/core/tasks.d.ts +6 -6
  17. package/dist/core/tasks.d.ts.map +1 -1
  18. package/dist/core/tasks.js +5 -5
  19. package/dist/core/tasks.js.map +1 -1
  20. package/dist/core/transport.d.ts +1 -1
  21. package/dist/core/transport.d.ts.map +1 -1
  22. package/dist/core/transport.js +44 -21
  23. package/dist/core/transport.js.map +1 -1
  24. package/dist/generated/developer-api.gen.d.ts +328 -78
  25. package/dist/generated/developer-api.gen.d.ts.map +1 -1
  26. package/dist/generated/tasks.gen.d.ts +30 -6
  27. package/dist/generated/tasks.gen.d.ts.map +1 -1
  28. package/dist/generated/tasks.gen.js +12 -0
  29. package/dist/generated/tasks.gen.js.map +1 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1 -1
  33. package/dist/index.js.map +1 -1
  34. package/package.json +1 -1
  35. package/src/core/index.ts +1 -0
  36. package/src/core/network-error.ts +8 -0
  37. package/src/core/operations.ts +6 -0
  38. package/src/core/tasks.ts +7 -7
  39. package/src/core/transport.ts +61 -24
  40. package/src/generated/developer-api.gen.ts +328 -78
  41. package/src/generated/tasks.gen.ts +16 -0
  42. package/src/index.ts +1 -0
@@ -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 = (request: Request, next: Fetch) => Promise<Response>;
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 puts its reason under `detail`; operation and storage errors use `message`. */
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 = (middleware: Middleware[], fetch: Fetch): Fetch => {
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
- const retryDelay = (error: unknown, attempt: number): number => {
109
- const backoff = 1000 * 2 ** attempt * (0.5 + Math.random() / 2);
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 backoff;
144
+ return undefined;
114
145
  }
115
146
  const delay = /^\d+$/u.test(value) ? Number(value) * 1000 : Date.parse(value) - Date.now();
116
- return Math.max(backoff, Number.isFinite(delay) ? delay : 0);
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
- path: string,
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 = retry
176
- ? compose(options.middleware ?? [], async (attempt) => {
177
- try {
178
- return await fetch(attempt);
179
- } catch (error) {
180
- networkError = error;
181
- throw error;
182
- }
183
- })
184
- : send;
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
- : error instanceof TypeError && error === networkError;
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);