Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,294 changes: 1,248 additions & 46 deletions api.ts

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions client/helpers/exports.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { CreateExportRequest, Export } from '../../index';
import {
BaseReq,
DefaultPolling,
isPollError,
MaxAttempts,
PollIntervalMs,
pollQueuedJob,
throwOnError,
} from '../index';
Expand Down Expand Up @@ -36,7 +35,7 @@ export async function createExport({
const pollRes = await pollQueuedJob<Export>({
id: queuedId,
getQueuedJob: (id) => client.exports.getQueuedExport({ id }),
polling: { intervalMs: PollIntervalMs, maxAttempts: MaxAttempts },
polling: DefaultPolling,
});
if (isPollError(pollRes.res)) throwOnError(pollRes);
if (verbose) onMsg(`Completed export ${pollRes.res.data.id}`);
Expand Down
46 changes: 46 additions & 0 deletions client/helpers/file-collections.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { FileCollectionMetadataData } from '../../api';
import { DeleteReq, getPage } from '../../client/index';

/**
* Delete all file collections.
*
* @param args - The {@link DeleteReq}.
*/
export async function deleteAllFileCollections({
client,
pageSize = 100,
exceptions = new Set(),
}: DeleteReq): Promise<FileCollectionMetadataData[]> {
let fileCollections: FileCollectionMetadataData[] = [];
let pageCursor: string | undefined;
do {
// eslint-disable-next-line no-await-in-loop
const res = await getPage(() =>
client.fileCollections.listFileCollections({
pageSize,
pageCursor,
})
);
const ids = res.page.data
.map((d) => d.id)
.filter((id) => !exceptions.has(id));
pageCursor = res.cursor;
// eslint-disable-next-line no-await-in-loop
const deleteRes = await Promise.allSettled(
ids.map((id) => client.fileCollections.deleteFileCollection({ id }))
);
deleteRes.forEach((r, index) => {
if (r.status === 'rejected') {
console.error(
`Failed to delete file collection with id=${ids[index]}: ${r.reason}`
);
exceptions.add(ids[index]);
}
});
fileCollections = fileCollections.concat(
res.page.data as FileCollectionMetadataData[]
);
} while (pageCursor);

return fileCollections;
}
2 changes: 2 additions & 0 deletions client/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export * from './exports';
export * from './file-collections';
export * from './files';
export * from './parts';
export * from './polling';
export * from './queued-jobs';
export * from './scene-items';
export * from './scene-views';
Expand Down
5 changes: 2 additions & 3 deletions client/helpers/parts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,13 @@ import {
} from '../../index';
import {
BaseReq,
DefaultPolling,
DeleteReq,
encodeIfNotEncoded,
getBySuppliedId,
getPage,
head,
MaxAttempts,
Polling,
PollIntervalMs,
prettyJson,
RenderImageReq,
toAccept,
Expand Down Expand Up @@ -85,7 +84,7 @@ export async function createPartFromFile({
client,
createPartReq,
onMsg = console.log,
polling = { intervalMs: PollIntervalMs, maxAttempts: MaxAttempts },
polling = DefaultPolling,
returnQueued = false,
verbose,
...rest
Expand Down
163 changes: 163 additions & 0 deletions client/helpers/polling.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/**
* Polling helpers for handling API polling operations.
*/

import { Polling } from '..';

export const DefaultPollIntervalMs = 500; // 500 milliseconds
export const DefaultPollTimeoutSeconds = 60 * 60; // 60 minutes
export const DefaultShortPollIntervalMs = 50; // 50 milliseconds
export const DefaultShortPollTimeoutSeconds = 60 * 10; // 10 minutes

/**
* Default backoff configuration for polling.
* Backoff numbers are added to `intervalMs` after the attempt number is reached
* e.g. if `intervalMs` is 500 and `backoff[10]` is 2000, then the polling interval
* will be 2500ms after the 10th attempt.
*/
export const DefaultBackoffMs: Record<number, number | undefined> = {
0: DefaultPollIntervalMs,
1: 500,
10: 2000,
30: 3000,
50: 5000,
300: 10000,
1000: 20000,
};

/**
* Default polling configuration for batch operations.
* Numbers below will result in a total delay of about 60 minutes
* prior to reaching the maximum number of attempts.
*/
export const DefaultPolling: Polling = getPollingConfiguration({
backoff: DefaultBackoffMs, // Use backoff
maxPollDurationSeconds: DefaultPollTimeoutSeconds, // 1 hour
});

/**
* Default short backoff configuration for polling.
* Use this for polling for operations that complete quickly
* and require a shorter delay between polling attempts.
*/
export const DefaultShortBackoffMs: Record<number, number | undefined> = {
0: DefaultShortPollIntervalMs,
1: 50,
10: 200,
20: 300,
40: 500,
50: 1000,
100: 3000,
200: 5000,
};

/**
* Default short polling configuration for quick running operations.
* Numbers below will result in a total delay of about 10 minutes
* prior to reaching the maximum number of attempts.
*/
export const DefaultShortPolling: Polling = getPollingConfiguration({
backoff: DefaultShortBackoffMs, // Use short backoff
maxPollDurationSeconds: DefaultShortPollTimeoutSeconds, // 10 minutes
});

/**
* Builds a polling configuration object based on the provided parameters.
*
* @param param0
* @returns
*/
export function getPollingConfiguration({
backoff,
intervalMs = DefaultPollIntervalMs,
maxPollDurationSeconds = DefaultPollTimeoutSeconds,
}: {
backoff?: Record<number, number | undefined>;
intervalMs?: number;
maxPollDurationSeconds?: number;
}): Polling {
return {
intervalMs: intervalMs,
maxAttempts: getMaxAttempts({
intervalMs,
maxPollDurationSeconds,
backoff,
}),
backoff,
};
}

/**
* Calculates the polling delay for a given attempt.
* @param param0
* @returns {number} - The delay in milliseconds for the polling attempt.
*/
export function getPollingDelay({
attempt,
polling,
}: {
attempt: number;
polling: Polling;
}): number {
return polling.intervalMs + getBackoffForAttempt(attempt, polling.backoff);
}

/**
* Gets the backoff keys from the backoff configuration.
* @param backoff - The backoff configuration.
* @returns {number[]} - The array of backoff keys.
*/
function getBackoffKeys(backoff: Record<number, number | undefined>): number[] {
return Object.keys(backoff)
.map((key) => parseInt(key, 10))
.reverse();
}

/**
* Calculates the maximum number of polling attempts based on the provided
* parameters.
* @param param0
* @returns {number} - The maximum number of polling attempts.
*/
function getMaxAttempts({
intervalMs,
maxPollDurationSeconds,
backoff,
}: {
intervalMs: number;
maxPollDurationSeconds: number;
backoff: Record<number, number | undefined> | undefined;
}): number {
if (backoff) {
let remainingTimeMs = maxPollDurationSeconds * 1000;
let attempt = 0;
const backoffKeys = getBackoffKeys(backoff);
while (remainingTimeMs > 0) {
const backoffMs = getBackoffForAttempt(attempt + 1, backoff, backoffKeys);
remainingTimeMs -= intervalMs + backoffMs;
attempt += 1;
}
return attempt;
}
return Math.max(1, Math.floor(maxPollDurationSeconds / intervalMs));
}

/**
* Gets the backoff delay for a specific polling attempt.
* @param attempt - The current polling attempt number.
* @param backoff - The backoff configuration.
* @param backoffKeys - Optional keys to use for backoff lookup.
* @returns {number} - The backoff delay in milliseconds.
*/
function getBackoffForAttempt(
attempt: number,
backoff?: Record<number, number | undefined>,
backoffKeys?: number[]
): number {
if (backoff) {
const keys = backoffKeys ?? getBackoffKeys(backoff);
const foundKey = [...keys].find((key) => attempt > key);
return backoff[foundKey ?? 0] ?? 0;
}
return 0;
}
26 changes: 20 additions & 6 deletions client/helpers/queued-jobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import axios, { AxiosResponse, CancelToken } from 'axios';
import { Limit } from 'p-limit';
import { hrtime } from 'process';

import { ApiError, Batch, Failure, Polling, QueuedJob } from '../../index';
import {
ApiError,
Batch,
Failure,
getPollingDelay,
Polling,
QueuedJob,
} from '../../index';
import {
defined,
delay,
Expand All @@ -14,7 +21,7 @@ import {
VertexError,
} from '../utils';

export const PollIntervalMs = 5000;
export const PollIntervalMs = 1000;

export const AttemptsPerMin = 60000 / PollIntervalMs;

Expand Down Expand Up @@ -68,8 +75,9 @@ export async function pollQueuedJob<T>({
getQueuedJob,
allow404 = false,
limit,
polling: { intervalMs, maxAttempts, backoff },
polling,
}: PollQueuedJobReq): Promise<PollQueuedJobRes<T>> {
const { maxAttempts } = polling;
async function poll(attempt: number): Promise<PollJobRes<T>> {
const cancelSrc = axios.CancelToken.source();
const timerId = setTimeout(
Expand Down Expand Up @@ -123,7 +131,10 @@ export async function pollQueuedJob<T>({
}

let attempts = 1;
let backoffMs = backoff?.[attempts] ?? 0;
let pollDelay = getPollingDelay({
attempt: attempts,
polling,
});
// eslint-disable-next-line prefer-const
let pollRes = await poll(attempts);
/* eslint-disable no-await-in-loop */
Expand All @@ -135,8 +146,11 @@ export async function pollQueuedJob<T>({
attempts <= maxAttempts
) {
attempts += 1;
backoffMs = backoff?.[attempts] ?? backoffMs;
await delay(intervalMs + backoffMs);
pollDelay = getPollingDelay({
attempt: attempts,
polling,
});
await delay(pollDelay);
pollRes = await poll(attempts);
}
/* eslint-enable no-await-in-loop */
Expand Down
5 changes: 2 additions & 3 deletions client/helpers/scene-items.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { CreateSceneItemRequest, SceneItem } from '../../index';
import {
BaseReq,
DefaultShortPolling,
defined,
isApiError,
isPollError,
MaxAttempts,
Polling,
PollIntervalMs,
pollQueuedJob,
throwOnError,
} from '../index';
Expand Down Expand Up @@ -57,7 +56,7 @@ export async function createSceneItem({
client,
createSceneItemReq,
onMsg = console.log,
polling = { intervalMs: PollIntervalMs, maxAttempts: MaxAttempts },
polling = DefaultShortPolling,
sceneId,
verbose,
}: CreateSceneItemReq): Promise<SceneItem> {
Expand Down
9 changes: 5 additions & 4 deletions client/helpers/scenes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
} from '../../index';
import {
BaseReq,
DefaultPolling,
DefaultShortPolling,
defined,
DeleteReq,
getPage,
Expand Down Expand Up @@ -165,8 +167,7 @@ export interface QueuedSceneItem {
readonly res?: Failure | QueuedJob;
}

const defaultPolling: Polling = { intervalMs: 200, maxAttempts: 4500 }; // 15 minute timeout for batch completions
const sceneReadyPolling: Polling = { intervalMs: 1000, maxAttempts: 3600 }; // one hour timeout for scene state ready
const sceneReadyPolling: Polling = DefaultPolling; // one hour timeout for scene state ready

/**
* Create a scene with scene items.
Expand All @@ -179,7 +180,7 @@ export async function createSceneAndSceneItems({
onMsg = console.log,
onProgress,
parallelism,
polling = defaultPolling,
polling = DefaultShortPolling,
returnQueued = false,
verbose,
}: CreateSceneAndSceneItemsReq): Promise<CreateSceneAndSceneItemsRes> {
Expand Down Expand Up @@ -467,7 +468,7 @@ export const createSceneItemBatch = async ({
onProgress,
limit,
sceneId,
polling = { intervalMs: PollIntervalMs, maxAttempts: MaxAttempts },
polling = DefaultShortPolling,
}: CreateSceneItemBatchReq): Promise<CreateSceneItemBatchRes> => {
let batchErrors: QueuedBatchOps[] = [];
let itemErrors: SceneItemError[] = [];
Expand Down
Loading