contree-client 0.1.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/LICENSE +177 -0
- package/README.md +131 -0
- package/lib/client.d.ts +181 -0
- package/lib/client.js +786 -0
- package/lib/errors.d.ts +30 -0
- package/lib/errors.js +46 -0
- package/lib/index.d.ts +19 -0
- package/lib/index.js +18 -0
- package/lib/models.d.ts +811 -0
- package/lib/models.js +2173 -0
- package/lib/operations.d.ts +193 -0
- package/lib/operations.js +543 -0
- package/lib/profiles.d.ts +31 -0
- package/lib/profiles.js +159 -0
- package/lib/runtime.d.ts +93 -0
- package/lib/runtime.js +415 -0
- package/lib/specInfo.d.ts +4 -0
- package/lib/specInfo.js +8 -0
- package/lib/testing.d.ts +37 -0
- package/lib/testing.js +122 -0
- package/package.json +35 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
// Generated by codegen from the OpenAPI spec - do not edit.
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ContreeAPIError,
|
|
5
|
+
ContreeError,
|
|
6
|
+
NotFoundError,
|
|
7
|
+
SSEStreamError,
|
|
8
|
+
} from "./errors.js";
|
|
9
|
+
import { AUTH_TYPE_IAM, ProfileError, resolveProfile } from "./profiles.js";
|
|
10
|
+
import {
|
|
11
|
+
IS_NODE,
|
|
12
|
+
SSEParser,
|
|
13
|
+
TIGHT_LOOP_FLOOR,
|
|
14
|
+
UA_PLATFORM,
|
|
15
|
+
UA_PRODUCT,
|
|
16
|
+
UA_RUNTIME,
|
|
17
|
+
decodeFramePayload,
|
|
18
|
+
encodeQuery,
|
|
19
|
+
errorForResponse,
|
|
20
|
+
monotonic,
|
|
21
|
+
retryAfterDelay,
|
|
22
|
+
retryDelays,
|
|
23
|
+
sha256,
|
|
24
|
+
isUuid,
|
|
25
|
+
sleep,
|
|
26
|
+
} from "./runtime.js";
|
|
27
|
+
import { DEFAULT_BASE_URL } from "./specInfo.js";
|
|
28
|
+
import { OperationEvent, isTerminalStatus } from "./models.js";
|
|
29
|
+
import * as operations from "./operations.js";
|
|
30
|
+
|
|
31
|
+
/** Contree API client on top of the platform fetch.
|
|
32
|
+
*
|
|
33
|
+
* fetch pools keepalive connections and decodes gzip on its own, so
|
|
34
|
+
* unlike the Python adapters there is exactly one transport. Pass a
|
|
35
|
+
* custom `fetch` implementation (undici dispatcher wrappers, MSW,
|
|
36
|
+
* ...) via the constructor options to customize it.
|
|
37
|
+
*/
|
|
38
|
+
export class ContreeClient {
|
|
39
|
+
constructor(
|
|
40
|
+
token,
|
|
41
|
+
{
|
|
42
|
+
baseUrl = DEFAULT_BASE_URL,
|
|
43
|
+
project = null,
|
|
44
|
+
timeout = 300,
|
|
45
|
+
retry = null,
|
|
46
|
+
identity = null,
|
|
47
|
+
fetch: fetchImpl = null,
|
|
48
|
+
} = {},
|
|
49
|
+
) {
|
|
50
|
+
// a typo like "htps://" must not silently degrade somewhere else
|
|
51
|
+
const parsed = new URL(baseUrl);
|
|
52
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
53
|
+
throw new RangeError(
|
|
54
|
+
`unsupported baseUrl scheme ${parsed.protocol} in ${baseUrl}: use http:// or https://`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
this.token = token;
|
|
58
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
59
|
+
this.project = project;
|
|
60
|
+
this.timeout = timeout;
|
|
61
|
+
this.retry = retry;
|
|
62
|
+
this.identity = identity;
|
|
63
|
+
this._fetch = fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Create a client from a saved Contree profile (Node). */
|
|
67
|
+
static async fromProfile(
|
|
68
|
+
profile = null,
|
|
69
|
+
{ configPath = null, ...options } = {},
|
|
70
|
+
) {
|
|
71
|
+
const resolved =
|
|
72
|
+
profile !== null && typeof profile === "object"
|
|
73
|
+
? profile
|
|
74
|
+
: await resolveProfile(profile, { path: configPath });
|
|
75
|
+
if (!resolved.token || !resolved.token.trim()) {
|
|
76
|
+
throw new ProfileError(
|
|
77
|
+
`profile ${JSON.stringify(resolved.name)} has no token`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
if (!resolved.url && resolved.authType !== AUTH_TYPE_IAM) {
|
|
81
|
+
throw new ProfileError(
|
|
82
|
+
`profile ${JSON.stringify(resolved.name)} has no URL`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return new this(resolved.token, {
|
|
86
|
+
baseUrl: resolved.url || DEFAULT_BASE_URL,
|
|
87
|
+
project: resolved.project ?? null,
|
|
88
|
+
...options,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Compose the User-Agent from the product tokens; the caller's
|
|
93
|
+
* `identity` leads. Browsers forbid sending the header, so it is
|
|
94
|
+
* only attached in Node. */
|
|
95
|
+
userAgent() {
|
|
96
|
+
return [this.identity, UA_PRODUCT, UA_RUNTIME, UA_PLATFORM]
|
|
97
|
+
.filter(Boolean)
|
|
98
|
+
.join(" ");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
buildUrl(spec) {
|
|
102
|
+
let url = `${this.baseUrl}/v1${spec.path}`;
|
|
103
|
+
if (spec.query && Object.keys(spec.query).length) {
|
|
104
|
+
url = `${url}?${encodeQuery(spec.query)}`;
|
|
105
|
+
}
|
|
106
|
+
return url;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
buildHeaders(spec) {
|
|
110
|
+
const headers = { Authorization: `Bearer ${this.token}` };
|
|
111
|
+
if (this.project !== null) {
|
|
112
|
+
headers["Project"] = this.project;
|
|
113
|
+
}
|
|
114
|
+
if (spec.contentType) {
|
|
115
|
+
headers["Content-Type"] = spec.contentType;
|
|
116
|
+
}
|
|
117
|
+
if (spec.accept) {
|
|
118
|
+
headers["Accept"] = spec.accept;
|
|
119
|
+
}
|
|
120
|
+
Object.assign(headers, spec.headers ?? {});
|
|
121
|
+
if (IS_NODE && !("User-Agent" in headers)) {
|
|
122
|
+
headers["User-Agent"] = this.userAgent();
|
|
123
|
+
}
|
|
124
|
+
return headers;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
_fetchOptions(spec, signal) {
|
|
128
|
+
const options = {
|
|
129
|
+
method: spec.method,
|
|
130
|
+
headers: this.buildHeaders(spec),
|
|
131
|
+
redirect: spec.redirect ?? "manual",
|
|
132
|
+
signal,
|
|
133
|
+
};
|
|
134
|
+
if (spec.body !== undefined && spec.body !== null) {
|
|
135
|
+
options.body = spec.body;
|
|
136
|
+
if (
|
|
137
|
+
typeof ReadableStream !== "undefined" &&
|
|
138
|
+
spec.body instanceof ReadableStream
|
|
139
|
+
) {
|
|
140
|
+
options.duplex = "half";
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return options;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Execute the request and return the buffered response. */
|
|
147
|
+
async request(spec) {
|
|
148
|
+
const controller = new AbortController();
|
|
149
|
+
// an explicit timer, not AbortSignal.timeout(): Node unrefs that
|
|
150
|
+
// timer, so it can never fire when nothing else keeps the event
|
|
151
|
+
// loop alive (custom fetch transports, for one)
|
|
152
|
+
const timer =
|
|
153
|
+
this.timeout !== null
|
|
154
|
+
? setTimeout(
|
|
155
|
+
() =>
|
|
156
|
+
controller.abort(
|
|
157
|
+
new DOMException("request timed out", "TimeoutError"),
|
|
158
|
+
),
|
|
159
|
+
this.timeout * 1000,
|
|
160
|
+
)
|
|
161
|
+
: null;
|
|
162
|
+
try {
|
|
163
|
+
const response = await this._fetch(
|
|
164
|
+
this.buildUrl(spec),
|
|
165
|
+
this._fetchOptions(spec, controller.signal),
|
|
166
|
+
);
|
|
167
|
+
const body = new Uint8Array(await response.arrayBuffer());
|
|
168
|
+
const headers = {};
|
|
169
|
+
response.headers.forEach((value, key) => {
|
|
170
|
+
headers[key.toLowerCase()] = value;
|
|
171
|
+
});
|
|
172
|
+
return { status: response.status, headers, body, url: response.url };
|
|
173
|
+
} finally {
|
|
174
|
+
if (timer !== null) {
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** fetch reports network failures as TypeError; timeouts abort. */
|
|
181
|
+
_transportRetryable(error) {
|
|
182
|
+
return error instanceof TypeError;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
_transportNonretryable(error) {
|
|
186
|
+
return (
|
|
187
|
+
error instanceof DOMException &&
|
|
188
|
+
(error.name === "AbortError" || error.name === "TimeoutError")
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** One transparent reconnect for a replayable idempotent request:
|
|
193
|
+
* fetch pools keep-alive sockets and cannot pre-check them the way
|
|
194
|
+
* the Python adapters do, so the first request on a socket the
|
|
195
|
+
* server already closed fails without ever reaching the server. */
|
|
196
|
+
async _reconnecting(spec) {
|
|
197
|
+
try {
|
|
198
|
+
return await this.request(spec);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (
|
|
201
|
+
!spec.idempotent ||
|
|
202
|
+
(typeof ReadableStream !== "undefined" &&
|
|
203
|
+
spec.body instanceof ReadableStream) ||
|
|
204
|
+
!this._transportRetryable(error) ||
|
|
205
|
+
this._transportNonretryable(error)
|
|
206
|
+
) {
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
return await this.request(spec);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Execute a buffered request, retrying per the client policy. */
|
|
214
|
+
async call(spec) {
|
|
215
|
+
const policy = this.retry;
|
|
216
|
+
if (policy === null) {
|
|
217
|
+
return await this._reconnecting(spec);
|
|
218
|
+
}
|
|
219
|
+
if (!spec.idempotent && !policy.retryUnsafe) {
|
|
220
|
+
// a lost response after a non-idempotent request (POST) could
|
|
221
|
+
// mean a second execution server-side
|
|
222
|
+
return await this.request(spec);
|
|
223
|
+
}
|
|
224
|
+
if (
|
|
225
|
+
typeof ReadableStream !== "undefined" &&
|
|
226
|
+
spec.body instanceof ReadableStream
|
|
227
|
+
) {
|
|
228
|
+
// a stream cannot be replayed: single attempt
|
|
229
|
+
return await this.request(spec);
|
|
230
|
+
}
|
|
231
|
+
const delays = retryDelays(policy.delays);
|
|
232
|
+
let attempts = 0;
|
|
233
|
+
for (;;) {
|
|
234
|
+
attempts += 1;
|
|
235
|
+
const exhausted =
|
|
236
|
+
policy.maxAttempts !== null && attempts >= policy.maxAttempts;
|
|
237
|
+
let response;
|
|
238
|
+
try {
|
|
239
|
+
response = await this.request(spec);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (
|
|
242
|
+
!this._transportRetryable(error) ||
|
|
243
|
+
this._transportNonretryable(error) ||
|
|
244
|
+
exhausted
|
|
245
|
+
) {
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
await sleep(delays.next().value);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (!policy.retryableStatus(response.status) || exhausted) {
|
|
252
|
+
return response;
|
|
253
|
+
}
|
|
254
|
+
const retryAfter = retryAfterDelay(response);
|
|
255
|
+
await sleep(retryAfter !== null ? retryAfter : delays.next().value);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Execute the request and yield response body chunks. */
|
|
260
|
+
async *stream(spec) {
|
|
261
|
+
const controller = new AbortController();
|
|
262
|
+
let timer = null;
|
|
263
|
+
const disarm = () => {
|
|
264
|
+
if (timer !== null) {
|
|
265
|
+
clearTimeout(timer);
|
|
266
|
+
timer = null;
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
const abortIn = (seconds) => {
|
|
270
|
+
disarm();
|
|
271
|
+
if (seconds === null) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
timer = setTimeout(
|
|
275
|
+
() =>
|
|
276
|
+
controller.abort(
|
|
277
|
+
new DOMException("stream read timed out", "TimeoutError"),
|
|
278
|
+
),
|
|
279
|
+
Math.max(0, seconds) * 1000,
|
|
280
|
+
);
|
|
281
|
+
};
|
|
282
|
+
const sse = spec.accept === "text/event-stream";
|
|
283
|
+
const deadline = spec.deadline ?? null; // monotonic seconds
|
|
284
|
+
// the budget for the next transport wait: downloads use the
|
|
285
|
+
// client timeout (idle cap, re-armed per chunk); SSE is unbounded
|
|
286
|
+
// unless the caller set an absolute deadline - keepalive frames
|
|
287
|
+
// re-enter abortIn with a shrinking remainder, so the bound stays
|
|
288
|
+
// absolute no matter how often the server keeps the stream warm
|
|
289
|
+
const nextBudget = () => {
|
|
290
|
+
if (!sse) {
|
|
291
|
+
return this.timeout;
|
|
292
|
+
}
|
|
293
|
+
return deadline === null ? null : deadline - monotonic();
|
|
294
|
+
};
|
|
295
|
+
// the server may close a pooled keep-alive socket between
|
|
296
|
+
// requests and fetch exposes no pool to pre-check (the Python
|
|
297
|
+
// adapters validate pooled connections instead): a replayable
|
|
298
|
+
// connect that fails on the transport is retried per the client
|
|
299
|
+
// policy, and even with no policy it gets one transparent
|
|
300
|
+
// reconnect - the body has not been consumed yet, so it is safe
|
|
301
|
+
const replayable =
|
|
302
|
+
spec.idempotent &&
|
|
303
|
+
!(
|
|
304
|
+
typeof ReadableStream !== "undefined" &&
|
|
305
|
+
spec.body instanceof ReadableStream
|
|
306
|
+
);
|
|
307
|
+
const policy = replayable ? this.retry : null;
|
|
308
|
+
const maxAttempts =
|
|
309
|
+
policy !== null ? policy.maxAttempts : replayable ? 2 : 1;
|
|
310
|
+
const delays = policy === null ? null : retryDelays(policy.delays);
|
|
311
|
+
let attempts = 0;
|
|
312
|
+
let response;
|
|
313
|
+
for (;;) {
|
|
314
|
+
attempts += 1;
|
|
315
|
+
abortIn(this.timeout); // the connect phase always has a bound
|
|
316
|
+
try {
|
|
317
|
+
response = await this._fetch(
|
|
318
|
+
this.buildUrl(spec),
|
|
319
|
+
this._fetchOptions(spec, controller.signal),
|
|
320
|
+
);
|
|
321
|
+
break;
|
|
322
|
+
} catch (error) {
|
|
323
|
+
disarm();
|
|
324
|
+
if (
|
|
325
|
+
!this._transportRetryable(error) ||
|
|
326
|
+
this._transportNonretryable(error) ||
|
|
327
|
+
(maxAttempts !== null && attempts >= maxAttempts)
|
|
328
|
+
) {
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
331
|
+
await sleep(delays === null ? 0 : delays.next().value);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (!(response.status >= 200 && response.status < 300)) {
|
|
335
|
+
// the error body is read under the same timer: a 500 with an
|
|
336
|
+
// endless body must not hang the caller
|
|
337
|
+
let body;
|
|
338
|
+
try {
|
|
339
|
+
body = new Uint8Array(await response.arrayBuffer());
|
|
340
|
+
} finally {
|
|
341
|
+
disarm();
|
|
342
|
+
}
|
|
343
|
+
const headers = {};
|
|
344
|
+
response.headers.forEach((value, key) => {
|
|
345
|
+
headers[key.toLowerCase()] = value;
|
|
346
|
+
});
|
|
347
|
+
throw errorForResponse({ status: response.status, headers, body });
|
|
348
|
+
}
|
|
349
|
+
disarm();
|
|
350
|
+
const reader = response.body.getReader();
|
|
351
|
+
try {
|
|
352
|
+
for (;;) {
|
|
353
|
+
abortIn(nextBudget());
|
|
354
|
+
const { done, value } = await reader.read();
|
|
355
|
+
// the timer covers only the transport wait, never the
|
|
356
|
+
// consumer's processing of the yielded chunk
|
|
357
|
+
disarm();
|
|
358
|
+
if (done) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
yield value;
|
|
362
|
+
}
|
|
363
|
+
} finally {
|
|
364
|
+
disarm();
|
|
365
|
+
try {
|
|
366
|
+
await reader.cancel();
|
|
367
|
+
} catch {
|
|
368
|
+
// the stream is already gone; releasing is best-effort
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async open() {}
|
|
374
|
+
|
|
375
|
+
async close() {}
|
|
376
|
+
|
|
377
|
+
/** Best-effort check that the operation reached a terminal state. */
|
|
378
|
+
async operationTerminal(operationId) {
|
|
379
|
+
let status;
|
|
380
|
+
try {
|
|
381
|
+
status = (await this.getOperationStatus(operationId)).status;
|
|
382
|
+
} catch (error) {
|
|
383
|
+
if (error instanceof ContreeError || this._transportRetryable(error)) {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
return status !== undefined && isTerminalStatus(status);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Wait until the operation finishes, driven by its event stream:
|
|
392
|
+
* follows the SSE log until `completion`, then fetches and returns
|
|
393
|
+
* the terminal OperationResponse. */
|
|
394
|
+
async waitOperation(operationId, { timeout = null } = {}) {
|
|
395
|
+
// eslint-disable-next-line no-unused-vars
|
|
396
|
+
for await (const event of this.followOperationEvents(operationId, {
|
|
397
|
+
timeout,
|
|
398
|
+
})) {
|
|
399
|
+
// draining the stream is the wait
|
|
400
|
+
}
|
|
401
|
+
return await this.getOperationStatus(operationId);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Stream operation events with transparent reconnection: network
|
|
405
|
+
* drops, in-band SSE error frames and retryable API statuses
|
|
406
|
+
* (410/425/5xx) reconnect from the last received event id; other
|
|
407
|
+
* API errors propagate. Ends after the `completion` event. */
|
|
408
|
+
async *followOperationEvents(
|
|
409
|
+
operationId,
|
|
410
|
+
{ last_event_id = null, spid = null, since = null, timeout = null } = {},
|
|
411
|
+
) {
|
|
412
|
+
let lastId = last_event_id;
|
|
413
|
+
const delays = retryDelays();
|
|
414
|
+
const deadline = timeout === null ? null : monotonic() + timeout;
|
|
415
|
+
const checkDeadline = () => {
|
|
416
|
+
if (deadline !== null && monotonic() >= deadline) {
|
|
417
|
+
throw new ContreeAPIError(
|
|
418
|
+
0,
|
|
419
|
+
`operation ${operationId} events did not complete within ${timeout}s`,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
for (;;) {
|
|
424
|
+
checkDeadline();
|
|
425
|
+
const eventsBefore = lastId;
|
|
426
|
+
try {
|
|
427
|
+
for await (const event of this.iterOperationEvents(operationId, {
|
|
428
|
+
follow: true,
|
|
429
|
+
spid,
|
|
430
|
+
since,
|
|
431
|
+
last_event_id: lastId,
|
|
432
|
+
deadline,
|
|
433
|
+
})) {
|
|
434
|
+
lastId = event.id;
|
|
435
|
+
yield event;
|
|
436
|
+
if (event.type === "completion") {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
checkDeadline();
|
|
440
|
+
}
|
|
441
|
+
} catch (error) {
|
|
442
|
+
if (error instanceof SSEStreamError) {
|
|
443
|
+
if (error.lastEventId !== null) {
|
|
444
|
+
lastId = error.lastEventId;
|
|
445
|
+
}
|
|
446
|
+
} else if (error instanceof ContreeAPIError) {
|
|
447
|
+
const retryable =
|
|
448
|
+
error.status === 410 ||
|
|
449
|
+
error.status === 425 ||
|
|
450
|
+
(error.status >= 500 && error.status < 600);
|
|
451
|
+
if (!retryable) {
|
|
452
|
+
throw error;
|
|
453
|
+
}
|
|
454
|
+
if (await this.operationTerminal(operationId)) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
let delay =
|
|
458
|
+
error.retryAfter !== null ? error.retryAfter : delays.next().value;
|
|
459
|
+
if (deadline !== null) {
|
|
460
|
+
// a Retry-After must not sleep past the caller's deadline
|
|
461
|
+
delay = Math.min(delay, Math.max(0, deadline - monotonic()));
|
|
462
|
+
}
|
|
463
|
+
await sleep(delay);
|
|
464
|
+
continue;
|
|
465
|
+
} else if (
|
|
466
|
+
!this._transportRetryable(error) ||
|
|
467
|
+
this._transportNonretryable(error)
|
|
468
|
+
) {
|
|
469
|
+
throw error;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// the stream ended or broke without a completion frame: the
|
|
473
|
+
// retry must not outlive the operation itself
|
|
474
|
+
if (await this.operationTerminal(operationId)) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (lastId === eventsBefore) {
|
|
478
|
+
await sleep(TIGHT_LOOP_FLOOR);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** Resolve an image reference (UUID, `tag:NAME` or bare tag) to a UUID. */
|
|
484
|
+
async resolveImage(ref) {
|
|
485
|
+
if (ref.startsWith("tag:")) {
|
|
486
|
+
return await this.inspectFindImageByTag(ref.slice(4));
|
|
487
|
+
}
|
|
488
|
+
if (isUuid(ref)) {
|
|
489
|
+
return ref;
|
|
490
|
+
}
|
|
491
|
+
return await this.inspectFindImageByTag(ref);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** Upload *content* unless the server already stores it (sha256
|
|
495
|
+
* dedup). A ReadableStream cannot be hashed and replayed, so
|
|
496
|
+
* without a caller-provided sha256 it uploads directly. */
|
|
497
|
+
async ensureFile(content, { sha256: digest = null } = {}) {
|
|
498
|
+
const resolved = digest !== null ? digest : await sha256(content);
|
|
499
|
+
if (resolved === null) {
|
|
500
|
+
return await this.uploadFile(content);
|
|
501
|
+
}
|
|
502
|
+
try {
|
|
503
|
+
return await this.getFile(resolved);
|
|
504
|
+
} catch (error) {
|
|
505
|
+
if (error instanceof NotFoundError) {
|
|
506
|
+
return await this.uploadFile(content);
|
|
507
|
+
}
|
|
508
|
+
throw error;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** List publicly available images (GET /images) */
|
|
513
|
+
async listImages(options = {}) {
|
|
514
|
+
const spec = operations.buildListImages(options);
|
|
515
|
+
return operations.parseListImages(await this.call(spec));
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** Iterate over listImages() results across pages; offset
|
|
519
|
+
* pagination is transparent, breaking out stops fetching. */
|
|
520
|
+
async *iterImages(options = {}) {
|
|
521
|
+
const { page_size = 1000, limit = null, ...filters } = options;
|
|
522
|
+
if (!Number.isInteger(page_size) || page_size < 1 || page_size > 1000) {
|
|
523
|
+
throw new RangeError("page_size must be an integer between 1 and 1000");
|
|
524
|
+
}
|
|
525
|
+
if (limit !== null && (!Number.isInteger(limit) || limit < 0)) {
|
|
526
|
+
throw new RangeError("limit must be null or a non-negative integer");
|
|
527
|
+
}
|
|
528
|
+
let fetched = 0;
|
|
529
|
+
let offset = 0;
|
|
530
|
+
for (;;) {
|
|
531
|
+
const size =
|
|
532
|
+
limit === null ? page_size : Math.min(page_size, limit - fetched);
|
|
533
|
+
if (size <= 0) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const response = await this.listImages({
|
|
537
|
+
...filters,
|
|
538
|
+
limit: size,
|
|
539
|
+
offset,
|
|
540
|
+
});
|
|
541
|
+
const page = response.images ?? [];
|
|
542
|
+
if (!page.length) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
for (const item of page) {
|
|
546
|
+
yield item;
|
|
547
|
+
fetched += 1;
|
|
548
|
+
if (limit !== null && fetched >= limit) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (page.length < size) {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
offset += page.length;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** Remove tags from an image (DELETE /images/{imageUUID}/tag) */
|
|
560
|
+
async deleteImageTag(imageUuid, options = {}) {
|
|
561
|
+
const spec = operations.buildDeleteImageTag(imageUuid, options);
|
|
562
|
+
return operations.parseDeleteImageTag(await this.call(spec));
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** Add a tag to an image (PATCH /images/{imageUUID}/tag) */
|
|
566
|
+
async updateImageTag(imageUuid, tag) {
|
|
567
|
+
const spec = operations.buildUpdateImageTag(imageUuid, tag);
|
|
568
|
+
return operations.parseUpdateImageTag(await this.call(spec));
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** Import a container image from a registry (POST /images/import) */
|
|
572
|
+
async importImage(registry, options = {}) {
|
|
573
|
+
const spec = operations.buildImportImage(registry, options);
|
|
574
|
+
return operations.parseImportImage(await this.call(spec));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** List uploaded files for the current namespace (GET /files) */
|
|
578
|
+
async listFiles(options = {}) {
|
|
579
|
+
const spec = operations.buildListFiles(options);
|
|
580
|
+
return operations.parseListFiles(await this.call(spec));
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** Iterate over listFiles() results across pages; offset
|
|
584
|
+
* pagination is transparent, breaking out stops fetching. */
|
|
585
|
+
async *iterFiles(options = {}) {
|
|
586
|
+
const { page_size = 1000, limit = null, ...filters } = options;
|
|
587
|
+
if (!Number.isInteger(page_size) || page_size < 1 || page_size > 1000) {
|
|
588
|
+
throw new RangeError("page_size must be an integer between 1 and 1000");
|
|
589
|
+
}
|
|
590
|
+
if (limit !== null && (!Number.isInteger(limit) || limit < 0)) {
|
|
591
|
+
throw new RangeError("limit must be null or a non-negative integer");
|
|
592
|
+
}
|
|
593
|
+
let fetched = 0;
|
|
594
|
+
let offset = 0;
|
|
595
|
+
for (;;) {
|
|
596
|
+
const size =
|
|
597
|
+
limit === null ? page_size : Math.min(page_size, limit - fetched);
|
|
598
|
+
if (size <= 0) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const response = await this.listFiles({
|
|
602
|
+
...filters,
|
|
603
|
+
limit: size,
|
|
604
|
+
offset,
|
|
605
|
+
});
|
|
606
|
+
const page = response.files ?? [];
|
|
607
|
+
if (!page.length) {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
for (const item of page) {
|
|
611
|
+
yield item;
|
|
612
|
+
fetched += 1;
|
|
613
|
+
if (limit !== null && fetched >= limit) {
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (page.length < size) {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
offset += page.length;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/** Upload a file to the server. the body must be a file content. (POST /files) */
|
|
625
|
+
async uploadFile(content) {
|
|
626
|
+
const spec = operations.buildUploadFile(content);
|
|
627
|
+
return operations.parseUploadFile(await this.call(spec));
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/** Get file info by SHA256 (GET /files/{sha256}) */
|
|
631
|
+
async getFile(sha256) {
|
|
632
|
+
const spec = operations.buildGetFile(sha256);
|
|
633
|
+
return operations.parseGetFile(await this.call(spec));
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** Check if a file exists for the current namespace (HEAD /files/{sha256}) */
|
|
637
|
+
async checkFileExists(sha256) {
|
|
638
|
+
const spec = operations.buildCheckFileExists(sha256);
|
|
639
|
+
return operations.parseCheckFileExists(await this.call(spec));
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** Spawn a new container instance (POST /instances) */
|
|
643
|
+
async spawnInstance(command, image, options = {}) {
|
|
644
|
+
const spec = operations.buildSpawnInstance(command, image, options);
|
|
645
|
+
return operations.parseSpawnInstance(await this.call(spec));
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** List operations (GET /operations) */
|
|
649
|
+
async listOperations(options = {}) {
|
|
650
|
+
const spec = operations.buildListOperations(options);
|
|
651
|
+
return operations.parseListOperations(await this.call(spec));
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/** Iterate over listOperations() results across pages; offset
|
|
655
|
+
* pagination is transparent, breaking out stops fetching. */
|
|
656
|
+
async *iterOperations(options = {}) {
|
|
657
|
+
const { page_size = 1000, limit = null, ...filters } = options;
|
|
658
|
+
if (!Number.isInteger(page_size) || page_size < 1 || page_size > 1000) {
|
|
659
|
+
throw new RangeError("page_size must be an integer between 1 and 1000");
|
|
660
|
+
}
|
|
661
|
+
if (limit !== null && (!Number.isInteger(limit) || limit < 0)) {
|
|
662
|
+
throw new RangeError("limit must be null or a non-negative integer");
|
|
663
|
+
}
|
|
664
|
+
let fetched = 0;
|
|
665
|
+
let offset = 0;
|
|
666
|
+
for (;;) {
|
|
667
|
+
const size =
|
|
668
|
+
limit === null ? page_size : Math.min(page_size, limit - fetched);
|
|
669
|
+
if (size <= 0) {
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
const response = await this.listOperations({
|
|
673
|
+
...filters,
|
|
674
|
+
limit: size,
|
|
675
|
+
offset,
|
|
676
|
+
});
|
|
677
|
+
const page = response ?? [];
|
|
678
|
+
if (!page.length) {
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
for (const item of page) {
|
|
682
|
+
yield item;
|
|
683
|
+
fetched += 1;
|
|
684
|
+
if (limit !== null && fetched >= limit) {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (page.length < size) {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
offset += page.length;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/** Get an operation status (GET /operations/{operationId}) */
|
|
696
|
+
async getOperationStatus(operationId, options = {}) {
|
|
697
|
+
const spec = operations.buildGetOperationStatus(operationId, options);
|
|
698
|
+
return operations.parseGetOperationStatus(await this.call(spec));
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/** Cancel an operation (DELETE /operations/{operationId}) */
|
|
702
|
+
async cancelOperation(operationId) {
|
|
703
|
+
const spec = operations.buildCancelOperation(operationId);
|
|
704
|
+
return operations.parseCancelOperation(await this.call(spec));
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/** Stream the operation event log via Server-Sent Events (GET /operations/{operationId}/events) */
|
|
708
|
+
async *iterOperationEvents(operationId, options = {}) {
|
|
709
|
+
const spec = operations.buildIterOperationEvents(operationId, options);
|
|
710
|
+
if (options.deadline != null) {
|
|
711
|
+
{
|
|
712
|
+
spec.deadline = options.deadline;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
const parser = new SSEParser();
|
|
716
|
+
let lastSeen = options.last_event_id ?? null;
|
|
717
|
+
for await (const chunk of this.stream(spec)) {
|
|
718
|
+
for (const frame of parser.feed(chunk)) {
|
|
719
|
+
const payload = decodeFramePayload(frame, lastSeen);
|
|
720
|
+
// id-only frames advance the resume cursor even
|
|
721
|
+
// though they carry no payload
|
|
722
|
+
if (frame.id !== null) {
|
|
723
|
+
lastSeen = frame.id;
|
|
724
|
+
}
|
|
725
|
+
if (payload === null) {
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
yield OperationEvent.fromWire(payload);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/** Find image by tag (GET /inspect/) */
|
|
734
|
+
async inspectFindImageByTag(tag) {
|
|
735
|
+
const spec = operations.buildInspectFindImageByTag(tag);
|
|
736
|
+
return operations.parseInspectFindImageByTag(await this.call(spec));
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/** Inspect an image (GET /inspect/{image_uuid}/) */
|
|
740
|
+
async inspectImage(imageUuid) {
|
|
741
|
+
const spec = operations.buildInspectImage(imageUuid);
|
|
742
|
+
return operations.parseInspectImage(await this.call(spec));
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/** Download a file from image (GET /inspect/{image_uuid}/download) */
|
|
746
|
+
async inspectImageDownload(imageUuid, path) {
|
|
747
|
+
const spec = operations.buildInspectImageDownload(imageUuid, path);
|
|
748
|
+
return operations.parseInspectImageDownload(await this.call(spec));
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/** Streaming variant of inspectImageDownload(). */
|
|
752
|
+
async *inspectImageDownloadStream(imageUuid, path) {
|
|
753
|
+
const spec = operations.buildInspectImageDownload(imageUuid, path);
|
|
754
|
+
yield* this.stream(spec);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/** Check if a file exists in image (HEAD /inspect/{image_uuid}/download) */
|
|
758
|
+
async checkImageFile(imageUuid, path) {
|
|
759
|
+
const spec = operations.buildCheckImageFile(imageUuid, path);
|
|
760
|
+
return operations.parseCheckImageFile(await this.call(spec));
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/** Download a file or a directory from image as tar archive (GET /inspect/{image_uuid}/archive) */
|
|
764
|
+
async *inspectImageArchive(imageUuid, path) {
|
|
765
|
+
const spec = operations.buildInspectImageArchive(imageUuid, path);
|
|
766
|
+
yield* this.stream(spec);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/** Check if a path can be archived from image (HEAD /inspect/{image_uuid}/archive) */
|
|
770
|
+
async checkImageArchive(imageUuid, path) {
|
|
771
|
+
const spec = operations.buildCheckImageArchive(imageUuid, path);
|
|
772
|
+
return operations.parseCheckImageArchive(await this.call(spec));
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/** List files in image (GET /inspect/{image_uuid}/list) */
|
|
776
|
+
async inspectImageList(imageUuid, path) {
|
|
777
|
+
const spec = operations.buildInspectImageList(imageUuid, path);
|
|
778
|
+
return operations.parseInspectImageList(await this.call(spec));
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/** Get current token information (GET /whoami) */
|
|
782
|
+
async whoami() {
|
|
783
|
+
const spec = operations.buildWhoami();
|
|
784
|
+
return operations.parseWhoami(await this.call(spec));
|
|
785
|
+
}
|
|
786
|
+
}
|