contree-client 0.3.0 → 0.4.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 +6 -4
- package/lib/client.js +251 -191
- package/lib/errors.d.ts +21 -15
- package/lib/errors.js +20 -18
- package/lib/index.d.ts +4 -0
- package/lib/index.js +4 -0
- package/lib/operations.js +24 -34
- package/lib/profiles.d.ts +1 -3
- package/lib/profiles.js +1 -2
- package/lib/runtime.d.ts +6 -3
- package/lib/runtime.js +13 -51
- package/lib/testing.js +2 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,9 +63,11 @@ const client = new ContreeClient(token, { retry: new RetryPolicy() });
|
|
|
63
63
|
const stored = await client.ensureFile(bytes);
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
`new RetryPolicy({ retryUnsafe: true })` to
|
|
66
|
+
Idempotent requests retry after connection errors, request
|
|
67
|
+
timeouts, and retryable responses (410/425/429/5xx). The client honors
|
|
68
|
+
`Retry-After`. Pass `new RetryPolicy({ retryUnsafe: true })` to retry
|
|
69
|
+
POSTs. Responses 425 and 429 are always safe to retry because they mean
|
|
70
|
+
the backend did not process the request.
|
|
69
71
|
|
|
70
72
|
## Bring your own fetch
|
|
71
73
|
|
|
@@ -93,7 +95,7 @@ const client = new ContreeClient(token, {
|
|
|
93
95
|
(`ReadableStream` uploads) are Node-only.
|
|
94
96
|
- A truncated compressed download ends the stream short instead of
|
|
95
97
|
throwing: `fetch` is lenient about a missing gzip trailer (the
|
|
96
|
-
Python client raises `
|
|
98
|
+
Python client raises `EOFError` there).
|
|
97
99
|
|
|
98
100
|
## Testing
|
|
99
101
|
|
package/lib/client.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// Generated by codegen from the OpenAPI spec - do not edit.
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
APIConnectionError,
|
|
5
|
+
APIStatusError,
|
|
6
|
+
ERROR_CLASSES,
|
|
7
|
+
ServerError,
|
|
6
8
|
NotFoundError,
|
|
7
|
-
SSEStreamError,
|
|
8
9
|
} from "./errors.js";
|
|
9
10
|
import { AUTH_TYPE_IAM, ProfileError, resolveProfile } from "./profiles.js";
|
|
10
11
|
import {
|
|
11
|
-
EVENTS_UNAVAILABLE_STATUSES,
|
|
12
12
|
IS_NODE,
|
|
13
13
|
SSEParser,
|
|
14
14
|
TIGHT_LOOP_FLOOR,
|
|
@@ -17,20 +17,15 @@ import {
|
|
|
17
17
|
UA_RUNTIME,
|
|
18
18
|
decodeFramePayload,
|
|
19
19
|
encodeQuery,
|
|
20
|
-
errorForResponse,
|
|
21
20
|
monotonic,
|
|
22
|
-
|
|
21
|
+
responseErrorDetails,
|
|
23
22
|
retryDelays,
|
|
24
23
|
sha256,
|
|
25
24
|
isUuid,
|
|
26
25
|
sleep,
|
|
27
26
|
} from "./runtime.js";
|
|
28
27
|
import { DEFAULT_BASE_URL } from "./specInfo.js";
|
|
29
|
-
import {
|
|
30
|
-
EventDataCompletion,
|
|
31
|
-
OperationEvent,
|
|
32
|
-
isTerminalStatus,
|
|
33
|
-
} from "./models.js";
|
|
28
|
+
import { OperationEvent, isTerminalStatus } from "./models.js";
|
|
34
29
|
import * as operations from "./operations.js";
|
|
35
30
|
|
|
36
31
|
/** Contree API client on top of the platform fetch.
|
|
@@ -63,6 +58,9 @@ export class ContreeClient {
|
|
|
63
58
|
`unsupported baseUrl scheme ${parsed.protocol} in ${baseUrl}: use http:// or https://`,
|
|
64
59
|
);
|
|
65
60
|
}
|
|
61
|
+
if (parsed.username || parsed.password) {
|
|
62
|
+
throw new RangeError("baseUrl must not include credentials");
|
|
63
|
+
}
|
|
66
64
|
this.token = token ?? null;
|
|
67
65
|
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
68
66
|
this.project = project;
|
|
@@ -133,6 +131,11 @@ export class ContreeClient {
|
|
|
133
131
|
if (IS_NODE && !("User-Agent" in headers)) {
|
|
134
132
|
headers["User-Agent"] = this.userAgent();
|
|
135
133
|
}
|
|
134
|
+
try {
|
|
135
|
+
new Headers(headers);
|
|
136
|
+
} catch {
|
|
137
|
+
throw new RangeError("invalid HTTP header name or value");
|
|
138
|
+
}
|
|
136
139
|
return headers;
|
|
137
140
|
}
|
|
138
141
|
|
|
@@ -155,33 +158,91 @@ export class ContreeClient {
|
|
|
155
158
|
return options;
|
|
156
159
|
}
|
|
157
160
|
|
|
158
|
-
/** Execute
|
|
161
|
+
/** Execute one request and map its transport and HTTP errors. */
|
|
159
162
|
async request(spec) {
|
|
160
163
|
const controller = new AbortController();
|
|
164
|
+
const deadline = spec.deadline ?? null;
|
|
165
|
+
const remaining = deadline === null ? null : deadline - monotonic();
|
|
166
|
+
if (remaining !== null && remaining <= 0) {
|
|
167
|
+
const cause = new DOMException("request timed out", "TimeoutError");
|
|
168
|
+
throw new APIConnectionError(cause.message, { cause, timedOut: true });
|
|
169
|
+
}
|
|
170
|
+
const url = this.buildUrl(spec);
|
|
171
|
+
const options = this._fetchOptions(spec, controller.signal);
|
|
172
|
+
const timeout =
|
|
173
|
+
this.timeout === null
|
|
174
|
+
? remaining
|
|
175
|
+
: remaining === null
|
|
176
|
+
? this.timeout
|
|
177
|
+
: Math.min(this.timeout, remaining);
|
|
161
178
|
// an explicit timer, not AbortSignal.timeout(): Node unrefs that
|
|
162
179
|
// timer, so it can never fire when nothing else keeps the event
|
|
163
180
|
// loop alive (custom fetch transports, for one)
|
|
164
181
|
const timer =
|
|
165
|
-
|
|
182
|
+
timeout !== null
|
|
166
183
|
? setTimeout(
|
|
167
184
|
() =>
|
|
168
185
|
controller.abort(
|
|
169
186
|
new DOMException("request timed out", "TimeoutError"),
|
|
170
187
|
),
|
|
171
|
-
|
|
188
|
+
Math.ceil(Math.max(0, timeout) * 1000),
|
|
172
189
|
)
|
|
173
190
|
: null;
|
|
174
191
|
try {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
this.
|
|
178
|
-
)
|
|
179
|
-
|
|
192
|
+
let response;
|
|
193
|
+
try {
|
|
194
|
+
response = await this._fetch(url, options);
|
|
195
|
+
} catch (cause) {
|
|
196
|
+
if (cause?.name === "AbortError" && !controller.signal.aborted) {
|
|
197
|
+
throw cause;
|
|
198
|
+
}
|
|
199
|
+
throw new APIConnectionError(String(cause?.message ?? cause), {
|
|
200
|
+
cause,
|
|
201
|
+
timedOut:
|
|
202
|
+
cause?.name === "TimeoutError" ||
|
|
203
|
+
controller.signal.reason?.name === "TimeoutError",
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
if (deadline !== null && monotonic() >= deadline) {
|
|
207
|
+
const cause = new DOMException("request timed out", "TimeoutError");
|
|
208
|
+
throw new APIConnectionError(cause.message, { cause, timedOut: true });
|
|
209
|
+
}
|
|
210
|
+
let body;
|
|
211
|
+
try {
|
|
212
|
+
body = new Uint8Array(await response.arrayBuffer());
|
|
213
|
+
} catch (cause) {
|
|
214
|
+
if (cause?.name === "AbortError" && !controller.signal.aborted) {
|
|
215
|
+
throw cause;
|
|
216
|
+
}
|
|
217
|
+
throw new APIConnectionError(String(cause?.message ?? cause), {
|
|
218
|
+
cause,
|
|
219
|
+
timedOut:
|
|
220
|
+
cause?.name === "TimeoutError" ||
|
|
221
|
+
controller.signal.reason?.name === "TimeoutError",
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
if (deadline !== null && monotonic() >= deadline) {
|
|
225
|
+
const cause = new DOMException("request timed out", "TimeoutError");
|
|
226
|
+
throw new APIConnectionError(cause.message, { cause, timedOut: true });
|
|
227
|
+
}
|
|
180
228
|
const headers = {};
|
|
181
229
|
response.headers.forEach((value, key) => {
|
|
182
230
|
headers[key.toLowerCase()] = value;
|
|
183
231
|
});
|
|
184
|
-
|
|
232
|
+
const data = {
|
|
233
|
+
status: response.status,
|
|
234
|
+
headers,
|
|
235
|
+
body,
|
|
236
|
+
url: response.url,
|
|
237
|
+
};
|
|
238
|
+
if (response.status >= 400) {
|
|
239
|
+
const { error, traceback, retryAfter } = responseErrorDetails(data);
|
|
240
|
+
const ErrorClass =
|
|
241
|
+
ERROR_CLASSES.get(response.status) ??
|
|
242
|
+
(response.status >= 500 ? ServerError : APIStatusError);
|
|
243
|
+
throw new ErrorClass(response.status, error, { traceback, retryAfter });
|
|
244
|
+
}
|
|
245
|
+
return data;
|
|
185
246
|
} finally {
|
|
186
247
|
if (timer !== null) {
|
|
187
248
|
clearTimeout(timer);
|
|
@@ -189,19 +250,7 @@ export class ContreeClient {
|
|
|
189
250
|
}
|
|
190
251
|
}
|
|
191
252
|
|
|
192
|
-
/**
|
|
193
|
-
_transportRetryable(error) {
|
|
194
|
-
return error instanceof TypeError;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
_transportNonretryable(error) {
|
|
198
|
-
return (
|
|
199
|
-
error instanceof DOMException &&
|
|
200
|
-
(error.name === "AbortError" || error.name === "TimeoutError")
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/** One transparent reconnect for a replayable idempotent request:
|
|
253
|
+
/** One transparent reconnect for a replayable connection error:
|
|
205
254
|
* fetch pools keep-alive sockets and cannot pre-check them the way
|
|
206
255
|
* the Python adapters do, so the first request on a socket the
|
|
207
256
|
* server already closed fails without ever reaching the server. */
|
|
@@ -213,8 +262,8 @@ export class ContreeClient {
|
|
|
213
262
|
!spec.idempotent ||
|
|
214
263
|
(typeof ReadableStream !== "undefined" &&
|
|
215
264
|
spec.body instanceof ReadableStream) ||
|
|
216
|
-
!
|
|
217
|
-
|
|
265
|
+
!(error instanceof APIConnectionError) ||
|
|
266
|
+
error.cause?.name !== "TypeError"
|
|
218
267
|
) {
|
|
219
268
|
throw error;
|
|
220
269
|
}
|
|
@@ -242,41 +291,45 @@ export class ContreeClient {
|
|
|
242
291
|
// contract guarantees both mean the request was rejected before
|
|
243
292
|
// any processing, so replaying is always safe.
|
|
244
293
|
const replaySafe = spec.idempotent || policy.retryUnsafe;
|
|
294
|
+
const deadline = spec.deadline ?? null;
|
|
245
295
|
const delays = retryDelays(policy.delays);
|
|
246
296
|
let attempts = 0;
|
|
247
297
|
for (;;) {
|
|
248
298
|
attempts += 1;
|
|
249
299
|
const exhausted =
|
|
250
300
|
policy.maxAttempts !== null && attempts >= policy.maxAttempts;
|
|
251
|
-
let response;
|
|
252
301
|
try {
|
|
253
|
-
|
|
302
|
+
return await this.request(spec);
|
|
254
303
|
} catch (error) {
|
|
304
|
+
const retryableStatus =
|
|
305
|
+
error instanceof APIStatusError &&
|
|
306
|
+
policy.retryableStatus(error.status);
|
|
307
|
+
const rejectedBeforeProcessing =
|
|
308
|
+
error instanceof APIStatusError &&
|
|
309
|
+
(error.status === 425 || error.status === 429);
|
|
255
310
|
if (
|
|
256
|
-
!
|
|
257
|
-
!
|
|
258
|
-
|
|
259
|
-
|
|
311
|
+
(!(error instanceof APIConnectionError) && !retryableStatus) ||
|
|
312
|
+
(!replaySafe && !rejectedBeforeProcessing) ||
|
|
313
|
+
exhausted ||
|
|
314
|
+
(deadline !== null && monotonic() >= deadline)
|
|
260
315
|
) {
|
|
261
316
|
throw error;
|
|
262
317
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
318
|
+
let delay =
|
|
319
|
+
error instanceof APIStatusError && error.retryAfter !== null
|
|
320
|
+
? error.retryAfter
|
|
321
|
+
: delays.next().value;
|
|
322
|
+
if (deadline !== null) {
|
|
323
|
+
delay = Math.min(delay, Math.max(0, deadline - monotonic()));
|
|
324
|
+
}
|
|
325
|
+
await sleep(delay);
|
|
271
326
|
}
|
|
272
|
-
const retryAfter = retryAfterDelay(response);
|
|
273
|
-
await sleep(retryAfter !== null ? retryAfter : delays.next().value);
|
|
274
327
|
}
|
|
275
328
|
}
|
|
276
329
|
|
|
277
330
|
/** Execute the request and yield response body chunks. */
|
|
278
331
|
async *stream(spec) {
|
|
279
|
-
|
|
332
|
+
let controller;
|
|
280
333
|
let timer = null;
|
|
281
334
|
const disarm = () => {
|
|
282
335
|
if (timer !== null) {
|
|
@@ -294,21 +347,33 @@ export class ContreeClient {
|
|
|
294
347
|
controller.abort(
|
|
295
348
|
new DOMException("stream read timed out", "TimeoutError"),
|
|
296
349
|
),
|
|
297
|
-
Math.max(0, seconds) * 1000,
|
|
350
|
+
Math.ceil(Math.max(0, seconds) * 1000),
|
|
298
351
|
);
|
|
299
352
|
};
|
|
300
353
|
const sse = spec.accept === "text/event-stream";
|
|
301
354
|
const deadline = spec.deadline ?? null; // monotonic seconds
|
|
302
355
|
// the budget for the next transport wait: downloads use the
|
|
303
|
-
// client timeout
|
|
304
|
-
// unless the caller set
|
|
305
|
-
// re-enter abortIn with a shrinking remainder, so
|
|
306
|
-
//
|
|
356
|
+
// client timeout as an idle cap, bounded by the caller's absolute
|
|
357
|
+
// deadline; SSE is unbounded unless the caller set a deadline.
|
|
358
|
+
// Keepalive frames re-enter abortIn with a shrinking remainder, so
|
|
359
|
+
// the bound stays absolute while the server keeps the stream warm.
|
|
307
360
|
const nextBudget = () => {
|
|
308
|
-
|
|
309
|
-
|
|
361
|
+
const remaining = deadline === null ? null : deadline - monotonic();
|
|
362
|
+
if (sse || this.timeout === null) {
|
|
363
|
+
return remaining;
|
|
310
364
|
}
|
|
311
|
-
return
|
|
365
|
+
return remaining === null
|
|
366
|
+
? this.timeout
|
|
367
|
+
: Math.min(this.timeout, remaining);
|
|
368
|
+
};
|
|
369
|
+
const connectBudget = () => {
|
|
370
|
+
const remaining = deadline === null ? null : deadline - monotonic();
|
|
371
|
+
if (this.timeout === null) {
|
|
372
|
+
return remaining;
|
|
373
|
+
}
|
|
374
|
+
return remaining === null
|
|
375
|
+
? this.timeout
|
|
376
|
+
: Math.min(this.timeout, remaining);
|
|
312
377
|
};
|
|
313
378
|
// the server may close a pooled keep-alive socket between
|
|
314
379
|
// requests and fetch exposes no pool to pre-check (the Python
|
|
@@ -322,57 +387,68 @@ export class ContreeClient {
|
|
|
322
387
|
typeof ReadableStream !== "undefined" &&
|
|
323
388
|
spec.body instanceof ReadableStream
|
|
324
389
|
);
|
|
325
|
-
|
|
390
|
+
// SSE reconnection belongs to followOperationEvents(), which
|
|
391
|
+
// checks terminal status after each failed stream.
|
|
392
|
+
const policy = replayable && !sse ? this.retry : null;
|
|
326
393
|
const maxAttempts =
|
|
327
394
|
policy !== null ? policy.maxAttempts : replayable ? 2 : 1;
|
|
328
395
|
const delays = policy === null ? null : retryDelays(policy.delays);
|
|
329
396
|
let attempts = 0;
|
|
330
397
|
let response;
|
|
331
398
|
for (;;) {
|
|
399
|
+
if (deadline !== null && monotonic() >= deadline) {
|
|
400
|
+
throw new DOMException("stream read timed out", "TimeoutError");
|
|
401
|
+
}
|
|
332
402
|
attempts += 1;
|
|
333
|
-
|
|
403
|
+
controller = new AbortController();
|
|
404
|
+
abortIn(connectBudget());
|
|
334
405
|
try {
|
|
335
406
|
response = await this._fetch(
|
|
336
407
|
this.buildUrl(spec),
|
|
337
408
|
this._fetchOptions(spec, controller.signal),
|
|
338
409
|
);
|
|
410
|
+
if (deadline !== null && monotonic() >= deadline) {
|
|
411
|
+
throw new DOMException("stream read timed out", "TimeoutError");
|
|
412
|
+
}
|
|
339
413
|
break;
|
|
340
414
|
} catch (error) {
|
|
341
415
|
disarm();
|
|
416
|
+
const reconnectable =
|
|
417
|
+
error?.name === "TypeError" ||
|
|
418
|
+
(policy !== null && error?.name === "TimeoutError");
|
|
342
419
|
if (
|
|
343
|
-
!
|
|
344
|
-
|
|
345
|
-
(
|
|
420
|
+
!reconnectable ||
|
|
421
|
+
(maxAttempts !== null && attempts >= maxAttempts) ||
|
|
422
|
+
(deadline !== null && monotonic() >= deadline)
|
|
346
423
|
) {
|
|
347
424
|
throw error;
|
|
348
425
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
// endless body must not hang the caller
|
|
355
|
-
let body;
|
|
356
|
-
try {
|
|
357
|
-
body = new Uint8Array(await response.arrayBuffer());
|
|
358
|
-
} finally {
|
|
359
|
-
disarm();
|
|
426
|
+
let delay = delays === null ? 0 : delays.next().value;
|
|
427
|
+
if (deadline !== null) {
|
|
428
|
+
delay = Math.min(delay, Math.max(0, deadline - monotonic()));
|
|
429
|
+
}
|
|
430
|
+
await sleep(delay);
|
|
360
431
|
}
|
|
361
|
-
const headers = {};
|
|
362
|
-
response.headers.forEach((value, key) => {
|
|
363
|
-
headers[key.toLowerCase()] = value;
|
|
364
|
-
});
|
|
365
|
-
throw errorForResponse({ status: response.status, headers, body });
|
|
366
432
|
}
|
|
367
433
|
disarm();
|
|
368
434
|
const reader = response.body.getReader();
|
|
369
435
|
try {
|
|
436
|
+
if (response.status >= 400) {
|
|
437
|
+
throw new RangeError(`HTTP ${response.status}`);
|
|
438
|
+
}
|
|
370
439
|
for (;;) {
|
|
371
|
-
|
|
440
|
+
const budget = nextBudget();
|
|
441
|
+
if (budget !== null && budget <= 0) {
|
|
442
|
+
throw new DOMException("stream read timed out", "TimeoutError");
|
|
443
|
+
}
|
|
444
|
+
abortIn(budget);
|
|
372
445
|
const { done, value } = await reader.read();
|
|
373
446
|
// the timer covers only the transport wait, never the
|
|
374
447
|
// consumer's processing of the yielded chunk
|
|
375
448
|
disarm();
|
|
449
|
+
if (deadline !== null && monotonic() >= deadline) {
|
|
450
|
+
throw new DOMException("stream read timed out", "TimeoutError");
|
|
451
|
+
}
|
|
376
452
|
if (done) {
|
|
377
453
|
return;
|
|
378
454
|
}
|
|
@@ -393,14 +469,42 @@ export class ContreeClient {
|
|
|
393
469
|
async close() {}
|
|
394
470
|
|
|
395
471
|
/** Best-effort check that the operation reached a terminal state. */
|
|
396
|
-
async operationTerminal(operationId) {
|
|
472
|
+
async operationTerminal(operationId, deadline = null) {
|
|
397
473
|
let status;
|
|
474
|
+
const deadlineLimited =
|
|
475
|
+
deadline !== null &&
|
|
476
|
+
(this.timeout === null || deadline - monotonic() <= this.timeout);
|
|
398
477
|
try {
|
|
399
|
-
|
|
478
|
+
// The outer event loop owns retries. Each status probe uses one
|
|
479
|
+
// transport attempt and stays inside the caller's deadline.
|
|
480
|
+
const spec = operations.buildGetOperationStatus(operationId);
|
|
481
|
+
spec.deadline = deadline;
|
|
482
|
+
status = operations.parseGetOperationStatus(
|
|
483
|
+
await this.request(spec),
|
|
484
|
+
).status;
|
|
400
485
|
} catch (error) {
|
|
401
|
-
if (error instanceof
|
|
486
|
+
if (error instanceof APIConnectionError) {
|
|
487
|
+
if (
|
|
488
|
+
deadline !== null &&
|
|
489
|
+
(monotonic() >= deadline || (deadlineLimited && error.timedOut))
|
|
490
|
+
) {
|
|
491
|
+
throw new DOMException(
|
|
492
|
+
`operation ${operationId} status probe exceeded its deadline`,
|
|
493
|
+
"TimeoutError",
|
|
494
|
+
);
|
|
495
|
+
}
|
|
402
496
|
return false;
|
|
403
497
|
}
|
|
498
|
+
if (error instanceof APIStatusError) {
|
|
499
|
+
if (
|
|
500
|
+
error.status === 410 ||
|
|
501
|
+
error.status === 425 ||
|
|
502
|
+
error.status === 429 ||
|
|
503
|
+
error.status >= 500
|
|
504
|
+
) {
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
404
508
|
throw error;
|
|
405
509
|
}
|
|
406
510
|
return status !== undefined && isTerminalStatus(status);
|
|
@@ -410,36 +514,49 @@ export class ContreeClient {
|
|
|
410
514
|
* follows the SSE log until `completion`, then fetches and returns
|
|
411
515
|
* the terminal OperationResponse. */
|
|
412
516
|
async waitOperation(operationId, { timeout = null } = {}) {
|
|
517
|
+
const deadline = timeout === null ? null : monotonic() + timeout;
|
|
413
518
|
// eslint-disable-next-line no-unused-vars
|
|
414
519
|
for await (const event of this.followOperationEvents(operationId, {
|
|
415
520
|
timeout,
|
|
416
521
|
})) {
|
|
417
522
|
// draining the stream is the wait
|
|
418
523
|
}
|
|
419
|
-
|
|
524
|
+
const spec = operations.buildGetOperationStatus(operationId);
|
|
525
|
+
spec.deadline = deadline;
|
|
526
|
+
const deadlineLimited =
|
|
527
|
+
deadline !== null &&
|
|
528
|
+
(this.timeout === null || deadline - monotonic() <= this.timeout);
|
|
529
|
+
try {
|
|
530
|
+
return operations.parseGetOperationStatus(await this.call(spec));
|
|
531
|
+
} catch (error) {
|
|
532
|
+
if (
|
|
533
|
+
deadline !== null &&
|
|
534
|
+
error instanceof APIConnectionError &&
|
|
535
|
+
(monotonic() >= deadline || (deadlineLimited && error.timedOut))
|
|
536
|
+
) {
|
|
537
|
+
throw new DOMException(
|
|
538
|
+
`operation ${operationId} did not complete within ${timeout}s`,
|
|
539
|
+
"TimeoutError",
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
throw error;
|
|
543
|
+
}
|
|
420
544
|
}
|
|
421
545
|
|
|
422
|
-
/** Stream operation events with transparent reconnection
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
* meaning the events endpoint itself doesn't exist for this
|
|
426
|
-
* operation/server (400/404/405/406) stops reconnecting and instead
|
|
427
|
-
* polls getOperationStatus() until the operation is terminal, then
|
|
428
|
-
* yields a synthesized `completion` event built from that status
|
|
429
|
-
* (there is no real event log to relay). Other API errors
|
|
430
|
-
* propagate. Ends after the `completion` event. */
|
|
546
|
+
/** Stream operation events with transparent reconnection.
|
|
547
|
+
* Native stream failures trigger a terminal-status probe and a
|
|
548
|
+
* reconnect from the last event id. */
|
|
431
549
|
async *followOperationEvents(
|
|
432
550
|
operationId,
|
|
433
551
|
{ last_event_id = null, spid = null, since = null, timeout = null } = {},
|
|
434
552
|
) {
|
|
435
553
|
let lastId = last_event_id;
|
|
436
|
-
const delays = retryDelays();
|
|
437
554
|
const deadline = timeout === null ? null : monotonic() + timeout;
|
|
438
555
|
const checkDeadline = () => {
|
|
439
556
|
if (deadline !== null && monotonic() >= deadline) {
|
|
440
|
-
throw new
|
|
441
|
-
0,
|
|
557
|
+
throw new DOMException(
|
|
442
558
|
`operation ${operationId} events did not complete within ${timeout}s`,
|
|
559
|
+
"TimeoutError",
|
|
443
560
|
);
|
|
444
561
|
}
|
|
445
562
|
};
|
|
@@ -462,101 +579,25 @@ export class ContreeClient {
|
|
|
462
579
|
checkDeadline();
|
|
463
580
|
}
|
|
464
581
|
} catch (error) {
|
|
465
|
-
if (error
|
|
466
|
-
if (error.lastEventId !== null) {
|
|
467
|
-
lastId = error.lastEventId;
|
|
468
|
-
}
|
|
469
|
-
} else if (error instanceof ContreeAPIError) {
|
|
470
|
-
if (EVENTS_UNAVAILABLE_STATUSES.has(error.status)) {
|
|
471
|
-
// the endpoint is gone, not just failing: reconnecting the
|
|
472
|
-
// same request will never work, but the operation may
|
|
473
|
-
// still finish - stop touching /events and poll status
|
|
474
|
-
// instead for the rest of this wait
|
|
475
|
-
for (;;) {
|
|
476
|
-
checkDeadline();
|
|
477
|
-
let response;
|
|
478
|
-
try {
|
|
479
|
-
response = await this.getOperationStatus(operationId);
|
|
480
|
-
} catch (pollError) {
|
|
481
|
-
if (
|
|
482
|
-
!(pollError instanceof ContreeError) &&
|
|
483
|
-
!this._transportRetryable(pollError)
|
|
484
|
-
) {
|
|
485
|
-
throw pollError;
|
|
486
|
-
}
|
|
487
|
-
response = null;
|
|
488
|
-
}
|
|
489
|
-
if (
|
|
490
|
-
response !== null &&
|
|
491
|
-
response.status !== undefined &&
|
|
492
|
-
isTerminalStatus(response.status)
|
|
493
|
-
) {
|
|
494
|
-
// no event log to relay, but a caller of
|
|
495
|
-
// followOperationEvents must still observe a terminal
|
|
496
|
-
// completion rather than nothing at all
|
|
497
|
-
lastId = lastId === null ? 0 : lastId + 1;
|
|
498
|
-
yield new OperationEvent({
|
|
499
|
-
id: lastId,
|
|
500
|
-
ts: new Date(),
|
|
501
|
-
type: "completion",
|
|
502
|
-
data: new EventDataCompletion({
|
|
503
|
-
status: response.status,
|
|
504
|
-
duration_ms:
|
|
505
|
-
typeof response.duration === "number"
|
|
506
|
-
? Math.round(response.duration * 1000)
|
|
507
|
-
: 0,
|
|
508
|
-
result_image_uuid: response.result_image_uuid,
|
|
509
|
-
error: response.error,
|
|
510
|
-
image_size_bytes:
|
|
511
|
-
typeof response.image_size === "number"
|
|
512
|
-
? response.image_size
|
|
513
|
-
: undefined,
|
|
514
|
-
}),
|
|
515
|
-
});
|
|
516
|
-
return;
|
|
517
|
-
}
|
|
518
|
-
let pollDelay = delays.next().value;
|
|
519
|
-
if (deadline !== null) {
|
|
520
|
-
pollDelay = Math.min(
|
|
521
|
-
pollDelay,
|
|
522
|
-
Math.max(0, deadline - monotonic()),
|
|
523
|
-
);
|
|
524
|
-
}
|
|
525
|
-
await sleep(pollDelay);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
const retryable =
|
|
529
|
-
error.status === 410 ||
|
|
530
|
-
error.status === 425 ||
|
|
531
|
-
(error.status >= 500 && error.status < 600);
|
|
532
|
-
if (!retryable) {
|
|
533
|
-
throw error;
|
|
534
|
-
}
|
|
535
|
-
if (await this.operationTerminal(operationId)) {
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
let delay =
|
|
539
|
-
error.retryAfter !== null ? error.retryAfter : delays.next().value;
|
|
540
|
-
if (deadline !== null) {
|
|
541
|
-
// a Retry-After must not sleep past the caller's deadline
|
|
542
|
-
delay = Math.min(delay, Math.max(0, deadline - monotonic()));
|
|
543
|
-
}
|
|
544
|
-
await sleep(delay);
|
|
545
|
-
continue;
|
|
546
|
-
} else if (
|
|
547
|
-
!this._transportRetryable(error) ||
|
|
548
|
-
this._transportNonretryable(error)
|
|
549
|
-
) {
|
|
582
|
+
if (error?.name === "AbortError") {
|
|
550
583
|
throw error;
|
|
551
584
|
}
|
|
585
|
+
checkDeadline();
|
|
586
|
+
if (Number.isInteger(error?.lastEventId)) {
|
|
587
|
+
lastId = error.lastEventId;
|
|
588
|
+
}
|
|
552
589
|
}
|
|
553
590
|
// the stream ended or broke without a completion frame: the
|
|
554
591
|
// retry must not outlive the operation itself
|
|
555
|
-
if (await this.operationTerminal(operationId)) {
|
|
592
|
+
if (await this.operationTerminal(operationId, deadline)) {
|
|
556
593
|
return;
|
|
557
594
|
}
|
|
558
595
|
if (lastId === eventsBefore) {
|
|
559
|
-
|
|
596
|
+
let delay = TIGHT_LOOP_FLOOR;
|
|
597
|
+
if (deadline !== null) {
|
|
598
|
+
delay = Math.min(delay, Math.max(0, deadline - monotonic()));
|
|
599
|
+
}
|
|
600
|
+
await sleep(delay);
|
|
560
601
|
}
|
|
561
602
|
}
|
|
562
603
|
}
|
|
@@ -717,7 +758,14 @@ export class ContreeClient {
|
|
|
717
758
|
/** Check if a file exists for the current namespace (HEAD /files/{sha256}) */
|
|
718
759
|
async checkFileExists(sha256) {
|
|
719
760
|
const spec = operations.buildCheckFileExists(sha256);
|
|
720
|
-
|
|
761
|
+
try {
|
|
762
|
+
return operations.parseCheckFileExists(await this.call(spec));
|
|
763
|
+
} catch (error) {
|
|
764
|
+
if (error instanceof NotFoundError) {
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
throw error;
|
|
768
|
+
}
|
|
721
769
|
}
|
|
722
770
|
|
|
723
771
|
/** Spawn a new container instance (POST /instances) */
|
|
@@ -797,12 +845,10 @@ export class ContreeClient {
|
|
|
797
845
|
let lastSeen = options.last_event_id ?? null;
|
|
798
846
|
for await (const chunk of this.stream(spec)) {
|
|
799
847
|
for (const frame of parser.feed(chunk)) {
|
|
800
|
-
const payload = decodeFramePayload(frame, lastSeen);
|
|
801
|
-
// id-only frames advance the resume cursor even
|
|
802
|
-
// though they carry no payload
|
|
803
848
|
if (frame.id !== null) {
|
|
804
849
|
lastSeen = frame.id;
|
|
805
850
|
}
|
|
851
|
+
const payload = decodeFramePayload(frame, lastSeen);
|
|
806
852
|
if (payload === null) {
|
|
807
853
|
continue;
|
|
808
854
|
}
|
|
@@ -875,7 +921,14 @@ export class ContreeClient {
|
|
|
875
921
|
/** Check if a file exists in image (HEAD /inspect/{image_uuid}/download) */
|
|
876
922
|
async checkImageFile(imageUuid, path) {
|
|
877
923
|
const spec = operations.buildCheckImageFile(imageUuid, path);
|
|
878
|
-
|
|
924
|
+
try {
|
|
925
|
+
return operations.parseCheckImageFile(await this.call(spec));
|
|
926
|
+
} catch (error) {
|
|
927
|
+
if (error instanceof NotFoundError) {
|
|
928
|
+
return false;
|
|
929
|
+
}
|
|
930
|
+
throw error;
|
|
931
|
+
}
|
|
879
932
|
}
|
|
880
933
|
|
|
881
934
|
/** Download a file or a directory from image as tar archive (GET /inspect/{image_uuid}/archive) */
|
|
@@ -887,7 +940,14 @@ export class ContreeClient {
|
|
|
887
940
|
/** Check if a path can be archived from image (HEAD /inspect/{image_uuid}/archive) */
|
|
888
941
|
async checkImageArchive(imageUuid, path) {
|
|
889
942
|
const spec = operations.buildCheckImageArchive(imageUuid, path);
|
|
890
|
-
|
|
943
|
+
try {
|
|
944
|
+
return operations.parseCheckImageArchive(await this.call(spec));
|
|
945
|
+
} catch (error) {
|
|
946
|
+
if (error instanceof NotFoundError) {
|
|
947
|
+
return false;
|
|
948
|
+
}
|
|
949
|
+
throw error;
|
|
950
|
+
}
|
|
891
951
|
}
|
|
892
952
|
|
|
893
953
|
/** List files in image (GET /inspect/{image_uuid}/list) */
|
package/lib/errors.d.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
export declare class ContreeError extends Error {
|
|
1
|
+
export declare class ContreeError extends Error {
|
|
2
|
+
constructor(message: string, options?: ErrorOptions);
|
|
3
|
+
}
|
|
2
4
|
|
|
3
|
-
export declare class
|
|
4
|
-
|
|
5
|
-
constructor(
|
|
5
|
+
export declare class APIConnectionError extends ContreeError {
|
|
6
|
+
timedOut: boolean;
|
|
7
|
+
constructor(
|
|
8
|
+
message: string,
|
|
9
|
+
options?: { cause?: unknown; timedOut?: boolean },
|
|
10
|
+
);
|
|
6
11
|
}
|
|
7
12
|
|
|
8
|
-
export declare class
|
|
13
|
+
export declare class APIStatusError extends ContreeError {
|
|
9
14
|
status: number;
|
|
10
15
|
error: unknown;
|
|
11
16
|
traceback: string[] | null;
|
|
@@ -17,14 +22,15 @@ export declare class ContreeAPIError extends ContreeError {
|
|
|
17
22
|
);
|
|
18
23
|
}
|
|
19
24
|
|
|
20
|
-
export declare class BadRequestError extends
|
|
21
|
-
export declare class
|
|
22
|
-
export declare class
|
|
23
|
-
export declare class NotFoundError extends
|
|
24
|
-
export declare class ConflictError extends
|
|
25
|
-
export declare class GoneError extends
|
|
26
|
-
export declare class UnprocessableEntityError extends
|
|
27
|
-
export declare class TooEarlyError extends
|
|
28
|
-
export declare class
|
|
25
|
+
export declare class BadRequestError extends APIStatusError {}
|
|
26
|
+
export declare class AuthenticationError extends APIStatusError {}
|
|
27
|
+
export declare class PermissionDeniedError extends APIStatusError {}
|
|
28
|
+
export declare class NotFoundError extends APIStatusError {}
|
|
29
|
+
export declare class ConflictError extends APIStatusError {}
|
|
30
|
+
export declare class GoneError extends APIStatusError {}
|
|
31
|
+
export declare class UnprocessableEntityError extends APIStatusError {}
|
|
32
|
+
export declare class TooEarlyError extends APIStatusError {}
|
|
33
|
+
export declare class RateLimitError extends APIStatusError {}
|
|
34
|
+
export declare class ServerError extends APIStatusError {}
|
|
29
35
|
|
|
30
|
-
export declare const ERROR_CLASSES: Map<number, typeof
|
|
36
|
+
export declare const ERROR_CLASSES: Map<number, typeof APIStatusError>;
|
package/lib/errors.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
/** Exception hierarchy for the Contree API client. */
|
|
2
2
|
|
|
3
3
|
export class ContreeError extends Error {
|
|
4
|
-
constructor(message) {
|
|
5
|
-
super(message);
|
|
4
|
+
constructor(message, options) {
|
|
5
|
+
super(message, options);
|
|
6
6
|
this.name = new.target.name;
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export class
|
|
11
|
-
constructor(message, {
|
|
12
|
-
super(message);
|
|
13
|
-
this.
|
|
10
|
+
export class APIConnectionError extends ContreeError {
|
|
11
|
+
constructor(message, { cause, timedOut = false } = {}) {
|
|
12
|
+
super(message, { cause });
|
|
13
|
+
this.timedOut = timedOut;
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export class
|
|
17
|
+
export class APIStatusError extends ContreeError {
|
|
18
18
|
constructor(status, error, { traceback = null, retryAfter = null } = {}) {
|
|
19
19
|
super(`HTTP ${status}: ${error}`);
|
|
20
20
|
this.status = status;
|
|
@@ -24,23 +24,25 @@ export class ContreeAPIError extends ContreeError {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export class BadRequestError extends
|
|
28
|
-
export class
|
|
29
|
-
export class
|
|
30
|
-
export class NotFoundError extends
|
|
31
|
-
export class ConflictError extends
|
|
32
|
-
export class GoneError extends
|
|
33
|
-
export class UnprocessableEntityError extends
|
|
34
|
-
export class TooEarlyError extends
|
|
35
|
-
export class
|
|
27
|
+
export class BadRequestError extends APIStatusError {}
|
|
28
|
+
export class AuthenticationError extends APIStatusError {}
|
|
29
|
+
export class PermissionDeniedError extends APIStatusError {}
|
|
30
|
+
export class NotFoundError extends APIStatusError {}
|
|
31
|
+
export class ConflictError extends APIStatusError {}
|
|
32
|
+
export class GoneError extends APIStatusError {}
|
|
33
|
+
export class UnprocessableEntityError extends APIStatusError {}
|
|
34
|
+
export class TooEarlyError extends APIStatusError {}
|
|
35
|
+
export class RateLimitError extends APIStatusError {}
|
|
36
|
+
export class ServerError extends APIStatusError {}
|
|
36
37
|
|
|
37
38
|
export const ERROR_CLASSES = new Map([
|
|
38
39
|
[400, BadRequestError],
|
|
39
|
-
[401,
|
|
40
|
-
[403,
|
|
40
|
+
[401, AuthenticationError],
|
|
41
|
+
[403, PermissionDeniedError],
|
|
41
42
|
[404, NotFoundError],
|
|
42
43
|
[409, ConflictError],
|
|
43
44
|
[410, GoneError],
|
|
44
45
|
[422, UnprocessableEntityError],
|
|
45
46
|
[425, TooEarlyError],
|
|
47
|
+
[429, RateLimitError],
|
|
46
48
|
]);
|
package/lib/index.d.ts
CHANGED
|
@@ -8,9 +8,13 @@ export {
|
|
|
8
8
|
RETRY_DELAYS,
|
|
9
9
|
RetryPolicy,
|
|
10
10
|
SSEParser,
|
|
11
|
+
base64ToBytes,
|
|
12
|
+
bytesToBase64,
|
|
13
|
+
bytesToText,
|
|
11
14
|
parseDatetime,
|
|
12
15
|
parseRetryAfter,
|
|
13
16
|
sha256,
|
|
17
|
+
textToBytes,
|
|
14
18
|
} from "./runtime.js";
|
|
15
19
|
export type { RequestSpec, ResponseData, RequestBody } from "./runtime.js";
|
|
16
20
|
export { ContreeClient, ContreeClientOptions } from "./client.js";
|
package/lib/index.js
CHANGED
|
@@ -8,9 +8,13 @@ export {
|
|
|
8
8
|
RETRY_DELAYS,
|
|
9
9
|
RetryPolicy,
|
|
10
10
|
SSEParser,
|
|
11
|
+
base64ToBytes,
|
|
12
|
+
bytesToBase64,
|
|
13
|
+
bytesToText,
|
|
11
14
|
parseDatetime,
|
|
12
15
|
parseRetryAfter,
|
|
13
16
|
sha256,
|
|
17
|
+
textToBytes,
|
|
14
18
|
} from "./runtime.js";
|
|
15
19
|
export { ContreeClient } from "./client.js";
|
|
16
20
|
export * as operations from "./operations.js";
|
package/lib/operations.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// Generated by codegen from the OpenAPI spec - do not edit.
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
errorForResponse,
|
|
5
4
|
formatTimeParam,
|
|
6
5
|
jsonArray,
|
|
7
6
|
jsonObject,
|
|
@@ -67,7 +66,7 @@ export function parseListImages(response) {
|
|
|
67
66
|
if (response.status >= 200 && response.status < 300) {
|
|
68
67
|
return ImageListResponse.fromWire(jsonObject(response));
|
|
69
68
|
}
|
|
70
|
-
throw
|
|
69
|
+
throw new RangeError(`unexpected HTTP status ${response.status}`);
|
|
71
70
|
}
|
|
72
71
|
|
|
73
72
|
/** Build the request for `DELETE /images/{imageUUID}/tag`. */
|
|
@@ -89,7 +88,7 @@ export function parseDeleteImageTag(response) {
|
|
|
89
88
|
if (response.status >= 200 && response.status < 300) {
|
|
90
89
|
return null;
|
|
91
90
|
}
|
|
92
|
-
throw
|
|
91
|
+
throw new RangeError(`unexpected HTTP status ${response.status}`);
|
|
93
92
|
}
|
|
94
93
|
|
|
95
94
|
/** Build the request for `PATCH /images/{imageUUID}/tag`. */
|
|
@@ -110,7 +109,7 @@ export function parseUpdateImageTag(response) {
|
|
|
110
109
|
if (response.status >= 200 && response.status < 300) {
|
|
111
110
|
return Image.fromWire(jsonObject(response));
|
|
112
111
|
}
|
|
113
|
-
throw
|
|
112
|
+
throw new RangeError(`unexpected HTTP status ${response.status}`);
|
|
114
113
|
}
|
|
115
114
|
|
|
116
115
|
/** Build the request for `POST /images/import`. */
|
|
@@ -130,7 +129,7 @@ export function parseImportImage(response) {
|
|
|
130
129
|
if (response.status >= 200 && response.status < 300) {
|
|
131
130
|
return String(jsonObject(response)["uuid"]);
|
|
132
131
|
}
|
|
133
|
-
throw
|
|
132
|
+
throw new RangeError(`unexpected HTTP status ${response.status}`);
|
|
134
133
|
}
|
|
135
134
|
|
|
136
135
|
/** Build the request for `GET /files`. */
|
|
@@ -161,7 +160,7 @@ export function parseListFiles(response) {
|
|
|
161
160
|
if (response.status >= 200 && response.status < 300) {
|
|
162
161
|
return FilesListResponse.fromWire(jsonObject(response));
|
|
163
162
|
}
|
|
164
|
-
throw
|
|
163
|
+
throw new RangeError(`unexpected HTTP status ${response.status}`);
|
|
165
164
|
}
|
|
166
165
|
|
|
167
166
|
/** Build the request for `POST /files`. */
|
|
@@ -180,7 +179,7 @@ export function parseUploadFile(response) {
|
|
|
180
179
|
if (response.status >= 200 && response.status < 300) {
|
|
181
180
|
return FileResponse.fromWire(jsonObject(response));
|
|
182
181
|
}
|
|
183
|
-
throw
|
|
182
|
+
throw new RangeError(`unexpected HTTP status ${response.status}`);
|
|
184
183
|
}
|
|
185
184
|
|
|
186
185
|
/** Build the request for `GET /files/{sha256}`. */
|
|
@@ -197,7 +196,7 @@ export function parseGetFile(response) {
|
|
|
197
196
|
if (response.status >= 200 && response.status < 300) {
|
|
198
197
|
return File.fromWire(jsonObject(response));
|
|
199
198
|
}
|
|
200
|
-
throw
|
|
199
|
+
throw new RangeError(`unexpected HTTP status ${response.status}`);
|
|
201
200
|
}
|
|
202
201
|
|
|
203
202
|
/** Build the request for `HEAD /files/{sha256}`. */
|
|
@@ -214,10 +213,7 @@ export function parseCheckFileExists(response) {
|
|
|
214
213
|
if (response.status >= 200 && response.status < 300) {
|
|
215
214
|
return true;
|
|
216
215
|
}
|
|
217
|
-
|
|
218
|
-
return false;
|
|
219
|
-
}
|
|
220
|
-
throw errorForResponse(response);
|
|
216
|
+
throw new RangeError(`unexpected HTTP status ${response.status}`);
|
|
221
217
|
}
|
|
222
218
|
|
|
223
219
|
/** Build the request for `POST /instances`. */
|
|
@@ -275,7 +271,7 @@ export function parseSpawnInstance(response) {
|
|
|
275
271
|
if (response.status >= 200 && response.status < 300) {
|
|
276
272
|
return InstanceSpawnResponse.fromWire(jsonObject(response));
|
|
277
273
|
}
|
|
278
|
-
throw
|
|
274
|
+
throw new RangeError(`unexpected HTTP status ${response.status}`);
|
|
279
275
|
}
|
|
280
276
|
|
|
281
277
|
/** Build the request for `GET /operations`. */
|
|
@@ -314,7 +310,7 @@ export function parseListOperations(response) {
|
|
|
314
310
|
if (response.status >= 200 && response.status < 300) {
|
|
315
311
|
return jsonArray(response).map((item) => OperationSummary.fromWire(item));
|
|
316
312
|
}
|
|
317
|
-
throw
|
|
313
|
+
throw new RangeError(`unexpected HTTP status ${response.status}`);
|
|
318
314
|
}
|
|
319
315
|
|
|
320
316
|
/** Build the request for `GET /operations/{operationId}`. */
|
|
@@ -339,7 +335,7 @@ export function parseGetOperationStatus(response) {
|
|
|
339
335
|
if (response.status >= 200 && response.status < 300) {
|
|
340
336
|
return OperationResponse.fromWire(jsonObject(response));
|
|
341
337
|
}
|
|
342
|
-
throw
|
|
338
|
+
throw new RangeError(`unexpected HTTP status ${response.status}`);
|
|
343
339
|
}
|
|
344
340
|
|
|
345
341
|
/** Build the request for `DELETE /operations/{operationId}`. */
|
|
@@ -356,7 +352,7 @@ export function parseCancelOperation(response) {
|
|
|
356
352
|
if (response.status >= 200 && response.status < 300) {
|
|
357
353
|
return null;
|
|
358
354
|
}
|
|
359
|
-
throw
|
|
355
|
+
throw new RangeError(`unexpected HTTP status ${response.status}`);
|
|
360
356
|
}
|
|
361
357
|
|
|
362
358
|
/** Build the request for `GET /operations/{operationId}/events`. */
|
|
@@ -420,7 +416,7 @@ export function parseOperationSubprocessCreate(response) {
|
|
|
420
416
|
if (response.status >= 200 && response.status < 300) {
|
|
421
417
|
return Number(jsonObject(response)["spid"]);
|
|
422
418
|
}
|
|
423
|
-
throw
|
|
419
|
+
throw new RangeError(`unexpected HTTP status ${response.status}`);
|
|
424
420
|
}
|
|
425
421
|
|
|
426
422
|
/** Build the request for `GET /operations/{operationId}/subprocesses/{spid}`. */
|
|
@@ -437,7 +433,7 @@ export function parseOperationSubprocess(response) {
|
|
|
437
433
|
if (response.status >= 200 && response.status < 300) {
|
|
438
434
|
return InstanceResult.fromWire(jsonObject(response));
|
|
439
435
|
}
|
|
440
|
-
throw
|
|
436
|
+
throw new RangeError(`unexpected HTTP status ${response.status}`);
|
|
441
437
|
}
|
|
442
438
|
|
|
443
439
|
/** Build the request for `DELETE /operations/{operationId}/subprocesses/{spid}`. */
|
|
@@ -463,7 +459,7 @@ export function parseOperationSubprocessKill(response) {
|
|
|
463
459
|
if (response.status >= 200 && response.status < 300) {
|
|
464
460
|
return null;
|
|
465
461
|
}
|
|
466
|
-
throw
|
|
462
|
+
throw new RangeError(`unexpected HTTP status ${response.status}`);
|
|
467
463
|
}
|
|
468
464
|
|
|
469
465
|
/** Build the request for `POST /operations/{operationId}/subprocesses/{spid}/stdin`. */
|
|
@@ -494,7 +490,7 @@ export function parseOperationSubprocessStdin(response) {
|
|
|
494
490
|
if (response.status >= 200 && response.status < 300) {
|
|
495
491
|
return null;
|
|
496
492
|
}
|
|
497
|
-
throw
|
|
493
|
+
throw new RangeError(`unexpected HTTP status ${response.status}`);
|
|
498
494
|
}
|
|
499
495
|
|
|
500
496
|
/** Build the request for `GET /inspect/`. */
|
|
@@ -522,7 +518,7 @@ export function parseInspectFindImageByTag(response) {
|
|
|
522
518
|
const path = new URL(response.url).pathname;
|
|
523
519
|
return path.replace(/\/+$/, "").split("/").pop();
|
|
524
520
|
}
|
|
525
|
-
throw
|
|
521
|
+
throw new RangeError(`unexpected HTTP status ${response.status}`);
|
|
526
522
|
}
|
|
527
523
|
|
|
528
524
|
/** Build the request for `GET /inspect/{image_uuid}/`. */
|
|
@@ -539,7 +535,7 @@ export function parseInspectImage(response) {
|
|
|
539
535
|
if (response.status >= 200 && response.status < 300) {
|
|
540
536
|
return Image.fromWire(jsonObject(response));
|
|
541
537
|
}
|
|
542
|
-
throw
|
|
538
|
+
throw new RangeError(`unexpected HTTP status ${response.status}`);
|
|
543
539
|
}
|
|
544
540
|
|
|
545
541
|
/** Build the request for `GET /inspect/{image_uuid}/download`. */
|
|
@@ -559,7 +555,7 @@ export function parseInspectImageDownload(response) {
|
|
|
559
555
|
if (response.status >= 200 && response.status < 300) {
|
|
560
556
|
return response.body;
|
|
561
557
|
}
|
|
562
|
-
throw
|
|
558
|
+
throw new RangeError(`unexpected HTTP status ${response.status}`);
|
|
563
559
|
}
|
|
564
560
|
|
|
565
561
|
/** Build the request for `HEAD /inspect/{image_uuid}/download`. */
|
|
@@ -579,10 +575,7 @@ export function parseCheckImageFile(response) {
|
|
|
579
575
|
if (response.status >= 200 && response.status < 300) {
|
|
580
576
|
return true;
|
|
581
577
|
}
|
|
582
|
-
|
|
583
|
-
return false;
|
|
584
|
-
}
|
|
585
|
-
throw errorForResponse(response);
|
|
578
|
+
throw new RangeError(`unexpected HTTP status ${response.status}`);
|
|
586
579
|
}
|
|
587
580
|
|
|
588
581
|
/** Build the request for `GET /inspect/{image_uuid}/archive`. */
|
|
@@ -614,10 +607,7 @@ export function parseCheckImageArchive(response) {
|
|
|
614
607
|
if (response.status >= 200 && response.status < 300) {
|
|
615
608
|
return true;
|
|
616
609
|
}
|
|
617
|
-
|
|
618
|
-
return false;
|
|
619
|
-
}
|
|
620
|
-
throw errorForResponse(response);
|
|
610
|
+
throw new RangeError(`unexpected HTTP status ${response.status}`);
|
|
621
611
|
}
|
|
622
612
|
|
|
623
613
|
/** Build the request for `GET /inspect/{image_uuid}/list`. */
|
|
@@ -637,7 +627,7 @@ export function parseInspectImageList(response) {
|
|
|
637
627
|
if (response.status >= 200 && response.status < 300) {
|
|
638
628
|
return DirectoryList.fromWire(jsonObject(response));
|
|
639
629
|
}
|
|
640
|
-
throw
|
|
630
|
+
throw new RangeError(`unexpected HTTP status ${response.status}`);
|
|
641
631
|
}
|
|
642
632
|
|
|
643
633
|
/** Build the request for `GET /inspect/{image_uuid}/grep`. */
|
|
@@ -690,7 +680,7 @@ export function parseInspectImageGrep(response) {
|
|
|
690
680
|
if (response.status >= 200 && response.status < 300) {
|
|
691
681
|
return GrepResult.fromWire(jsonObject(response));
|
|
692
682
|
}
|
|
693
|
-
throw
|
|
683
|
+
throw new RangeError(`unexpected HTTP status ${response.status}`);
|
|
694
684
|
}
|
|
695
685
|
|
|
696
686
|
/** Build the request for `GET /whoami`. */
|
|
@@ -703,5 +693,5 @@ export function parseWhoami(response) {
|
|
|
703
693
|
if (response.status >= 200 && response.status < 300) {
|
|
704
694
|
return WhoAmIResponse.fromWire(jsonObject(response));
|
|
705
695
|
}
|
|
706
|
-
throw
|
|
696
|
+
throw new RangeError(`unexpected HTTP status ${response.status}`);
|
|
707
697
|
}
|
package/lib/profiles.d.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { ContreeError } from "./errors.js";
|
|
2
|
-
|
|
3
1
|
export declare const AUTH_TYPE_IAM: "iam";
|
|
4
2
|
export declare const AUTH_TYPE_JWT: "jwt";
|
|
5
3
|
export declare const PROFILE_PREFIX: "profile:";
|
|
6
4
|
export declare const DEFAULT_PROFILE: "default";
|
|
7
5
|
|
|
8
|
-
export declare class ProfileError extends
|
|
6
|
+
export declare class ProfileError extends Error {}
|
|
9
7
|
|
|
10
8
|
export interface Profile {
|
|
11
9
|
name: string;
|
package/lib/profiles.js
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
* loaded lazily so browser bundlers never pull them in.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { ContreeError } from "./errors.js";
|
|
11
10
|
import { DEFAULT_BASE_URL } from "./specInfo.js";
|
|
12
11
|
|
|
13
12
|
export const AUTH_TYPE_IAM = "iam";
|
|
@@ -15,7 +14,7 @@ export const AUTH_TYPE_JWT = "jwt";
|
|
|
15
14
|
export const PROFILE_PREFIX = "profile:";
|
|
16
15
|
export const DEFAULT_PROFILE = "default";
|
|
17
16
|
|
|
18
|
-
export class ProfileError extends
|
|
17
|
+
export class ProfileError extends Error {}
|
|
19
18
|
|
|
20
19
|
function record() {
|
|
21
20
|
// a null-prototype object: keys come from an external file, and a
|
package/lib/runtime.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export interface RequestSpec {
|
|
|
28
28
|
accept?: string | null;
|
|
29
29
|
idempotent?: boolean;
|
|
30
30
|
redirect?: "manual" | "follow";
|
|
31
|
-
/** absolute deadline in monotonic seconds; bounds
|
|
31
|
+
/** absolute deadline in monotonic seconds; bounds transport waits */
|
|
32
32
|
deadline?: number | null;
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -53,7 +53,6 @@ export declare class RetryPolicy {
|
|
|
53
53
|
export declare function parseRetryAfter(
|
|
54
54
|
value: string | null | undefined,
|
|
55
55
|
): number | null;
|
|
56
|
-
export declare function retryAfterDelay(response: ResponseData): number | null;
|
|
57
56
|
export declare function sleep(seconds: number): Promise<void>;
|
|
58
57
|
export declare function monotonic(): number;
|
|
59
58
|
export declare function isUuid(ref: string): boolean;
|
|
@@ -75,7 +74,11 @@ export declare function jsonObject(
|
|
|
75
74
|
response: ResponseData,
|
|
76
75
|
): Record<string, unknown>;
|
|
77
76
|
export declare function jsonArray(response: ResponseData): unknown[];
|
|
78
|
-
export declare function
|
|
77
|
+
export declare function responseErrorDetails(response: ResponseData): {
|
|
78
|
+
error: unknown;
|
|
79
|
+
traceback: string[] | null;
|
|
80
|
+
retryAfter: number | null;
|
|
81
|
+
};
|
|
79
82
|
|
|
80
83
|
export interface SSEFrame {
|
|
81
84
|
id: number | null;
|
package/lib/runtime.js
CHANGED
|
@@ -5,16 +5,9 @@
|
|
|
5
5
|
* fetch itself, so only the protocol logic lives here.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
ContreeAPIError,
|
|
10
|
-
ERROR_CLASSES,
|
|
11
|
-
ServerError,
|
|
12
|
-
SSEStreamError,
|
|
13
|
-
} from "./errors.js";
|
|
14
|
-
|
|
15
8
|
export const CHUNK_SIZE = 65536;
|
|
16
9
|
|
|
17
|
-
export const PACKAGE_VERSION = "0.
|
|
10
|
+
export const PACKAGE_VERSION = "0.4.0";
|
|
18
11
|
export const UA_PRODUCT = `contree-client-js/${PACKAGE_VERSION}`;
|
|
19
12
|
|
|
20
13
|
const NODE_VERSION =
|
|
@@ -38,17 +31,6 @@ export const RETRY_DELAYS = Object.freeze([0.1, 0.2, 0.5, 1.0, 2.0, 5.0]);
|
|
|
38
31
|
// returning immediate empty streams does not spin the client
|
|
39
32
|
export const TIGHT_LOOP_FLOOR = 0.5;
|
|
40
33
|
|
|
41
|
-
// the events route itself doesn't exist for this operation/server
|
|
42
|
-
// (malformed request an older backend rejects, or a reverse proxy
|
|
43
|
-
// that never forwards it) rather than merely being down: reconnecting
|
|
44
|
-
// will never succeed, but the operation itself may still complete -
|
|
45
|
-
// degrade to polling instead of failing the whole wait. 401/403 are
|
|
46
|
-
// deliberately excluded: an auth/permission failure here likely means
|
|
47
|
-
// the whole client is broken, not just this route, so it still throws
|
|
48
|
-
export const EVENTS_UNAVAILABLE_STATUSES = Object.freeze(
|
|
49
|
-
new Set([400, 404, 405, 406]),
|
|
50
|
-
);
|
|
51
|
-
|
|
52
34
|
/** An endless ladder of backoff delays: the ladder is walked once and
|
|
53
35
|
* then the tail delay repeats forever. */
|
|
54
36
|
export function* retryDelays(delays = RETRY_DELAYS) {
|
|
@@ -125,10 +107,6 @@ export function parseRetryAfter(value) {
|
|
|
125
107
|
return Math.max(0, (moment - Date.now()) / 1000);
|
|
126
108
|
}
|
|
127
109
|
|
|
128
|
-
export function retryAfterDelay(response) {
|
|
129
|
-
return parseRetryAfter(response.headers["retry-after"] ?? null);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
110
|
export function sleep(seconds) {
|
|
133
111
|
return new Promise((resolve) => setTimeout(resolve, seconds * 1000));
|
|
134
112
|
}
|
|
@@ -266,7 +244,9 @@ export function jsonBody(response) {
|
|
|
266
244
|
export function jsonObject(response) {
|
|
267
245
|
const data = jsonBody(response);
|
|
268
246
|
if (data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
269
|
-
|
|
247
|
+
const type =
|
|
248
|
+
data === null ? "null" : Array.isArray(data) ? "array" : typeof data;
|
|
249
|
+
throw new TypeError(`expected a JSON object, got ${type}`);
|
|
270
250
|
}
|
|
271
251
|
return data;
|
|
272
252
|
}
|
|
@@ -274,13 +254,14 @@ export function jsonObject(response) {
|
|
|
274
254
|
export function jsonArray(response) {
|
|
275
255
|
const data = jsonBody(response);
|
|
276
256
|
if (!Array.isArray(data)) {
|
|
277
|
-
|
|
257
|
+
const type = data === null ? "null" : typeof data;
|
|
258
|
+
throw new TypeError(`expected a JSON array, got ${type}`);
|
|
278
259
|
}
|
|
279
260
|
return data;
|
|
280
261
|
}
|
|
281
262
|
|
|
282
|
-
/**
|
|
283
|
-
export function
|
|
263
|
+
/** Extract diagnostic fields from an unsuccessful response. */
|
|
264
|
+
export function responseErrorDetails(response) {
|
|
284
265
|
let error = bytesToText(response.body);
|
|
285
266
|
let traceback = null;
|
|
286
267
|
let payload = null;
|
|
@@ -301,11 +282,7 @@ export function errorForResponse(response) {
|
|
|
301
282
|
}
|
|
302
283
|
const parsedRetry = parseRetryAfter(response.headers["retry-after"] ?? null);
|
|
303
284
|
const retryAfter = parsedRetry === null ? null : Math.trunc(parsedRetry);
|
|
304
|
-
|
|
305
|
-
if (cls === undefined) {
|
|
306
|
-
cls = response.status >= 500 ? ServerError : ContreeAPIError;
|
|
307
|
-
}
|
|
308
|
-
return new cls(response.status, error, { traceback, retryAfter });
|
|
285
|
+
return { error, traceback, retryAfter };
|
|
309
286
|
}
|
|
310
287
|
|
|
311
288
|
/** Incremental sans-io parser for `text/event-stream` bytes.
|
|
@@ -334,7 +311,7 @@ export class SSEParser {
|
|
|
334
311
|
// accumulated so far: many short data lines must not grow the
|
|
335
312
|
// pending event unboundedly
|
|
336
313
|
if (this.buffer.length + this.pendingSize > SSEParser.MAX_BUFFER) {
|
|
337
|
-
throw new
|
|
314
|
+
throw new RangeError(
|
|
338
315
|
`SSE frame exceeds ${SSEParser.MAX_BUFFER} bytes before completion`,
|
|
339
316
|
);
|
|
340
317
|
}
|
|
@@ -398,30 +375,15 @@ export class SSEParser {
|
|
|
398
375
|
}
|
|
399
376
|
}
|
|
400
377
|
|
|
401
|
-
/** Decode an SSE frame's JSON payload.
|
|
402
|
-
*
|
|
403
|
-
* Raises SSEStreamError for in-band `sse_error` frames, carrying
|
|
404
|
-
* *lastEventId* so the caller can reconnect from that point; returns
|
|
405
|
-
* null for frames that carry no event payload. The caller turns the
|
|
406
|
-
* plain object into a typed OperationEvent.
|
|
407
|
-
*/
|
|
378
|
+
/** Decode an SSE frame's JSON payload. */
|
|
408
379
|
export function decodeFramePayload(frame, lastEventId = null) {
|
|
409
380
|
if (frame.event === "sse_error") {
|
|
410
|
-
throw new
|
|
381
|
+
throw Object.assign(new Error(frame.data), { lastEventId });
|
|
411
382
|
}
|
|
412
383
|
if (!frame.data) {
|
|
413
384
|
return null;
|
|
414
385
|
}
|
|
415
|
-
|
|
416
|
-
try {
|
|
417
|
-
payload = JSON.parse(frame.data);
|
|
418
|
-
} catch (error) {
|
|
419
|
-
// surface protocol corruption through the SSE error channel so
|
|
420
|
-
// followOperationEvents reconnects instead of crashing
|
|
421
|
-
throw new SSEStreamError(`malformed SSE event payload: ${error}`, {
|
|
422
|
-
lastEventId,
|
|
423
|
-
});
|
|
424
|
-
}
|
|
386
|
+
const payload = JSON.parse(frame.data);
|
|
425
387
|
if (
|
|
426
388
|
payload === null ||
|
|
427
389
|
typeof payload !== "object" ||
|
package/lib/testing.js
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { ContreeClient as GeneratedClient } from "./client.js";
|
|
10
|
-
import { ContreeError } from "./errors.js";
|
|
11
10
|
|
|
12
11
|
export const RESERVED = new Set([
|
|
13
12
|
"constructor",
|
|
@@ -31,7 +30,7 @@ function apiMethodNames() {
|
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
function unmocked(operation) {
|
|
34
|
-
return new
|
|
33
|
+
return new Error(
|
|
35
34
|
`no mock configured for ${operation}(); arm it with` +
|
|
36
35
|
` client.mock(${JSON.stringify(operation)}, result)`,
|
|
37
36
|
);
|
|
@@ -94,9 +93,7 @@ export class ContreeClient extends GeneratedClient {
|
|
|
94
93
|
/** Queue an outcome for *operation*; the last one queued is sticky. */
|
|
95
94
|
mock(operation, result = null, { error = null } = {}) {
|
|
96
95
|
if (typeof this[operation] !== "function" || RESERVED.has(operation)) {
|
|
97
|
-
throw new
|
|
98
|
-
`unknown API operation ${JSON.stringify(operation)}`,
|
|
99
|
-
);
|
|
96
|
+
throw new TypeError(`unknown API operation ${JSON.stringify(operation)}`);
|
|
100
97
|
}
|
|
101
98
|
const queue = this.mocks.get(operation) ?? [];
|
|
102
99
|
queue.push({ result, error });
|