xapi-to 0.1.20 → 0.1.22

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/src/client.ts CHANGED
@@ -26,6 +26,13 @@ export interface ClientOptions {
26
26
  apiKey?: string;
27
27
  }
28
28
 
29
+ export interface ApiKeyApiRequestOptions {
30
+ method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
31
+ body?: unknown;
32
+ timeoutMs?: number;
33
+ retries?: number;
34
+ }
35
+
29
36
  export class HttpError extends Error {
30
37
  constructor(
31
38
  public readonly status: number,
@@ -100,8 +107,28 @@ function parseRetryAfterMs(res: Response): number | undefined {
100
107
  return Number.isFinite(at) ? Math.max(0, at - Date.now()) : undefined;
101
108
  }
102
109
 
103
- function sleep(ms: number): Promise<void> {
104
- return new Promise((resolve) => setTimeout(resolve, ms));
110
+ function abortError(signal?: AbortSignal | null): Error {
111
+ const reason = signal?.reason;
112
+ return reason instanceof Error
113
+ ? reason
114
+ : new DOMException('The operation was aborted', 'AbortError');
115
+ }
116
+
117
+ function sleep(ms: number, signal?: AbortSignal | null): Promise<void> {
118
+ if (signal?.aborted) return Promise.reject(abortError(signal));
119
+ return new Promise((resolve, reject) => {
120
+ let timer: ReturnType<typeof setTimeout> | undefined;
121
+ const onAbort = () => {
122
+ if (timer !== undefined) clearTimeout(timer);
123
+ signal?.removeEventListener('abort', onAbort);
124
+ reject(abortError(signal));
125
+ };
126
+ timer = setTimeout(() => {
127
+ signal?.removeEventListener('abort', onAbort);
128
+ resolve();
129
+ }, ms);
130
+ signal?.addEventListener('abort', onAbort, { once: true });
131
+ });
105
132
  }
106
133
 
107
134
  export async function request<T>(
@@ -138,7 +165,7 @@ export async function request<T>(
138
165
  if (isRetryableStatus(res.status) && attempt < retries) {
139
166
  await res.text().catch(() => ''); // drain body so the socket can be reused
140
167
  clearTimeout(timer);
141
- await sleep(backoffDelayMs(attempt, retryAfterMs));
168
+ await sleep(backoffDelayMs(attempt, retryAfterMs), callerSignal);
142
169
  attempt++;
143
170
  continue;
144
171
  }
@@ -174,7 +201,7 @@ export async function request<T>(
174
201
  if (timedOut) {
175
202
  const timeoutError = new RequestTimeoutError(timeoutMs);
176
203
  if (attempt < retries) {
177
- await sleep(backoffDelayMs(attempt));
204
+ await sleep(backoffDelayMs(attempt), callerSignal);
178
205
  attempt++;
179
206
  continue;
180
207
  }
@@ -182,7 +209,7 @@ export async function request<T>(
182
209
  }
183
210
  if (isRetryableNetworkError(e) && attempt < retries) {
184
211
  clearTimeout(timer);
185
- await sleep(backoffDelayMs(attempt));
212
+ await sleep(backoffDelayMs(attempt), callerSignal);
186
213
  attempt++;
187
214
  continue;
188
215
  }
@@ -200,6 +227,30 @@ function headers(apiKey?: string): Record<string, string> {
200
227
  return h;
201
228
  }
202
229
 
230
+ /** Call a scoped API-Key control-plane endpoint without exchanging for a JWT. */
231
+ export function apiKeyApiRequest<T>(
232
+ apiHost: string,
233
+ apiKey: string,
234
+ path: string,
235
+ options: ApiKeyApiRequestOptions = {},
236
+ ): Promise<T> {
237
+ const method = options.method ?? 'GET';
238
+ const requestHeaders: Record<string, string> = { 'XAPI-KEY': apiKey };
239
+ if (options.body !== undefined) requestHeaders['Content-Type'] = 'application/json';
240
+ return request<T>(
241
+ `${scheme(apiHost)}://${apiHost}${path}`,
242
+ {
243
+ method,
244
+ headers: requestHeaders,
245
+ ...(options.body !== undefined
246
+ ? { body: JSON.stringify(options.body) }
247
+ : {}),
248
+ },
249
+ options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
250
+ options.retries ?? 0,
251
+ );
252
+ }
253
+
203
254
  function baseUrl(opts: ClientOptions): string {
204
255
  return `${scheme(opts.actionHost)}://${opts.actionHost}`;
205
256
  }
@@ -632,7 +683,11 @@ export async function initiateOAuth(
632
683
  );
633
684
  }
634
685
 
635
- export async function listOAuthBindings(jwtToken: string, apiHost: string) {
686
+ export async function listOAuthBindings(
687
+ jwtToken: string,
688
+ apiHost: string,
689
+ signal?: AbortSignal,
690
+ ) {
636
691
  return request<Array<{
637
692
  id: string;
638
693
  apiKeyId: string;
@@ -645,7 +700,7 @@ export async function listOAuthBindings(jwtToken: string, apiHost: string) {
645
700
  provider: { id: string; name: string; type: string };
646
701
  }>>(
647
702
  `${scheme(apiHost)}://${apiHost}/api/oauth/bindings`,
648
- { method: 'GET', headers: jwtHeaders(jwtToken) },
703
+ { method: 'GET', headers: jwtHeaders(jwtToken), signal },
649
704
  DEFAULT_TIMEOUT_MS,
650
705
  IDEMPOTENT_RETRIES,
651
706
  );
@@ -26,6 +26,7 @@ export interface SandboxRequirements {
26
26
  gpu?: { count?: number; model?: string };
27
27
  regions?: string[];
28
28
  capabilities?: string[];
29
+ minContinuousRuntimeSeconds?: number;
29
30
  [key: string]: unknown;
30
31
  }
31
32
 
@@ -263,24 +264,43 @@ export async function sandboxWait(
263
264
  signal?: AbortSignal,
264
265
  ): Promise<SandboxDetail> {
265
266
  const deadline = Date.now() + timeoutMs;
267
+ const deadlineController = new AbortController();
268
+ const abortFromCaller = () => deadlineController.abort();
269
+ const deadlineTimer = setTimeout(() => deadlineController.abort(), Math.max(0, timeoutMs));
270
+ if (signal?.aborted) deadlineController.abort();
271
+ else signal?.addEventListener('abort', abortFromCaller, { once: true });
266
272
  let last: SandboxDetail | undefined;
267
- while (Date.now() < deadline) {
268
- if (signal?.aborted) throw new Error(`sandbox wait interrupted while waiting for ${wanted.join(' or ')}`);
269
- last = await sandboxGet(opts, id, signal);
270
- const state = String(last.observedState || '');
271
- if (wanted.includes(state)) return last;
272
- if (['FAILED', 'TERMINATED'].includes(state) && !wanted.includes(state)) {
273
- throw new Error(`sandbox ${id} entered ${state} while waiting for ${wanted.join(' or ')}`);
273
+ try {
274
+ while (Date.now() < deadline) {
275
+ if (signal?.aborted) throw new Error(`sandbox wait interrupted while waiting for ${wanted.join(' or ')}`);
276
+ try {
277
+ last = await sandboxGet(opts, id, deadlineController.signal);
278
+ } catch (error) {
279
+ if (signal?.aborted) {
280
+ throw new Error(`sandbox wait interrupted while waiting for ${wanted.join(' or ')}`);
281
+ }
282
+ if (Date.now() >= deadline) break;
283
+ throw error;
284
+ }
285
+ if (Date.now() >= deadline) break;
286
+ const state = String(last.observedState || '');
287
+ if (wanted.includes(state)) return last;
288
+ if (['FAILED', 'TERMINATED'].includes(state) && !wanted.includes(state)) {
289
+ throw new Error(`sandbox ${id} entered ${state} while waiting for ${wanted.join(' or ')}`);
290
+ }
291
+ await new Promise<void>((resolve) => {
292
+ const done = () => {
293
+ clearTimeout(timer);
294
+ signal?.removeEventListener('abort', done);
295
+ resolve();
296
+ };
297
+ const timer = setTimeout(done, Math.min(intervalMs, Math.max(0, deadline - Date.now())));
298
+ signal?.addEventListener('abort', done, { once: true });
299
+ });
274
300
  }
275
- await new Promise<void>((resolve) => {
276
- const done = () => {
277
- clearTimeout(timer);
278
- signal?.removeEventListener('abort', done);
279
- resolve();
280
- };
281
- const timer = setTimeout(done, Math.min(intervalMs, Math.max(0, deadline - Date.now())));
282
- signal?.addEventListener('abort', done, { once: true });
283
- });
301
+ } finally {
302
+ clearTimeout(deadlineTimer);
303
+ signal?.removeEventListener('abort', abortFromCaller);
284
304
  }
285
305
  throw new Error(
286
306
  `sandbox ${id} did not enter ${wanted.join(' or ')} within ${timeoutMs}ms` +