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/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
+ }