feat(nodejs): pause and resume jobs from the TypeScript SDK (#4113)

Companion to #4112: `pauseJob` and `resumeJob` on `Connection`, posting
to the server's `/v1/jobs/pause` and `/v1/jobs/resume` endpoints. A
pause parks the job until it is resumed: its workers drain and stop, and
a resume re-queues it to pick work back up from checkpoints.

The outcome strings mirror the server's answers — `"pausing"`,
`"already_paused"`, or `"committing"` (a job finalizing its results
cannot be parked; retry shortly), and `"resumed"`, `"still_pausing"`, or
`"not_paused"` — so a caller can retry the transient refusals rather
than treat them as failures.

Stacked on #4112 (shares the Rust core change); draft until the
server-side endpoints reach production.
This commit is contained in:
Wyatt Alt
2026-09-24 02:35:03 +08:00
committed by GitHub
parent 1fc72427e3
commit 2ebd6142e4
4 changed files with 119 additions and 0 deletions
+44
View File
@@ -798,6 +798,28 @@ abstract openTable(
***
### pauseJob()
```ts
abstract pauseJob(jobId): Promise<string>
```
Pause a server-side job by id.
The job's workers drain and it stays parked until resumed. Resolves to
"pausing", "already_paused", or "committing" -- a job finalizing its
results cannot be parked; retry shortly.
#### Parameters
* **jobId**: `string`
#### Returns
`Promise`&lt;`string`&gt;
***
### renameTable()
```ts
@@ -831,6 +853,28 @@ a "not supported" error.
***
### resumeJob()
```ts
abstract resumeJob(jobId): Promise<string>
```
Resume a paused server-side job by id.
Its workers pick their work back up from checkpoints. Resolves to
"resumed", "still_pausing" -- the pause's worker drain is not confirmed
yet; retry shortly -- or "not_paused".
#### Parameters
* **jobId**: `string`
#### Returns
`Promise`&lt;`string`&gt;
***
### ~~tableNames()~~
#### tableNames(options)
+21
View File
@@ -1152,6 +1152,24 @@ describe("remote connection jobs surface", () => {
res
.writeHead(200, { "Content-Type": "application/json" })
.end('{"job_id": "job-1"}');
} else if (req.url === "/v1/jobs/pause") {
if (payload["job_id"] !== "job-1") {
res.writeHead(404).end("no such job");
return;
}
res
.writeHead(200, { "Content-Type": "application/json" })
.end('{"job_id": "job-1", "paused": true}');
} else if (req.url === "/v1/jobs/resume") {
if (payload["job_id"] !== "job-1") {
res.writeHead(404).end("no such job");
return;
}
res
.writeHead(200, { "Content-Type": "application/json" })
.end(
'{"job_id": "job-1", "resumed": false, "still_pausing": true}',
);
} else if (req.url === "/v1/jobs/query_events") {
queryEventsPayloads.push(payload);
res
@@ -1173,6 +1191,9 @@ describe("remote connection jobs surface", () => {
expect(await db.cancelJob("job-1")).toBe(true);
expect(await db.cancelJob("missing")).toBe(false);
expect(await db.pauseJob("job-1")).toEqual("pausing");
expect(await db.resumeJob("job-1")).toEqual("still_pausing");
// Opening a job hands back a populated handle; a missing one rejects.
await expect(db.openJob("missing")).rejects.toThrow("not found");
const finished = await db.openJob("job-2");
+26
View File
@@ -640,6 +640,24 @@ export abstract class Connection {
* such job exists. Cancelling an already-terminal job is a no-op success.
*/
abstract cancelJob(jobId: string): Promise<boolean>;
/**
* Pause a server-side job by id.
*
* The job's workers drain and it stays parked until resumed. Resolves to
* "pausing", "already_paused", or "committing" -- a job finalizing its
* results cannot be parked; retry shortly.
*/
abstract pauseJob(jobId: string): Promise<string>;
/**
* Resume a paused server-side job by id.
*
* Its workers pick their work back up from checkpoints. Resolves to
* "resumed", "still_pausing" -- the pause's worker drain is not confirmed
* yet; retry shortly -- or "not_paused".
*/
abstract resumeJob(jobId: string): Promise<string>;
}
/** @hideconstructor */
@@ -1035,6 +1053,14 @@ export class LocalConnection extends Connection {
async cancelJob(jobId: string): Promise<boolean> {
return this.inner.cancelJob(jobId);
}
async pauseJob(jobId: string): Promise<string> {
return this.inner.pauseJob(jobId);
}
async resumeJob(jobId: string): Promise<string> {
return this.inner.resumeJob(jobId);
}
}
/**
+28
View File
@@ -583,6 +583,34 @@ impl Connection {
self.get_inner()?.cancel_job(&job_id).await.default_error()
}
/// Pause a server-side job by id: its workers drain and it stays parked
/// until resumed. Returns "pausing", "already_paused", or "committing".
#[napi(catch_unwind)]
pub async fn pause_job(&self, job_id: String) -> napi::Result<String> {
let status = self.get_inner()?.pause_job(&job_id).await.default_error()?;
Ok(match status {
lancedb::database::PauseJobStatus::Pausing => "pausing".to_string(),
lancedb::database::PauseJobStatus::AlreadyPaused => "already_paused".to_string(),
lancedb::database::PauseJobStatus::Committing => "committing".to_string(),
})
}
/// Resume a paused server-side job by id. Returns "resumed",
/// "still_pausing", or "not_paused".
#[napi(catch_unwind)]
pub async fn resume_job(&self, job_id: String) -> napi::Result<String> {
let status = self
.get_inner()?
.resume_job(&job_id)
.await
.default_error()?;
Ok(match status {
lancedb::database::ResumeJobStatus::Resumed => "resumed".to_string(),
lancedb::database::ResumeJobStatus::StillPausing => "still_pausing".to_string(),
lancedb::database::ResumeJobStatus::NotPaused => "not_paused".to_string(),
})
}
#[napi(catch_unwind)]
/// Describe a namespace and return its properties.
pub async fn describe_namespace(