Skip to content

Commit 30419d1

Browse files
authored
refactor: normalize wait-for implementations (#20)
1 parent 12d1af3 commit 30419d1

File tree

6 files changed

+198
-320
lines changed

6 files changed

+198
-320
lines changed

src/index.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,6 @@ export {
2222
} from './core/error';
2323

2424
// Export knowledge base helpers
25-
export {
26-
waitForDatabase,
27-
type WaitForDatabaseOptions,
28-
WaitForDatabaseTimeoutError,
29-
WaitForDatabaseFailedError,
30-
} from './resources/knowledge-bases/wait-for-database';
3125
export {
3226
IndexingJobAbortedError,
3327
IndexingJobNotFoundError,

src/resources/agents/agents.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,9 +306,11 @@ export class Agents extends APIResource {
306306
* });
307307
* ```
308308
*/
309-
async waitForReady(uuid: string, options: WaitForAgentOptions): Promise<AgentReadinessResponse> {
309+
async waitForReady(uuid: string, options: WaitForAgentOptions = {}): Promise<AgentReadinessResponse> {
310310
const { interval = 3000, timeout = 180000, signal } = options;
311311
const start = Date.now();
312+
let pollCount = 0;
313+
let currentInterval = interval;
312314

313315
while (true) {
314316
signal?.throwIfAborted();
@@ -331,7 +333,14 @@ export class Agents extends APIResource {
331333
throw new AgentDeploymentError(uuid, status);
332334
}
333335

334-
await sleep(interval);
336+
await sleep(currentInterval);
337+
338+
// Apply exponential backoff: double the interval after each poll, up to maxInterval
339+
pollCount++;
340+
if (pollCount > 2) {
341+
// Start exponential backoff after 2 polls
342+
currentInterval = Math.min(currentInterval * 2, 30000);
343+
}
335344
}
336345
}
337346
}

src/resources/knowledge-bases/indexing-jobs.ts

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ import { RequestOptions } from '../../internal/request-options';
77
import { path } from '../../internal/utils/path';
88
import { sleep } from '../../internal/utils/sleep';
99

10+
export interface IndexingJobWaitForCompletionOptions extends RequestOptions {
11+
/**
12+
* Initial polling interval in milliseconds (default: 5000ms).
13+
* This interval will be used for the first few polls, then exponential backoff applies.
14+
*/
15+
interval?: number;
16+
}
17+
1018
/**
1119
* Error thrown when an indexing job polling operation is aborted
1220
*/
@@ -239,39 +247,20 @@ export class IndexingJobs extends APIResource {
239247
*/
240248
async waitForCompletion(
241249
uuid: string,
242-
options: {
243-
/**
244-
* Initial polling interval in milliseconds (default: 5000ms).
245-
* This interval will be used for the first few polls, then exponential backoff applies.
246-
*/
247-
interval?: number;
248-
/**
249-
* Maximum time to wait in milliseconds (default: 600000ms = 10 minutes)
250-
*/
251-
timeout?: number;
252-
/**
253-
* Maximum polling interval in milliseconds (default: 30000ms = 30 seconds).
254-
* Exponential backoff will not exceed this value.
255-
*/
256-
maxInterval?: number;
257-
/**
258-
* Request options to pass to each poll request
259-
*/
260-
requestOptions?: RequestOptions;
261-
} = {},
250+
options: IndexingJobWaitForCompletionOptions = {},
262251
): Promise<IndexingJobRetrieveResponse> {
263-
const { interval = 5000, timeout = 600000, maxInterval = 30000, requestOptions } = options;
252+
const { interval = 5000, timeout = 600000, signal } = options;
264253
const startTime = Date.now();
265254
let currentInterval = interval;
266255
let pollCount = 0;
267256

268257
while (true) {
269258
// Check if operation was aborted
270-
if (requestOptions?.signal?.aborted) {
259+
if (signal?.aborted) {
271260
throw new IndexingJobAbortedError();
272261
}
273262

274-
const response = await this.retrieve(uuid, requestOptions);
263+
const response = await this.retrieve(uuid, options);
275264
const job = response.job;
276265

277266
if (!job) {
@@ -303,7 +292,7 @@ export class IndexingJobs extends APIResource {
303292
pollCount++;
304293
if (pollCount > 2) {
305294
// Start exponential backoff after 2 polls
306-
currentInterval = Math.min(currentInterval * 2, maxInterval);
295+
currentInterval = Math.min(currentInterval * 2, 30000);
307296
}
308297
}
309298
}

src/resources/knowledge-bases/knowledge-bases.ts

Lines changed: 174 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,112 @@ import {
3636
import { APIPromise } from '../../core/api-promise';
3737
import { RequestOptions } from '../../internal/request-options';
3838
import { path } from '../../internal/utils/path';
39-
import { waitForDatabase } from './wait-for-database';
39+
import { GradientError } from '../../core/error';
40+
import { sleep } from '../../internal/utils/sleep';
41+
42+
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
43+
44+
export interface WaitForDatabaseOptions extends RequestOptions {
45+
/**
46+
* The polling interval in milliseconds. Defaults to 5000 (5 seconds).
47+
*/
48+
interval?: number;
49+
}
50+
51+
/**
52+
* Database status values that indicate a successful deployment.
53+
*/
54+
const ONLINE_STATUSES = ['ONLINE'] as const;
55+
56+
/**
57+
* Database status values that indicate a failed deployment.
58+
*/
59+
const FAILED_STATUSES = ['DECOMMISSIONED', 'UNHEALTHY'] as const;
60+
61+
/**
62+
* Database status values that indicate the deployment is still in progress.
63+
*/
64+
const PENDING_STATUSES = [
65+
'CREATING',
66+
'POWEROFF',
67+
'REBUILDING',
68+
'REBALANCING',
69+
'FORKING',
70+
'MIGRATING',
71+
'RESIZING',
72+
'RESTORING',
73+
'POWERING_ON',
74+
] as const;
75+
76+
export class WaitForDatabaseTimeoutError extends GradientError {
77+
constructor(message: string, kbId?: string, timeout?: number) {
78+
super(message);
79+
this.name = 'WaitForDatabaseTimeoutError';
80+
if (kbId) {
81+
(this as any).knowledgeBaseId = kbId;
82+
(this as any).timeout = timeout;
83+
}
84+
}
85+
}
86+
87+
export class WaitForDatabaseFailedError extends GradientError {
88+
constructor(message: string, kbId?: string, status?: string) {
89+
super(message);
90+
this.name = 'WaitForDatabaseFailedError';
91+
if (kbId) {
92+
(this as any).knowledgeBaseId = kbId;
93+
(this as any).databaseStatus = status;
94+
}
95+
}
96+
}
97+
98+
/**
99+
* Polls for knowledge base database creation to complete.
100+
*
101+
* This helper function polls the knowledge base status until the database is ONLINE,
102+
* handling various error states and providing configurable timeout and polling intervals.
103+
*
104+
* @example
105+
* ```ts
106+
* import Gradient from '@digitalocean/gradient';
107+
*
108+
* const client = new Gradient();
109+
*
110+
* // Basic usage
111+
* try {
112+
* const kb = await client.knowledgeBases.waitForDatabase('123e4567-e89b-12d3-a456-426614174000');
113+
* console.log('Database is ready:', kb.database_status); // 'ONLINE'
114+
* } catch (error) {
115+
* if (error instanceof WaitForDatabaseTimeoutError) {
116+
* console.log('Polling timed out');
117+
* } else if (error instanceof WaitForDatabaseFailedError) {
118+
* console.log('Database deployment failed');
119+
* }
120+
* }
121+
*
122+
* // With AbortSignal
123+
* const controller = new AbortController();
124+
* setTimeout(() => controller.abort(), 30000); // Cancel after 30 seconds
125+
*
126+
* try {
127+
* const kb = await client.knowledgeBases.waitForDatabase('123e4567-e89b-12d3-a456-426614174000', {
128+
* signal: controller.signal
129+
* });
130+
* } catch (error) {
131+
* if (error.message === 'Operation was aborted') {
132+
* console.log('Operation was cancelled');
133+
* }
134+
* }
135+
* ```
136+
*
137+
* @param client - The Gradient client instance
138+
* @param uuid - The knowledge base UUID to poll for
139+
* @param options - Configuration options for polling behavior
140+
* @returns Promise<KnowledgeBaseRetrieveResponse> - The knowledge base with ONLINE database status
141+
* @throws WaitForDatabaseTimeoutError - If polling times out
142+
* @throws WaitForDatabaseFailedError - If the database enters a failed state
143+
* @throws Error - If the operation is aborted via AbortSignal
144+
*/
40145

41146
export class KnowledgeBases extends APIResource {
42147
dataSources: DataSourcesAPI.DataSources = new DataSourcesAPI.DataSources(this._client);
@@ -159,9 +264,75 @@ export class KnowledgeBases extends APIResource {
159264
*/
160265
async waitForDatabase(
161266
uuid: string,
162-
options?: import('./wait-for-database').WaitForDatabaseOptions,
267+
options: WaitForDatabaseOptions = {},
163268
): Promise<KnowledgeBaseRetrieveResponse> {
164-
return waitForDatabase(this._client, uuid, options || {});
269+
const { interval = 5000, timeout = 600000, signal } = options;
270+
271+
const startTime = Date.now();
272+
273+
while (true) {
274+
// Check if operation was aborted
275+
if (signal?.aborted) {
276+
throw new Error('Operation was aborted');
277+
}
278+
279+
const elapsed = Date.now() - startTime;
280+
281+
if (elapsed > timeout) {
282+
throw new WaitForDatabaseTimeoutError(
283+
`Knowledge base database ${uuid} did not become ONLINE within ${timeout}ms`,
284+
uuid,
285+
timeout,
286+
);
287+
}
288+
289+
try {
290+
const response = await this.retrieve(uuid, options);
291+
const status = response.database_status;
292+
293+
if (!status) {
294+
// If database_status is not present, continue polling
295+
await sleep(interval);
296+
continue;
297+
}
298+
299+
// Check for successful completion
300+
if (ONLINE_STATUSES.includes(status as any)) {
301+
return response;
302+
}
303+
304+
// Check for failed states
305+
if (FAILED_STATUSES.includes(status as any)) {
306+
throw new WaitForDatabaseFailedError(
307+
`Knowledge base database ${uuid} entered failed state: ${status}`,
308+
uuid,
309+
status,
310+
);
311+
}
312+
313+
// Check if still in progress
314+
if (PENDING_STATUSES.includes(status as any)) {
315+
await sleep(interval);
316+
continue;
317+
}
318+
319+
// Unknown status - treat as error for safety
320+
throw new WaitForDatabaseFailedError(
321+
`Knowledge base database ${uuid} entered unknown state: ${status}`,
322+
uuid,
323+
status,
324+
);
325+
} catch (error) {
326+
// If it's our custom error, re-throw it
327+
if (error instanceof WaitForDatabaseFailedError || error instanceof WaitForDatabaseTimeoutError) {
328+
throw error;
329+
}
330+
331+
// For other errors (network issues, etc.), try waiting a bit longer before retrying
332+
await sleep(Math.min(interval * 2, 30000)); // Max 30 seconds between retries
333+
continue;
334+
}
335+
}
165336
}
166337
}
167338

@@ -450,13 +621,6 @@ export interface KnowledgeBaseListParams {
450621
KnowledgeBases.DataSources = DataSources;
451622
KnowledgeBases.IndexingJobs = IndexingJobs;
452623

453-
export {
454-
waitForDatabase,
455-
WaitForDatabaseOptions,
456-
WaitForDatabaseTimeoutError,
457-
WaitForDatabaseFailedError,
458-
} from './wait-for-database';
459-
460624
export declare namespace KnowledgeBases {
461625
export {
462626
type APIKnowledgeBase as APIKnowledgeBase,

0 commit comments

Comments
 (0)