@shardflux/sdk 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +73 -0
- package/README.md +228 -24
- package/dist/cell.d.ts +67 -2
- package/dist/cell.js +98 -8
- package/dist/client.d.ts +148 -22
- package/dist/client.js +237 -29
- package/dist/errors.d.ts +20 -0
- package/dist/errors.js +10 -0
- package/dist/generated/app-api.d.ts +10223 -5857
- package/dist/generated/cell-api.d.ts +140 -8
- package/dist/http.d.ts +30 -1
- package/dist/http.js +57 -3
- package/dist/index.d.ts +13 -9
- package/dist/index.js +5 -4
- package/dist/lifecycle.d.ts +48 -0
- package/dist/lifecycle.js +33 -0
- package/dist/progress.d.ts +166 -0
- package/dist/progress.js +240 -0
- package/dist/secrets.d.ts +83 -6
- package/dist/secrets.js +53 -1
- package/dist/templates.d.ts +279 -7
- package/dist/templates.js +216 -4
- package/dist/tokens.d.ts +8 -3
- package/dist/tokens.js +25 -13
- package/dist/tools.d.ts +17 -0
- package/dist/tools.js +5 -1
- package/dist/workspace.d.ts +112 -20
- package/dist/workspace.js +176 -10
- package/package.json +2 -1
package/dist/client.js
CHANGED
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
import { OperationFailedError, OperationTimeoutError, ShardfluxApiError } from "./errors.js";
|
|
2
|
-
import { HttpClient, SDK_VERSION, defaultSleep, randomId } from "./http.js";
|
|
2
|
+
import { HttpClient, SDK_VERSION, SERVER_WAIT_MAX_S, defaultFetch, defaultSleep, pollWithWait, randomId } from "./http.js";
|
|
3
3
|
import { Workspace } from "./workspace.js";
|
|
4
4
|
import { AuditApi } from "./audit.js";
|
|
5
5
|
import { EgressPolicyApi } from "./egress.js";
|
|
6
6
|
import { SecretsApi } from "./secrets.js";
|
|
7
|
-
import { TemplatesApi } from "./templates.js";
|
|
7
|
+
import { TemplatesApi, saveAsTemplateBody } from "./templates.js";
|
|
8
8
|
import { UsageApi } from "./usage.js";
|
|
9
9
|
import { VolumesApi } from "./volumes.js";
|
|
10
|
+
import { AFTER_WAIT, TRACE, runLifecycle } from "./lifecycle.js";
|
|
11
|
+
import { Trace, combineListeners, traced } from "./progress.js";
|
|
12
|
+
/**
|
|
13
|
+
* The workspace a key names (contracts §19.11): the live row (deleted_at null) when there is one, since at most one live
|
|
14
|
+
* workspace holds a key; otherwise the newest tombstone (ended sessions leave tombstones with the same key, and a
|
|
15
|
+
* deleted persistent key keeps its tombstone). Null when no row has exactly this key. Rows need not be sorted.
|
|
16
|
+
*/
|
|
17
|
+
export function pickByKey(rows, key) {
|
|
18
|
+
let tomb = null;
|
|
19
|
+
for (const r of rows) {
|
|
20
|
+
if (r.workspace_key !== key)
|
|
21
|
+
continue;
|
|
22
|
+
if (r.deleted_at === null)
|
|
23
|
+
return r;
|
|
24
|
+
// UUIDv7 ids sort by creation time: the greatest id is the newest tombstone.
|
|
25
|
+
if (tomb === null || r.id > tomb.id)
|
|
26
|
+
tomb = r;
|
|
27
|
+
}
|
|
28
|
+
return tomb;
|
|
29
|
+
}
|
|
10
30
|
const TERMINAL = new Set(['succeeded', 'failed', 'canceled']);
|
|
11
31
|
/** Races the injected sleep against the signal (the injected sleep itself may not be abortable). */
|
|
12
32
|
function abortableSleep(sleep, ms, signal) {
|
|
@@ -23,6 +43,8 @@ function abortableSleep(sleep, ms, signal) {
|
|
|
23
43
|
}
|
|
24
44
|
export class WorkspacesApi {
|
|
25
45
|
#ctx;
|
|
46
|
+
/** The cell endpoint of the last tool token seen: an open pre-connects to it while the server works (contracts §22.3). */
|
|
47
|
+
#cellHint = null;
|
|
26
48
|
constructor(ctx) {
|
|
27
49
|
this.#ctx = ctx;
|
|
28
50
|
}
|
|
@@ -39,6 +61,8 @@ export class WorkspacesApi {
|
|
|
39
61
|
* Opens a workspace by key: creates it from the template's latest published version on first
|
|
40
62
|
* use, reconnects (or resumes) afterwards; never resets an existing workspace. Waits until it is
|
|
41
63
|
* ready unless `wait: false`; on timeout throws OperationTimeoutError carrying the operation id.
|
|
64
|
+
* The timing of the open (client phases, retries, the operation's server timing) is on `workspace.lastTiming`, on
|
|
65
|
+
* `onProgress` as it happens, and on the error's `timing` when the open fails.
|
|
42
66
|
*/
|
|
43
67
|
async open(params) {
|
|
44
68
|
const body = { key: params.key, template: params.template };
|
|
@@ -50,29 +74,130 @@ export class WorkspacesApi {
|
|
|
50
74
|
body.agent_label = params.agentLabel;
|
|
51
75
|
if (params.tools !== undefined)
|
|
52
76
|
body.tools = params.tools;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
77
|
+
if (params.secrets !== undefined)
|
|
78
|
+
body.secrets = params.secrets;
|
|
79
|
+
if (params.lifetime !== undefined)
|
|
80
|
+
body.lifetime = params.lifetime;
|
|
81
|
+
const waitOpts = params.wait === false ? null : (params.wait ?? {});
|
|
82
|
+
const trace = new Trace('open', combineListeners(this.#ctx().onProgress, params.onProgress, waitOpts?.onProgress));
|
|
83
|
+
return traced(trace, async () => {
|
|
84
|
+
// The wait's signal also ends the held request (it can take up to 20 s), not only the polls after it.
|
|
85
|
+
const init = { json: body, idempotencyKey: params.idempotencyKey ?? randomId('open-'), onRetry: trace.onRetry, ...(waitOpts?.signal ? { signal: waitOpts.signal } : {}) };
|
|
86
|
+
const started = Date.now();
|
|
87
|
+
const timeoutMs = waitOpts?.timeoutMs ?? 300_000;
|
|
88
|
+
if (waitOpts && waitOpts.serverWait !== false) {
|
|
89
|
+
// Held open (contracts §22.3): the server answers once the operation is terminal (200 with the running workspace
|
|
90
|
+
// and a tool token) or the wait elapsed (202); a server without it answers 202 at once and the poll below runs.
|
|
91
|
+
const s = Math.min(SERVER_WAIT_MAX_S, Math.floor(timeoutMs / 1000));
|
|
92
|
+
if (s >= 1) {
|
|
93
|
+
init.headers = { prefer: `wait=${s}` };
|
|
94
|
+
init.timeoutMs = Math.max(this.#http.opts.timeoutMs, s * 1000 + 10_000);
|
|
95
|
+
}
|
|
96
|
+
this.#preconnect();
|
|
97
|
+
}
|
|
98
|
+
const held = init.headers?.prefer !== undefined;
|
|
99
|
+
trace.phase('request', held ? 'held' : null);
|
|
100
|
+
const res = await this.#http.jsonWithStatus('POST', '/v1/workspaces/open', init, this.#auth);
|
|
101
|
+
trace.workspaceId = res.body.workspace.id;
|
|
102
|
+
if (res.body.operation)
|
|
103
|
+
trace.observe(res.body.operation);
|
|
104
|
+
this.#noteToken(res.body.tool_token);
|
|
105
|
+
const wrapOpts = { agentLabel: params.agentLabel, tools: params.tools, token: res.body.tool_token, trace };
|
|
106
|
+
if (res.status === 200 || params.wait === false || res.body.operation === null)
|
|
107
|
+
return this.#wrap(res.body.workspace, wrapOpts);
|
|
108
|
+
if (TERMINAL.has(res.body.operation.state)) {
|
|
109
|
+
if (res.body.operation.state !== 'succeeded')
|
|
110
|
+
throw new OperationFailedError(res.body.operation);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
// A held open already spent part of the budget; without the preference the budget is the poll's (as before).
|
|
114
|
+
const wait = { ...(params.wait ?? {}), ...(held ? { timeoutMs: Math.max(1, timeoutMs - (Date.now() - started)) } : {}), [TRACE]: trace };
|
|
115
|
+
await this.waitForOperation(res.body.operation.id, wait);
|
|
116
|
+
}
|
|
117
|
+
// Ready: read the view and issue the first tool token in parallel (a 200 open returns both at once). A token
|
|
118
|
+
// failure (e.g. a key without tool permissions) is left to the first tool call, which fetches and reports it.
|
|
119
|
+
const id = res.body.workspace.id;
|
|
120
|
+
const [view, token] = await Promise.all([
|
|
121
|
+
trace.span('view', () => this.#getView(id, trace.onRetry)),
|
|
122
|
+
trace.span('token', () => this.#issueToken(id, params.agentLabel, params.tools, trace.onRetry)),
|
|
123
|
+
]);
|
|
124
|
+
return this.#wrap(view, { ...wrapOpts, token });
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
#noteToken(token) {
|
|
128
|
+
if (token?.cell_endpoint)
|
|
129
|
+
this.#cellHint = token.cell_endpoint;
|
|
59
130
|
}
|
|
60
131
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
132
|
+
* Opens a connection to the last known cell endpoint in the background (GET /healthz, body discarded), so the
|
|
133
|
+
* first tool call after the open reuses it instead of paying the TCP and TLS handshakes. Best effort: errors are
|
|
134
|
+
* ignored, and a fetch that closes connections (Connection: close) simply gains nothing.
|
|
135
|
+
*/
|
|
136
|
+
#preconnect() {
|
|
137
|
+
const hint = this.#cellHint;
|
|
138
|
+
if (!hint)
|
|
139
|
+
return;
|
|
140
|
+
const { fetch: f, userAgent } = this.#ctx();
|
|
141
|
+
let url;
|
|
142
|
+
try {
|
|
143
|
+
url = new URL('/healthz', hint).toString();
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
void f(url, { method: 'GET', headers: { 'user-agent': userAgent }, signal: AbortSignal.timeout(5_000) })
|
|
149
|
+
.then((r) => r.body?.cancel())
|
|
150
|
+
.catch(() => undefined);
|
|
151
|
+
}
|
|
152
|
+
async #issueToken(workspaceId, agentLabel, tools, onRetry) {
|
|
153
|
+
const body = {};
|
|
154
|
+
if (agentLabel !== undefined)
|
|
155
|
+
body.agent_label = agentLabel;
|
|
156
|
+
if (tools !== undefined)
|
|
157
|
+
body.tools = [...tools];
|
|
158
|
+
try {
|
|
159
|
+
const token = await this.#http.json('POST', `/v1/workspaces/${encodeURIComponent(workspaceId)}/tool-tokens`, { json: body, ...(onRetry ? { onRetry } : {}) }, this.#auth);
|
|
160
|
+
this.#noteToken(token);
|
|
161
|
+
return token;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Waits for an operation: each GET /v1/operations/{id} asks the server to hold the response until the state changes
|
|
169
|
+
* (`Prefer: wait`, at most 20 s, contracts §3), so completion is seen within one notification of the commit. A server
|
|
170
|
+
* that does not wait is polled with bounded exponential backoff (+-20 % jitter). Resolves when the operation
|
|
171
|
+
* succeeds; throws OperationFailedError when it fails or is canceled, OperationTimeoutError after `timeoutMs` (the
|
|
172
|
+
* operation keeps running and can be awaited again). Both errors carry the wait's `timing`; `onProgress` sees each
|
|
173
|
+
* state change (queued, capacity_pending, running, with the server's reason) as it is observed.
|
|
64
174
|
*/
|
|
65
175
|
async waitForOperation(operationId, opts = {}) {
|
|
66
|
-
const
|
|
176
|
+
const inherited = opts[TRACE];
|
|
177
|
+
const trace = inherited ?? new Trace('wait', combineListeners(this.#ctx().onProgress, opts.onProgress), { operationId });
|
|
178
|
+
const run = () => this.#poll(operationId, opts, trace);
|
|
179
|
+
if (inherited)
|
|
180
|
+
return run();
|
|
181
|
+
// A wait on its own starts with its first poll: a held poll can take seconds before the first state is known.
|
|
182
|
+
trace.phase('request');
|
|
183
|
+
return traced(trace, run);
|
|
184
|
+
}
|
|
185
|
+
async #poll(operationId, opts, trace) {
|
|
186
|
+
const { sleep, http, authorization } = this.#ctx();
|
|
67
187
|
const timeoutMs = opts.timeoutMs ?? 300_000;
|
|
68
188
|
const maxInterval = opts.maxPollIntervalMs ?? 5_000;
|
|
69
189
|
let interval = opts.pollIntervalMs ?? 250;
|
|
70
190
|
const started = Date.now();
|
|
71
191
|
const aborted = () => (opts.signal?.reason instanceof Error ? opts.signal.reason : new Error('aborted'));
|
|
192
|
+
let lastKey = null;
|
|
72
193
|
for (;;) {
|
|
73
194
|
if (opts.signal?.aborted)
|
|
74
195
|
throw aborted();
|
|
75
|
-
const
|
|
196
|
+
const waitS = opts.serverWait === false ? 0 : (timeoutMs - (Date.now() - started)) / 1000;
|
|
197
|
+
const t0 = Date.now();
|
|
198
|
+
const { body, applied } = await pollWithWait(http, `/v1/operations/${encodeURIComponent(operationId)}`, authorization, waitS, opts.signal, trace.onRetry);
|
|
199
|
+
const operation = body.operation;
|
|
200
|
+
trace.observe(operation);
|
|
76
201
|
if (operation.state === 'succeeded')
|
|
77
202
|
return operation;
|
|
78
203
|
if (TERMINAL.has(operation.state))
|
|
@@ -80,6 +205,13 @@ export class WorkspacesApi {
|
|
|
80
205
|
const waited = Date.now() - started;
|
|
81
206
|
if (waited >= timeoutMs)
|
|
82
207
|
throw new OperationTimeoutError(operation, waited);
|
|
208
|
+
const key = `${operation.state}/${operation.state_reason ?? ''}`;
|
|
209
|
+
// The server held the poll (or answered a change): ask again at once. An applied wait that returned quickly with
|
|
210
|
+
// no change falls through to the backoff, so a misbehaving server can never make this loop spin.
|
|
211
|
+
const changed = key !== lastKey;
|
|
212
|
+
lastKey = key;
|
|
213
|
+
if (applied && (changed || Date.now() - t0 >= 1_000))
|
|
214
|
+
continue;
|
|
83
215
|
const jitter = interval * 0.2 * (Math.random() * 2 - 1);
|
|
84
216
|
const delay = Math.max(10, Math.min(interval + jitter, timeoutMs - waited));
|
|
85
217
|
// The wait itself is abortable: a signal ends it at once instead of after the sleep.
|
|
@@ -94,8 +226,8 @@ export class WorkspacesApi {
|
|
|
94
226
|
const { operation } = await this.#http.json('GET', `/v1/operations/${encodeURIComponent(operationId)}`, opts.signal ? { signal: opts.signal } : {}, this.#auth);
|
|
95
227
|
return operation;
|
|
96
228
|
}
|
|
97
|
-
async #getView(workspaceId) {
|
|
98
|
-
return this.#http.json('GET', `/v1/workspaces/${encodeURIComponent(workspaceId)}`, {}, this.#auth);
|
|
229
|
+
async #getView(workspaceId, onRetry) {
|
|
230
|
+
return this.#http.json('GET', `/v1/workspaces/${encodeURIComponent(workspaceId)}`, onRetry ? { onRetry } : {}, this.#auth);
|
|
99
231
|
}
|
|
100
232
|
async get(workspaceId, opts = {}) {
|
|
101
233
|
return this.#wrap(await this.#getView(workspaceId), opts);
|
|
@@ -109,6 +241,8 @@ export class WorkspacesApi {
|
|
|
109
241
|
project_id: params.projectId,
|
|
110
242
|
organization_id: params.organizationId,
|
|
111
243
|
include_deleted: params.includeDeleted,
|
|
244
|
+
lifetime: params.lifetime,
|
|
245
|
+
purpose: params.purpose,
|
|
112
246
|
limit: params.limit,
|
|
113
247
|
cursor: params.cursor,
|
|
114
248
|
},
|
|
@@ -124,25 +258,98 @@ export class WorkspacesApi {
|
|
|
124
258
|
cursor = page.nextCursor ?? undefined;
|
|
125
259
|
} while (cursor);
|
|
126
260
|
}
|
|
127
|
-
|
|
128
|
-
|
|
261
|
+
/**
|
|
262
|
+
* Looks a workspace up by its exact key across every lifetime and purpose (`lifetime=any&purpose=any`), preferring the
|
|
263
|
+
* live workspace over tombstones of ended sessions or deleted workspaces with the same key (see pickByKey). Null when
|
|
264
|
+
* no workspace of the project has the key. This is the lookup the CLI and the MCP server use.
|
|
265
|
+
*/
|
|
266
|
+
async findByKey(key, opts = {}) {
|
|
267
|
+
const includeDeleted = opts.includeDeleted !== false;
|
|
268
|
+
const seen = [];
|
|
269
|
+
let cursor;
|
|
270
|
+
do {
|
|
271
|
+
if (opts.signal?.aborted)
|
|
272
|
+
throw opts.signal.reason instanceof Error ? opts.signal.reason : new Error('aborted');
|
|
273
|
+
const page = await this.#http.json('GET', '/v1/workspaces', {
|
|
274
|
+
query: {
|
|
275
|
+
key_prefix: key,
|
|
276
|
+
lifetime: 'any',
|
|
277
|
+
purpose: 'any',
|
|
278
|
+
include_deleted: includeDeleted,
|
|
279
|
+
project_id: opts.projectId,
|
|
280
|
+
organization_id: opts.organizationId,
|
|
281
|
+
limit: 200,
|
|
282
|
+
cursor,
|
|
283
|
+
},
|
|
284
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
285
|
+
}, this.#auth);
|
|
286
|
+
const live = page.data.find((v) => v.workspace_key === key && v.deleted_at === null);
|
|
287
|
+
if (live)
|
|
288
|
+
return this.#wrap(live, { agentLabel: opts.agentLabel, tools: opts.tools });
|
|
289
|
+
seen.push(...page.data.filter((v) => v.workspace_key === key));
|
|
290
|
+
cursor = page.next_cursor ?? undefined;
|
|
291
|
+
} while (cursor);
|
|
292
|
+
const hit = pickByKey(seen, key);
|
|
293
|
+
return hit ? this.#wrap(hit, { agentLabel: opts.agentLabel, tools: opts.tools }) : null;
|
|
294
|
+
}
|
|
295
|
+
async #lifecycle(method, path, json, idempotencyKey, init = {}) {
|
|
296
|
+
return this.#http.json(method, path, { ...(json === undefined ? {} : { json }), idempotencyKey: idempotencyKey ?? randomId('op-'), ...init }, this.#auth);
|
|
297
|
+
}
|
|
298
|
+
#op(kind, workspaceId, json, opts) {
|
|
299
|
+
const path = `/v1/workspaces/${encodeURIComponent(workspaceId)}${kind === 'delete' ? '' : `/${kind}`}`;
|
|
300
|
+
return runLifecycle(this.#ctx(), kind, workspaceId, async (init) => (await this.#lifecycle(kind === 'delete' ? 'DELETE' : 'POST', path, json, opts.idempotencyKey, init)).operation, opts);
|
|
129
301
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
return (await this.#lifecycle('DELETE', `/v1/workspaces/${encodeURIComponent(workspaceId)}`, undefined, opts.idempotencyKey)).operation;
|
|
302
|
+
delete(workspaceId, opts = {}) {
|
|
303
|
+
return this.#op('delete', workspaceId, undefined, opts);
|
|
133
304
|
}
|
|
134
|
-
|
|
135
|
-
return
|
|
305
|
+
suspend(workspaceId, opts = {}) {
|
|
306
|
+
return this.#op('suspend', workspaceId, undefined, opts);
|
|
136
307
|
}
|
|
137
|
-
|
|
138
|
-
return
|
|
308
|
+
resume(workspaceId, opts = {}) {
|
|
309
|
+
return this.#op('resume', workspaceId, undefined, opts);
|
|
139
310
|
}
|
|
140
|
-
|
|
141
|
-
return
|
|
311
|
+
snapshot(workspaceId, opts = {}) {
|
|
312
|
+
return this.#op('snapshot', workspaceId, opts.label === undefined ? {} : { label: opts.label }, opts);
|
|
313
|
+
}
|
|
314
|
+
async close(workspaceId, opts = {}) {
|
|
315
|
+
return (await this.closeWithView(workspaceId, opts)).operation;
|
|
316
|
+
}
|
|
317
|
+
/** close() plus the workspace view after the close (a tombstone). */
|
|
318
|
+
async closeWithView(workspaceId, opts = {}) {
|
|
319
|
+
let workspace;
|
|
320
|
+
const operation = await runLifecycle(this.#ctx(), 'close', workspaceId, async (init) => {
|
|
321
|
+
const out = await this.#lifecycle('POST', `/v1/workspaces/${encodeURIComponent(workspaceId)}/close`, undefined, opts.idempotencyKey, init);
|
|
322
|
+
workspace = out.workspace;
|
|
323
|
+
return out.operation;
|
|
324
|
+
}, opts);
|
|
325
|
+
return { operation, workspace: workspace };
|
|
326
|
+
}
|
|
327
|
+
reset(workspaceId, opts = {}) {
|
|
328
|
+
const body = { confirm_destructive: true };
|
|
329
|
+
return this.#op('reset', workspaceId, body, opts);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Saves a layered workspace as the next version of an organization template (contracts §19.8). A running workspace is
|
|
333
|
+
* captured briefly (`operation`, layer_snapshot); poll `build` with templates.builds.waitForBuild. Owners/admins and
|
|
334
|
+
* API keys with a tool permission only.
|
|
335
|
+
*/
|
|
336
|
+
saveAsTemplate(workspaceId, params) {
|
|
337
|
+
return this.#http.json('POST', `/v1/workspaces/${encodeURIComponent(workspaceId)}/save-as-template`, { json: saveAsTemplateBody(params), idempotencyKey: params.idempotencyKey ?? randomId('save-') }, this.#auth);
|
|
142
338
|
}
|
|
143
339
|
async fork(workspaceId, target, opts = {}) {
|
|
144
|
-
|
|
145
|
-
|
|
340
|
+
let copy;
|
|
341
|
+
const operation = await runLifecycle(this.#ctx(), 'fork', workspaceId, async (init) => {
|
|
342
|
+
const out = await this.#lifecycle('POST', `/v1/workspaces/${encodeURIComponent(workspaceId)}/fork`, target, opts.idempotencyKey, init);
|
|
343
|
+
copy = this.#wrap(out.workspace);
|
|
344
|
+
return out.operation;
|
|
345
|
+
}, {
|
|
346
|
+
...opts,
|
|
347
|
+
[AFTER_WAIT]: async (trace, op) => {
|
|
348
|
+
await trace.span('view', () => copy.refresh());
|
|
349
|
+
await opts[AFTER_WAIT]?.(trace, op);
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
return { operation, workspace: copy };
|
|
146
353
|
}
|
|
147
354
|
async operations(workspaceId, params = {}) {
|
|
148
355
|
const page = await this.#http.json('GET', `/v1/workspaces/${encodeURIComponent(workspaceId)}/operations`, { query: { limit: params.limit, cursor: params.cursor, state: params.state, kind: params.kind } }, this.#auth);
|
|
@@ -203,7 +410,7 @@ export class Shardflux {
|
|
|
203
410
|
constructor(opts) {
|
|
204
411
|
if (!/^sfk_[a-z2-7]{16}_[A-Za-z0-9]+$/.test(opts.apiKey))
|
|
205
412
|
throw new Error('apiKey must be a Shardflux project key (sfk_<key_id>_<secret>)');
|
|
206
|
-
const f = opts.fetch ??
|
|
413
|
+
const f = opts.fetch ?? defaultFetch();
|
|
207
414
|
const userAgent = opts.userAgent ?? `shardflux-sdk-ts/${SDK_VERSION}`;
|
|
208
415
|
const sleep = opts.sleep ?? defaultSleep;
|
|
209
416
|
this.workspaces = new WorkspacesApi(() => this.#ctx);
|
|
@@ -221,6 +428,7 @@ export class Shardflux {
|
|
|
221
428
|
userAgent,
|
|
222
429
|
sleep,
|
|
223
430
|
workspaces: this.workspaces,
|
|
431
|
+
onProgress: opts.onProgress,
|
|
224
432
|
};
|
|
225
433
|
}
|
|
226
434
|
/** The authenticated principal (the API key, its organization and project). */
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import type { components as AppComponents } from './generated/app-api.js';
|
|
2
2
|
import type { components as CellComponents } from './generated/cell-api.js';
|
|
3
|
+
import type { LifecycleTiming } from './progress.js';
|
|
3
4
|
export type AppErrorBody = AppComponents['schemas']['ErrorBody'];
|
|
4
5
|
export type AppErrorCode = AppErrorBody['error']['code'];
|
|
5
6
|
export type CellErrorCode = CellComponents['schemas']['ErrorCode'];
|
|
6
7
|
/** Closed error codes of the application API and the cell gateway (both published in OpenAPI). */
|
|
7
8
|
export type ErrorCode = AppErrorCode | CellErrorCode;
|
|
9
|
+
/**
|
|
10
|
+
* `details.reason` values the SDK knows (the error code enum is closed; new cases add reasons). Templates v2
|
|
11
|
+
* (contracts §19.14) added: 409 conflict legacy_disk_layout, not_session, session_lifetime, lifetime_mismatch,
|
|
12
|
+
* not_resettable, template_not_layered, draft_exists, draft_stale, build_in_progress, file_list_unavailable,
|
|
13
|
+
* file_list_indexing (retryable), guest_feature_unavailable; 422 validation_failed confirm_destructive_required,
|
|
14
|
+
* reserved_key_prefix, invalid_defaults, update_policy_not_available, invalid_path, too_many_acknowledged_findings;
|
|
15
|
+
* 403 forbidden template_dev_mode_role; 404 not_found draft_not_found, version_not_found, path_not_found.
|
|
16
|
+
*/
|
|
17
|
+
export type KnownErrorReason = 'workspace_not_running' | 'operation_in_progress' | 'workspace_deleted' | 'secret_not_available' | 'legacy_disk_layout' | 'not_session' | 'session_lifetime' | 'lifetime_mismatch' | 'not_resettable' | 'template_not_layered' | 'draft_exists' | 'draft_stale' | 'build_in_progress' | 'file_list_unavailable' | 'file_list_indexing' | 'guest_feature_unavailable' | 'confirm_destructive_required' | 'reserved_key_prefix' | 'invalid_defaults' | 'update_policy_not_available' | 'invalid_path' | 'too_many_acknowledged_findings' | 'template_dev_mode_role' | 'draft_not_found' | 'version_not_found' | 'path_not_found';
|
|
18
|
+
/** A known reason, or any other string the server sends (reasons are open-ended). */
|
|
19
|
+
export type ErrorReason = KnownErrorReason | (string & {});
|
|
8
20
|
export interface ErrorBodyLike {
|
|
9
21
|
error: {
|
|
10
22
|
code: string;
|
|
@@ -26,6 +38,10 @@ export declare class ShardfluxApiError extends Error {
|
|
|
26
38
|
readonly operationId: string | undefined;
|
|
27
39
|
readonly source: 'api' | 'cell';
|
|
28
40
|
readonly retryAfterSeconds: number | undefined;
|
|
41
|
+
/** `details.reason` when it is a string (e.g. `not_session`, `draft_not_found`, `legacy_disk_layout`). */
|
|
42
|
+
readonly reason: ErrorReason | undefined;
|
|
43
|
+
/** Where the time went when a traced call (open, a waited lifecycle call, a wake) failed with this error. */
|
|
44
|
+
timing: LifecycleTiming | undefined;
|
|
29
45
|
constructor(status: number, body: ErrorBodyLike, source: 'api' | 'cell', retryAfterSeconds?: number);
|
|
30
46
|
}
|
|
31
47
|
/** The response was not the documented shape (e.g. a proxy error page). */
|
|
@@ -45,6 +61,8 @@ export declare class OperationTimeoutError extends Error {
|
|
|
45
61
|
readonly lastState: Operation['state'];
|
|
46
62
|
readonly lastReason: string | null;
|
|
47
63
|
readonly waitedMs: number;
|
|
64
|
+
/** Where the time went: client phases, retries and the operation's own server timing so far. */
|
|
65
|
+
timing: LifecycleTiming | undefined;
|
|
48
66
|
constructor(op: Operation, waitedMs: number);
|
|
49
67
|
}
|
|
50
68
|
/** The operation reached `failed` or `canceled`. */
|
|
@@ -52,6 +70,8 @@ export declare class OperationFailedError extends Error {
|
|
|
52
70
|
readonly operation: Operation;
|
|
53
71
|
readonly operationId: string;
|
|
54
72
|
readonly errorCode: string | null;
|
|
73
|
+
/** Where the time went before the operation failed. */
|
|
74
|
+
timing: LifecycleTiming | undefined;
|
|
55
75
|
constructor(op: Operation);
|
|
56
76
|
}
|
|
57
77
|
export {};
|
package/dist/errors.js
CHANGED
|
@@ -14,6 +14,10 @@ export class ShardfluxApiError extends Error {
|
|
|
14
14
|
operationId;
|
|
15
15
|
source;
|
|
16
16
|
retryAfterSeconds;
|
|
17
|
+
/** `details.reason` when it is a string (e.g. `not_session`, `draft_not_found`, `legacy_disk_layout`). */
|
|
18
|
+
reason;
|
|
19
|
+
/** Where the time went when a traced call (open, a waited lifecycle call, a wake) failed with this error. */
|
|
20
|
+
timing = undefined;
|
|
17
21
|
constructor(status, body, source, retryAfterSeconds) {
|
|
18
22
|
super(body.error.message);
|
|
19
23
|
this.name = 'ShardfluxApiError';
|
|
@@ -25,6 +29,8 @@ export class ShardfluxApiError extends Error {
|
|
|
25
29
|
this.operationId = body.error.operation_id;
|
|
26
30
|
this.source = source;
|
|
27
31
|
this.retryAfterSeconds = retryAfterSeconds;
|
|
32
|
+
const reason = body.error.details?.reason;
|
|
33
|
+
this.reason = typeof reason === 'string' ? reason : undefined;
|
|
28
34
|
}
|
|
29
35
|
}
|
|
30
36
|
/** The response was not the documented shape (e.g. a proxy error page). */
|
|
@@ -47,6 +53,8 @@ export class OperationTimeoutError extends Error {
|
|
|
47
53
|
lastState;
|
|
48
54
|
lastReason;
|
|
49
55
|
waitedMs;
|
|
56
|
+
/** Where the time went: client phases, retries and the operation's own server timing so far. */
|
|
57
|
+
timing = undefined;
|
|
50
58
|
constructor(op, waitedMs) {
|
|
51
59
|
super(`Operation ${op.id} (${op.kind}) is still ${op.state}${op.state_reason ? ` (${op.state_reason})` : ''} after ${Math.round(waitedMs)} ms; it continues server side.`);
|
|
52
60
|
this.name = 'OperationTimeoutError';
|
|
@@ -62,6 +70,8 @@ export class OperationFailedError extends Error {
|
|
|
62
70
|
operation;
|
|
63
71
|
operationId;
|
|
64
72
|
errorCode;
|
|
73
|
+
/** Where the time went before the operation failed. */
|
|
74
|
+
timing = undefined;
|
|
65
75
|
constructor(op) {
|
|
66
76
|
const code = typeof op.error?.code === 'string' ? op.error.code : null;
|
|
67
77
|
super(`Operation ${op.id} (${op.kind}) ${op.state}${code ? `: ${code}` : ''}`);
|