Skip to content

Commit 9c81c01

Browse files
authored
feat: replace polling with long-poll loop for wait endpoints (#745)
1 parent 8694d51 commit 9c81c01

21 files changed

Lines changed: 1081 additions & 276 deletions

.github/workflows/sdk-coverage.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ jobs:
5454
- name: Run smoke tests with coverage
5555
id: tests
5656
continue-on-error: true
57-
run: yarn test:objects-coverage 2>&1 | tee test-output.log
57+
run: |
58+
set -o pipefail
59+
yarn test:objects-coverage 2>&1 | tee test-output.log
5860
5961
- name: Upload coverage report
6062
uses: runloopai/upload-artifact@main

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,7 @@ export declare namespace Runloop {
756756
export type RunProfile = API.RunProfile;
757757
}
758758

759+
export { type LongPollRequestOptions, LongPollAbortError, PollingTimeoutError } from './lib/polling';
759760
export { toFile, fileFromPath } from './uploads';
760761
export {
761762
RunloopError,

src/lib/devbox-state.ts

Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { poll, PollingOptions } from './polling';
1+
import { longPollUntil } from './polling';
22

33
export interface DevboxStateWaitOptions<T> {
44
client: {
@@ -8,45 +8,37 @@ export interface DevboxStateWaitOptions<T> {
88
targetState: string;
99
statesToCheck: string[];
1010
transitionStates: string[];
11-
pollingOptions?: Partial<PollingOptions<T>> | undefined;
11+
/** Timeout in milliseconds for the long-poll operation. */
12+
timeoutMs?: number | undefined;
13+
/** Optional AbortSignal to cancel the long-poll loop externally. */
14+
signal?: AbortSignal | null | undefined;
1215
errorMessage: (id: string, actualState: string) => string;
1316
}
1417

1518
/**
1619
* Shared utility for waiting for a devbox to reach a specific state.
17-
* Uses the /wait_for_status endpoint with polling.
20+
* Uses the /wait_for_status long-poll endpoint.
1821
*/
1922
export async function awaitDevboxState<T extends { status: string }>(
2023
options: DevboxStateWaitOptions<T>,
2124
): Promise<T> {
22-
const { client, devboxId, targetState, statesToCheck, transitionStates, pollingOptions, errorMessage } =
23-
options;
25+
const { client, devboxId, targetState, statesToCheck, transitionStates, errorMessage } = options;
2426

25-
const longPoll = (): Promise<T> => {
26-
// This either returns a DevboxView when status matches one of statesToCheck;
27-
// Otherwise it throws an 408 error when times out.
28-
return client.post(`/v1/devboxes/${devboxId}/wait_for_status`, {
29-
body: { statuses: statesToCheck },
30-
});
31-
};
32-
33-
const finalResult = await poll(
34-
() => longPoll(),
35-
() => longPoll(),
27+
const finalResult = await longPollUntil(
28+
(signal) =>
29+
client.post(`/v1/devboxes/${devboxId}/wait_for_status`, {
30+
body: { statuses: statesToCheck },
31+
signal,
32+
// Per-request HTTP timeout must exceed the server's max long-poll hold (30s)
33+
// so the server's 408 always arrives before the client aborts the connection.
34+
// The longPollUntil AbortSignal enforces the caller's actual deadline.
35+
timeout: 600000,
36+
maxRetries: 0,
37+
}),
3638
{
37-
...pollingOptions,
38-
shouldStop: (result) => {
39-
return !transitionStates.includes(result.status);
40-
},
41-
onError: (error: any) => {
42-
if (error.status === 408) {
43-
// Return a placeholder result to continue polling
44-
return { status: transitionStates[0] } as T;
45-
}
46-
47-
// For any other error, rethrow it
48-
throw error;
49-
},
39+
timeoutMs: options.timeoutMs,
40+
shouldStop: (result) => !transitionStates.includes(result.status),
41+
signal: options.signal,
5042
},
5143
);
5244

0 commit comments

Comments
 (0)