Skip to content

Commit 56dfdbc

Browse files
committed
add ts sdk
1 parent 19e8883 commit 56dfdbc

4 files changed

Lines changed: 196 additions & 21 deletions

File tree

docs/i18n/zh-Hans/docusaurus-plugin-content-docs/version-1.11.x/User Guides/commit.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,35 @@ async def commit_and_wait(sandbox):
178178
```
179179

180180
当调用方不希望等待,或需要自行控制轮询逻辑时,可直接使用 `commit_async()`。该方法返回任务的初始状态,后续状态通过 `get_commit_status()` 查询。
181+
182+
## TypeScript SDK
183+
184+
使用 `commit()` 发起任务并等待最终状态。该方法内部调用 `commitAsync()`,并轮询 `getCommitStatus()``timeout``interval` 参数的单位为秒,默认值分别为 `180``2`
185+
186+
```typescript
187+
import { CommitPhase } from 'rl-rock';
188+
189+
async function commitAndWait(sandbox) {
190+
const status = await sandbox.commit(
191+
'registry.example.com/team/app:v1',
192+
'registry-user',
193+
'registry-password',
194+
180,
195+
2
196+
);
197+
if (!status) {
198+
throw new Error('sandbox_id 未设置');
199+
}
200+
201+
if (status.phase === CommitPhase.FAILED) {
202+
throw new Error(
203+
`commit 失败: code=${status.errorCode}, ` +
204+
`stage=${status.failedStage}, message=${status.errorMessage}`
205+
);
206+
}
207+
208+
console.log(`镜像推送成功: ${status.imageTag}`);
209+
}
210+
```
211+
212+
超时只会停止 SDK 端的等待,不会取消 worker 上已经运行的任务。调用方需要自行控制轮询时可使用 `commitAsync()`,之后通过 `getCommitStatus()` 查询任务状态。

docs/versioned_docs/version-1.11.x/User Guides/commit.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,35 @@ async def commit_and_wait(sandbox):
178178
```
179179

180180
Use `commit_async()` directly when the caller must not wait or needs custom polling behavior. Its return value is the initial task status; call `get_commit_status()` to query subsequent status.
181+
182+
## TypeScript SDK
183+
184+
Use `commit()` to start the task and wait for its final status. It calls `commitAsync()` internally and polls `getCommitStatus()`. The `timeout` and `interval` arguments use seconds and default to `180` and `2`.
185+
186+
```typescript
187+
import { CommitPhase } from 'rl-rock';
188+
189+
async function commitAndWait(sandbox) {
190+
const status = await sandbox.commit(
191+
'registry.example.com/team/app:v1',
192+
'registry-user',
193+
'registry-password',
194+
180,
195+
2
196+
);
197+
if (!status) {
198+
throw new Error('sandbox_id is not set');
199+
}
200+
201+
if (status.phase === CommitPhase.FAILED) {
202+
throw new Error(
203+
`commit failed: code=${status.errorCode}, ` +
204+
`stage=${status.failedStage}, message=${status.errorMessage}`
205+
);
206+
}
207+
208+
console.log(`image pushed: ${status.imageTag}`);
209+
}
210+
```
211+
212+
A timeout only stops the SDK from waiting; it does not cancel the worker task. Use `commitAsync()` when the caller needs to control polling, and call `getCommitStatus()` to query the task later.

rock/ts-sdk/src/sandbox/client.test.ts

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1172,4 +1172,85 @@ describe('uploadByPath() with uploadMode', () => {
11721172

11731173
expect(result).toBeDefined();
11741174
});
1175-
});
1175+
});
1176+
1177+
describe('Sandbox commit()', () => {
1178+
afterEach(() => {
1179+
jest.useRealTimers();
1180+
});
1181+
1182+
test('starts an asynchronous commit and polls until completion', async () => {
1183+
jest.useFakeTimers();
1184+
const sandbox = new Sandbox({ image: 'test:latest' });
1185+
const running = {
1186+
sandboxId: 'sandbox-1',
1187+
imageTag: 'registry.example/app:v1',
1188+
phase: 'RUNNING' as const,
1189+
startedAt: '2026-01-01T00:00:00Z',
1190+
completedAt: null,
1191+
exitCode: null,
1192+
failedStage: null,
1193+
errorCode: null,
1194+
errorMessage: null,
1195+
};
1196+
const succeeded = {
1197+
...running,
1198+
phase: 'SUCCEEDED' as const,
1199+
completedAt: '2026-01-01T00:00:05Z',
1200+
exitCode: 0,
1201+
};
1202+
const commitAsync = jest.spyOn(sandbox, 'commitAsync').mockResolvedValue(running);
1203+
const getCommitStatus = jest.spyOn(sandbox, 'getCommitStatus').mockResolvedValue(succeeded);
1204+
1205+
const resultPromise = sandbox.commit(
1206+
'registry.example/app:v1',
1207+
'registry-user',
1208+
'registry-password',
1209+
10,
1210+
0.25
1211+
);
1212+
await Promise.resolve();
1213+
await jest.advanceTimersByTimeAsync(250);
1214+
1215+
await expect(resultPromise).resolves.toEqual(succeeded);
1216+
expect(commitAsync).toHaveBeenCalledWith(
1217+
'registry.example/app:v1',
1218+
'registry-user',
1219+
'registry-password'
1220+
);
1221+
expect(getCommitStatus).toHaveBeenCalledTimes(1);
1222+
});
1223+
1224+
test('uses the default interval and rejects when the default timeout expires', async () => {
1225+
jest.useFakeTimers();
1226+
const sandbox = new Sandbox({ image: 'test:latest' });
1227+
const running = {
1228+
sandboxId: 'sandbox-1',
1229+
imageTag: 'registry.example/app:v1',
1230+
phase: 'RUNNING' as const,
1231+
startedAt: '2026-01-01T00:00:00Z',
1232+
completedAt: null,
1233+
exitCode: null,
1234+
failedStage: null,
1235+
errorCode: null,
1236+
errorMessage: null,
1237+
};
1238+
jest.spyOn(sandbox, 'commitAsync').mockResolvedValue(running);
1239+
const getCommitStatus = jest.spyOn(sandbox, 'getCommitStatus').mockResolvedValue(running);
1240+
1241+
const resultPromise = sandbox.commit(
1242+
'registry.example/app:v1',
1243+
'registry-user',
1244+
'registry-password'
1245+
);
1246+
const rejection = expect(resultPromise).rejects.toThrow('Commit timed out after 180 seconds');
1247+
await Promise.resolve();
1248+
await jest.advanceTimersByTimeAsync(1999);
1249+
expect(getCommitStatus).not.toHaveBeenCalled();
1250+
await jest.advanceTimersByTimeAsync(1);
1251+
expect(getCommitStatus).toHaveBeenCalledTimes(1);
1252+
await jest.advanceTimersByTimeAsync(178000);
1253+
1254+
await rejection;
1255+
});
1256+
});

rock/ts-sdk/src/sandbox/client.ts

Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,13 @@ export abstract class AbstractSandbox {
120120
abstract delete(): Promise<void>;
121121
abstract restart(): Promise<void>;
122122
abstract archive(): Promise<void>;
123-
abstract commit(imageTag: string, username: string, password: string): Promise<CommandResponse | undefined>;
123+
abstract commit(
124+
imageTag: string,
125+
username: string,
126+
password: string,
127+
timeout?: number,
128+
interval?: number
129+
): Promise<CommitStatusResponse | undefined>;
124130
abstract commitAsync(imageTag: string, username: string, password: string): Promise<CommitStatusResponse | undefined>;
125131
abstract getCommitStatus(): Promise<CommitStatusResponse | undefined>;
126132
abstract attach(sandboxId: string): Promise<void>;
@@ -511,32 +517,56 @@ export class Sandbox extends AbstractSandbox {
511517
}
512518

513519
/**
514-
* Commit the sandbox container as a new Docker image.
520+
* Commit the sandbox container as a new Docker image and wait for completion.
515521
*
516522
* @param imageTag - Tag for the new image (e.g., "my-image:v1")
517523
* @param username - Registry username for authentication
518524
* @param password - Registry password for authentication
519-
* @returns CommandResponse with stdout, stderr, and exit_code from the commit operation,
520-
* or undefined if sandbox_id is not set.
525+
* @param timeout - Maximum total wait time in seconds
526+
* @param interval - Delay between status queries in seconds
527+
* @returns Final task status, or undefined if sandbox_id is not set.
521528
*/
522-
async commit(imageTag: string, username: string, password: string): Promise<CommandResponse | undefined> {
523-
if (!this.sandboxId) {
524-
return;
525-
}
526-
const url = `${this.url}/commit`;
527-
const headers = this.buildHeaders();
528-
const data = {
529-
sandboxId: this.sandboxId,
530-
imageTag,
531-
username,
532-
password,
529+
async commit(
530+
imageTag: string,
531+
username: string,
532+
password: string,
533+
timeout: number = 180,
534+
interval: number = 2
535+
): Promise<CommitStatusResponse | undefined> {
536+
const deadline = Date.now() + timeout * 1000;
537+
const timeoutError = (): Error => new Error(`Commit timed out after ${timeout} seconds`);
538+
const waitWithTimeout = async <T>(operation: Promise<T>): Promise<T> => {
539+
const remaining = deadline - Date.now();
540+
if (remaining <= 0) {
541+
throw timeoutError();
542+
}
543+
544+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
545+
try {
546+
return await Promise.race([
547+
operation,
548+
new Promise<never>((_, reject) => {
549+
timeoutId = setTimeout(() => reject(timeoutError()), remaining);
550+
}),
551+
]);
552+
} finally {
553+
clearTimeout(timeoutId);
554+
}
533555
};
534-
const response = await HttpUtils.post<CommandResponse & { code?: number }>(url, headers, data);
535-
logger.debug(`Commit sandbox response: ${JSON.stringify(response)}`);
536-
if (response.status !== 'Success') {
537-
throw new Error(`Failed to execute command: ${JSON.stringify(response)}`);
556+
557+
let status = await waitWithTimeout(this.commitAsync(imageTag, username, password));
558+
while (status?.phase === 'RUNNING') {
559+
const remaining = deadline - Date.now();
560+
if (remaining <= 0) {
561+
throw timeoutError();
562+
}
563+
await sleep(Math.min(interval * 1000, remaining));
564+
if (Date.now() >= deadline) {
565+
throw timeoutError();
566+
}
567+
status = await waitWithTimeout(this.getCommitStatus());
538568
}
539-
return CommandResponseSchema.parse(response.result);
569+
return status;
540570
}
541571

542572
/**

0 commit comments

Comments
 (0)