@pipelex/sdk 0.52.0 → 0.53.0
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/README.md +17 -17
- package/dist/answer-reading.d.ts +70 -0
- package/dist/answer-reading.js +106 -0
- package/dist/answer-reading.js.map +1 -0
- package/dist/artifacts.js +45 -32
- package/dist/artifacts.js.map +1 -1
- package/dist/cli/present.d.ts +11 -14
- package/dist/cli/present.js +41 -37
- package/dist/cli/present.js.map +1 -1
- package/dist/cli/run.js +8 -7
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/script.d.ts +1 -1
- package/dist/cli/source.js +2 -2
- package/dist/cli/source.js.map +1 -1
- package/dist/client.d.ts +248 -218
- package/dist/client.js +538 -477
- package/dist/client.js.map +1 -1
- package/dist/codegen-check.d.ts +21 -15
- package/dist/codegen-check.js +54 -27
- package/dist/codegen-check.js.map +1 -1
- package/dist/errors.d.ts +298 -455
- package/dist/errors.js +426 -534
- package/dist/errors.js.map +1 -1
- package/dist/exact-json.d.ts +8 -12
- package/dist/exact-json.js +19 -12
- package/dist/exact-json.js.map +1 -1
- package/dist/hooks/check-core.d.ts +12 -7
- package/dist/hooks/check-core.js +20 -14
- package/dist/hooks/check-core.js.map +1 -1
- package/dist/hooks/validate-stage.js +12 -3
- package/dist/hooks/validate-stage.js.map +1 -1
- package/dist/index.d.ts +4 -5
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/dist/method-selector.js +7 -5
- package/dist/method-selector.js.map +1 -1
- package/dist/method-trees.d.ts +6 -7
- package/dist/method-trees.js +31 -37
- package/dist/method-trees.js.map +1 -1
- package/dist/models.d.ts +33 -210
- package/dist/models.js +2 -1
- package/dist/models.js.map +1 -1
- package/dist/prepare-inputs.d.ts +2 -2
- package/dist/prepare-inputs.js +51 -54
- package/dist/prepare-inputs.js.map +1 -1
- package/dist/problem-items.d.ts +19 -0
- package/dist/problem-items.js +52 -0
- package/dist/problem-items.js.map +1 -0
- package/dist/product-models.d.ts +23 -21
- package/dist/records.d.ts +12 -0
- package/dist/records.js +23 -0
- package/dist/records.js.map +1 -0
- package/dist/runs.d.ts +49 -38
- package/dist/runs.js +45 -19
- package/dist/runs.js.map +1 -1
- package/dist/transport-reading.d.ts +18 -0
- package/dist/{run-may-have-started.js → transport-reading.js} +15 -44
- package/dist/transport-reading.js.map +1 -0
- package/dist/upload-grant.d.ts +9 -6
- package/dist/upload-grant.js +42 -29
- package/dist/upload-grant.js.map +1 -1
- package/dist/upload.js +53 -33
- package/dist/upload.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/architecture.md +19 -19
- package/docs/artifact-download.md +6 -6
- package/docs/cli.md +1 -1
- package/docs/crate-routes.md +13 -14
- package/docs/errors.md +154 -267
- package/docs/exact-integers.md +1 -1
- package/docs/files.md +13 -13
- package/docs/hook-bundle.md +15 -1
- package/docs/input-preparation.md +14 -14
- package/docs/method-copy.md +24 -24
- package/docs/method-examples.md +13 -14
- package/docs/method-trees.md +27 -28
- package/docs/organization-branding.md +14 -16
- package/docs/run-results.md +8 -8
- package/package.json +1 -1
- package/dist/error-models.d.ts +0 -302
- package/dist/error-models.js +0 -203
- package/dist/error-models.js.map +0 -1
- package/dist/error-verdicts.d.ts +0 -46
- package/dist/error-verdicts.js +0 -120
- package/dist/error-verdicts.js.map +0 -1
- package/dist/run-may-have-started.d.ts +0 -31
- package/dist/run-may-have-started.js.map +0 -1
package/dist/client.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// Method-source predicates come from the standard, not a local restatement: which
|
|
2
2
|
// source combinations are legal is an invariant of `RunRequest` itself, so this
|
|
3
3
|
// client and the MTHDS runners cannot disagree about what they reject.
|
|
4
|
-
import { assertOneMethodSource, parseJsonBody } from "mthds/protocol";
|
|
5
|
-
import { assertArtifactSelection, assertWaitOptions, pollUntilResult, readRunDegradation, RUN_STATUSES, runProgressFault, runStatusFilter, selectionIncludesMainStuff, throwIfAborted,
|
|
6
|
-
import { ApiResponseError, ApiUnreachableError, MissingMainStuffError, PagingNotTerminatingError, PipelineExecuteTimeoutError,
|
|
7
|
-
import { isPlainObject
|
|
4
|
+
import { assertOneMethodSource, buildProblem, isProblem, parseJsonBody, readProblemBody, } from "mthds/protocol";
|
|
5
|
+
import { assertArtifactSelection, assertWaitOptions, pollUntilResult, readRunDegradation, RUN_STATUSES, runProgressFault, runStatusFilter, selectionIncludesMainStuff, throwIfAborted, withCheckedProgressHealth, } from "./runs.js";
|
|
6
|
+
import { ABORT_TIMEOUT, ApiResponseError, argumentInvalidProblem, ApiUnreachableError, MissingMainStuffError, PagingNotTerminatingError, PipelineExecuteTimeoutError, ProblemError, RequestArgumentError, RunLifecycleUnavailableError, RunStillRunningError, } from "./errors.js";
|
|
7
|
+
import { isPlainObject } from "./records.js";
|
|
8
|
+
import { readProblemItems } from "./problem-items.js";
|
|
9
|
+
import { outcomeUnknownProblem, readHostedAnswer, runIdNamedBy } from "./answer-reading.js";
|
|
8
10
|
import { HANDSHAKE_TTL_MS, INLINE_TREE_MAX_BYTES, MAX_CONCURRENT_TRANSFERS, REQUEST_SENDING, SAVE_SENDING, announcedTreeLimits, contentsTreeSource, declaresBlobStore, eitherSignal, emptyTreeRefusal, fileByBlob, forEachConcurrently, hashMethodFiles, inlineFilesOf, inlineMapBytes, inlineThreshold, isMethodTreeTooLarge, listDirectoryTree, listerLimitsOf, missingBlobsOf, presentMap, readTreeBytesInto, redactSignedLinks, sha256Hex, treeShapeFault, treeListerFor, treeTotalSize, uniqueBlobs, uploadBlob, writeMethodTreeTo, } from "./method-trees.js";
|
|
9
11
|
import { buildUserAgent } from "./user-agent.js";
|
|
10
12
|
import { uploadFile as uploadFileImpl } from "./upload.js";
|
|
@@ -141,9 +143,8 @@ const BARE_RUNNER_IMPLEMENTATION = "pipelex-api";
|
|
|
141
143
|
*
|
|
142
144
|
* Implements `MTHDSProtocol<DictPipeOutput>` so the protocol-execution methods
|
|
143
145
|
* stay shaped like the standard's wire surface (`mthds/protocol`), all but
|
|
144
|
-
* `validate`, whose verdict is the one the hosted plane answers:
|
|
145
|
-
*
|
|
146
|
-
* hosted plane does not send yet (see `PipelexValidationResult`). The Pipelex
|
|
146
|
+
* `validate`, whose verdict is the one the hosted plane answers: the protocol's problem items
|
|
147
|
+
* beside members of the hosted plane's own (see `PipelexValidationResult`). The Pipelex
|
|
147
148
|
* extensions (the crate and tools routes, the run lifecycle) ride on top.
|
|
148
149
|
*/
|
|
149
150
|
/**
|
|
@@ -161,7 +162,7 @@ function requestBodyText(method, url, body) {
|
|
|
161
162
|
catch (err) {
|
|
162
163
|
const reason = err instanceof Error ? err.message : String(err);
|
|
163
164
|
throw new RequestArgumentError(`The body of ${method} ${new URL(url).pathname} cannot be written as JSON, so nothing was ` +
|
|
164
|
-
`sent: ${reason}`, { cause: err });
|
|
165
|
+
`sent: ${reason}`, { problem: argumentInvalidProblem("body", "not_json"), cause: err });
|
|
165
166
|
}
|
|
166
167
|
}
|
|
167
168
|
/**
|
|
@@ -200,7 +201,7 @@ function repeatedFilter(call, name, values) {
|
|
|
200
201
|
throw new TypeError(`${call}() takes "${name}" as a list of strings.`);
|
|
201
202
|
}
|
|
202
203
|
if (values.length === 0) {
|
|
203
|
-
throw new RequestArgumentError(`${call}() takes "${name}" as one or more values; omit it to list every ${name}
|
|
204
|
+
throw new RequestArgumentError(`${call}() takes "${name}" as one or more values; omit it to list every ${name}.`, { problem: argumentInvalidProblem(name, "empty") });
|
|
204
205
|
}
|
|
205
206
|
return values;
|
|
206
207
|
}
|
|
@@ -211,40 +212,110 @@ function inWords(items, conjunction) {
|
|
|
211
212
|
return `${items.slice(0, -1).join(", ")} ${conjunction} ${items[items.length - 1]}`;
|
|
212
213
|
}
|
|
213
214
|
/** What each id a route puts in its path is, as a refusal of it names it. */
|
|
214
|
-
const METHOD_ID_ARGUMENT =
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
215
|
+
const METHOD_ID_ARGUMENT = {
|
|
216
|
+
name: "method_id",
|
|
217
|
+
what: "a method id, the method's non-empty bare catalog id",
|
|
218
|
+
};
|
|
219
|
+
const EXAMPLE_ID_ARGUMENT = {
|
|
220
|
+
name: "example_id",
|
|
221
|
+
what: "an example id, the example's non-empty `example_id`",
|
|
222
|
+
};
|
|
223
|
+
const RUN_ID_ARGUMENT = {
|
|
224
|
+
name: "run_id",
|
|
225
|
+
what: "a run id, the run's non-empty `pipeline_run_id`",
|
|
226
|
+
};
|
|
227
|
+
const ORG_ID_ARGUMENT = {
|
|
228
|
+
name: "org_id",
|
|
229
|
+
what: "an organization id, the organization's non-empty `org_id`",
|
|
230
|
+
};
|
|
231
|
+
const API_KEY_ID_ARGUMENT = {
|
|
232
|
+
name: "key_id",
|
|
233
|
+
what: "an API key id, the key's non-empty `id`",
|
|
234
|
+
};
|
|
235
|
+
const FILE_ID_ARGUMENT = {
|
|
236
|
+
name: "file_id",
|
|
237
|
+
what: "a file id, the file's non-empty `file_id`",
|
|
238
|
+
};
|
|
239
|
+
const ARCHIVE_ID_ARGUMENT = {
|
|
240
|
+
name: "archive_id",
|
|
241
|
+
what: "an archive id, the archive's non-empty `archive_id`",
|
|
242
|
+
};
|
|
221
243
|
/** What the run id of each example write is, as a refusal of it names it. */
|
|
222
|
-
const CREATE_RUN_ID_ARGUMENT =
|
|
223
|
-
|
|
244
|
+
const CREATE_RUN_ID_ARGUMENT = {
|
|
245
|
+
name: "run_id",
|
|
246
|
+
what: "a run_id naming a run of this method that used these inputs, a non-empty string, or none",
|
|
247
|
+
};
|
|
248
|
+
const RECORD_RUN_ID_ARGUMENT = {
|
|
249
|
+
name: "run_id",
|
|
250
|
+
what: "a run_id naming a run of this method that used the example's inputs, a non-empty string",
|
|
251
|
+
};
|
|
224
252
|
/** What the example an assistant save keeps current is, as a refusal of it names it. */
|
|
225
|
-
const ASSISTANT_EXAMPLE_ID_ARGUMENT =
|
|
226
|
-
|
|
253
|
+
const ASSISTANT_EXAMPLE_ID_ARGUMENT = {
|
|
254
|
+
name: "example_id",
|
|
255
|
+
what: "an example_id naming the assistant's example the caller read, a non-empty string, or none",
|
|
256
|
+
};
|
|
257
|
+
const ASSISTANT_REVISION_ARGUMENT = {
|
|
258
|
+
name: "input_revision",
|
|
259
|
+
what: "an input_revision, the revision the caller read the assistant's example at, a positive integer",
|
|
260
|
+
};
|
|
227
261
|
/** What each member of the files routes' bodies is, as a refusal of it names it. */
|
|
228
|
-
const STORAGE_URI_ARGUMENT =
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
262
|
+
const STORAGE_URI_ARGUMENT = {
|
|
263
|
+
name: "uri",
|
|
264
|
+
what: "a `pipelex-storage://` address, a non-empty string",
|
|
265
|
+
};
|
|
266
|
+
const RENAME_NAME_ARGUMENT = {
|
|
267
|
+
name: "name",
|
|
268
|
+
what: "the file's new name, a string",
|
|
269
|
+
};
|
|
270
|
+
const LINK_TARGET_ARGUMENT = {
|
|
271
|
+
name: "files",
|
|
272
|
+
what: "a file to link, an object naming exactly one of `file_id` and `uri`, each a non-empty string",
|
|
273
|
+
};
|
|
274
|
+
const ARCHIVE_FILE_ID_ARGUMENT = {
|
|
275
|
+
name: "file_ids",
|
|
276
|
+
what: "a file id to zip, a non-empty string",
|
|
277
|
+
};
|
|
278
|
+
const ARCHIVE_RUN_ID_ARGUMENT = {
|
|
279
|
+
name: "run_id",
|
|
280
|
+
what: "a run_id naming the run whose files to zip, a non-empty string",
|
|
281
|
+
};
|
|
282
|
+
const ARCHIVE_NAME_ARGUMENT = {
|
|
283
|
+
name: "name",
|
|
284
|
+
what: "a name for the zip, a string, or none",
|
|
285
|
+
};
|
|
286
|
+
const DRAFT_PATH_ARGUMENT = {
|
|
287
|
+
name: "path",
|
|
288
|
+
what: "the tree path the file takes in the draft, a non-empty string",
|
|
289
|
+
};
|
|
290
|
+
const DRAFT_TOKEN_ARGUMENT = {
|
|
291
|
+
name: "expected_updated_at",
|
|
292
|
+
what: "the draft token the caller last read, the method's `updated_at`, a string, or none",
|
|
293
|
+
};
|
|
236
294
|
/** What each argument of a method copy is, as a refusal of it names it. */
|
|
237
|
-
const COPY_DESTINATION_ARGUMENT =
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
295
|
+
const COPY_DESTINATION_ARGUMENT = {
|
|
296
|
+
name: "destination_org_id",
|
|
297
|
+
what: "a destination_org_id, the canonical id of the organization to copy into, a non-empty string",
|
|
298
|
+
};
|
|
299
|
+
const COPY_NAME_ARGUMENT = {
|
|
300
|
+
name: "name",
|
|
301
|
+
what: "a name for the copy, a string, or none",
|
|
302
|
+
};
|
|
303
|
+
const COPY_DRAFT_TOKEN_ARGUMENT = {
|
|
304
|
+
name: "expected_draft_updated_at",
|
|
305
|
+
what: "an expected_draft_updated_at, the draft token the caller last read, the method's " +
|
|
306
|
+
"`updated_at`, a string, or none",
|
|
307
|
+
};
|
|
241
308
|
/**
|
|
242
309
|
* The refusal of an argument a call cannot take, worded as every such refusal is:
|
|
243
310
|
* `<call>() takes <what>; got <got>.`, with what made it unusable as `cause` when there is one.
|
|
311
|
+
* Its problem is `client.argument_invalid`, naming the argument and `reason`.
|
|
244
312
|
*/
|
|
245
|
-
function argumentRefusal(call,
|
|
246
|
-
const message = `${call}() takes ${what}; got ${got}.`;
|
|
247
|
-
return new RequestArgumentError(message,
|
|
313
|
+
function argumentRefusal(call, argument, got, reason, cause) {
|
|
314
|
+
const message = `${call}() takes ${argument.what}; got ${got}.`;
|
|
315
|
+
return new RequestArgumentError(message, {
|
|
316
|
+
cause,
|
|
317
|
+
problem: argumentInvalidProblem(argument.name, reason),
|
|
318
|
+
});
|
|
248
319
|
}
|
|
249
320
|
/**
|
|
250
321
|
* A caller's id, checked to be a non-empty string: the one check every id a call takes goes
|
|
@@ -260,7 +331,7 @@ function requireNonEmptyId(value, call, what) {
|
|
|
260
331
|
got = "an empty string";
|
|
261
332
|
else
|
|
262
333
|
got = value === null ? "null" : typeof value;
|
|
263
|
-
throw argumentRefusal(call, what, got);
|
|
334
|
+
throw argumentRefusal(call, what, got, typeof value === "string" ? "empty" : "not_a_string");
|
|
264
335
|
}
|
|
265
336
|
/**
|
|
266
337
|
* The check the example writes that take a name and inputs share, the create and the assistant's
|
|
@@ -276,7 +347,9 @@ function requireNameAndInputs(name, inputs, call, needs) {
|
|
|
276
347
|
got = "neither";
|
|
277
348
|
else
|
|
278
349
|
got = name == null ? "no name" : "no inputs";
|
|
279
|
-
throw new RequestArgumentError(`${call}() needs ${needs}; got ${got}
|
|
350
|
+
throw new RequestArgumentError(`${call}() needs ${needs}; got ${got}.`, {
|
|
351
|
+
problem: argumentInvalidProblem(name == null ? "name" : "inputs", "missing"),
|
|
352
|
+
});
|
|
280
353
|
}
|
|
281
354
|
/**
|
|
282
355
|
* A caller's value as the refusal of a number argument names it: a number by its value, anything
|
|
@@ -303,31 +376,31 @@ function describeNumberArgument(value) {
|
|
|
303
376
|
function idPathSegment(value, call, what) {
|
|
304
377
|
const id = requireNonEmptyId(value, call, what);
|
|
305
378
|
if (id === "." || id === "..") {
|
|
306
|
-
throw argumentRefusal(call, what, `"${id}", a dot segment, which the URL would resolve into another route's path
|
|
379
|
+
throw argumentRefusal(call, what, `"${id}", a dot segment, which the URL would resolve into another route's path`, "dot_segment");
|
|
307
380
|
}
|
|
308
381
|
try {
|
|
309
382
|
return encodeURIComponent(id);
|
|
310
383
|
}
|
|
311
384
|
catch (err) {
|
|
312
385
|
const got = "a string holding a lone surrogate, which a URL cannot carry";
|
|
313
|
-
throw argumentRefusal(call, what, got, err);
|
|
386
|
+
throw argumentRefusal(call, what, got, "lone_surrogate", err);
|
|
314
387
|
}
|
|
315
388
|
}
|
|
316
389
|
/**
|
|
317
390
|
* The path of a method route, `methods/{id}`, for a bare catalog id, its segment refused as
|
|
318
391
|
* `idPathSegment` refuses one; `call` names the method in the message. The method routes address
|
|
319
392
|
* the method itself, never one of its versions, and the platform does not parse a suffix there:
|
|
320
|
-
* `mt_x@3` would be looked up as an id of its own and answer `404 not_found`,
|
|
321
|
-
* method that does not exist. So a suffixed id is refused before anything is sent,
|
|
322
|
-
* read what it names. Stripping it instead would answer the draft for a caller that
|
|
323
|
-
* version.
|
|
393
|
+
* `mt_x@3` would be looked up as an id of its own and answer `404` `pipelex.catalog.not_found`,
|
|
394
|
+
* which reads as a method that does not exist. So a suffixed id is refused before anything is sent,
|
|
395
|
+
* saying how to read what it names. Stripping it instead would answer the draft for a caller that
|
|
396
|
+
* named a version.
|
|
324
397
|
*/
|
|
325
398
|
function methodPath(methodId, call) {
|
|
326
399
|
const segment = idPathSegment(methodId, call, METHOD_ID_ARGUMENT);
|
|
327
400
|
if (methodId.includes("@")) {
|
|
328
401
|
throw new RequestArgumentError(`"${methodId}" carries a version suffix, and the method routes take a bare catalog id: ` +
|
|
329
402
|
"they address the method itself, never one of its versions. Strip the suffix with " +
|
|
330
|
-
"parseMethodSelector, and read a published version with getMethodVersion.");
|
|
403
|
+
"parseMethodSelector, and read a published version with getMethodVersion.", { problem: argumentInvalidProblem("method_id", "version_suffix") });
|
|
331
404
|
}
|
|
332
405
|
return `methods/${segment}`;
|
|
333
406
|
}
|
|
@@ -360,13 +433,13 @@ const RUN_FILES_LISTS = ["inputs", "outputs", "intermediates", "links"];
|
|
|
360
433
|
*/
|
|
361
434
|
function fileLinkTarget(value) {
|
|
362
435
|
if (!isPlainObject(value)) {
|
|
363
|
-
throw argumentRefusal("linkFiles", LINK_TARGET_ARGUMENT, describeNumberArgument(value));
|
|
436
|
+
throw argumentRefusal("linkFiles", LINK_TARGET_ARGUMENT, describeNumberArgument(value), "entry_mistyped");
|
|
364
437
|
}
|
|
365
438
|
const fileId = value.file_id ?? undefined;
|
|
366
439
|
const uri = value.uri ?? undefined;
|
|
367
440
|
if ((fileId === undefined) === (uri === undefined)) {
|
|
368
441
|
const got = fileId === undefined ? "an object naming neither" : "an object naming both";
|
|
369
|
-
throw argumentRefusal("linkFiles", LINK_TARGET_ARGUMENT, got);
|
|
442
|
+
throw argumentRefusal("linkFiles", LINK_TARGET_ARGUMENT, got, "not_exactly_one_selector");
|
|
370
443
|
}
|
|
371
444
|
if (fileId !== undefined) {
|
|
372
445
|
return { file_id: requireNonEmptyId(fileId, "linkFiles", LINK_TARGET_ARGUMENT) };
|
|
@@ -433,13 +506,13 @@ export class PipelexApiClient {
|
|
|
433
506
|
// (e.g. `.../v1`) composes as `/v1/v1/...` and fails with a misleading
|
|
434
507
|
// endpoint error instead of a clear base-URL one. Trailing slashes are
|
|
435
508
|
// stripped first; a remaining path/query/fragment/credentials is rejected. The
|
|
436
|
-
// refusal is `
|
|
437
|
-
// It never quotes the value whole: what the rule refuses is where a secret
|
|
438
|
-
// (a password, a token in a query), so it names those parts without their text.
|
|
509
|
+
// refusal is `client.not_configured`: the value typically comes from PIPELEX_BASE_URL, the
|
|
510
|
+
// environment. It never quotes the value whole: what the rule refuses is where a secret
|
|
511
|
+
// travels (a password, a token in a query), so it names those parts without their text.
|
|
439
512
|
if (!isValidBaseUrl(normalizedBaseUrl)) {
|
|
440
513
|
throw new RequestArgumentError(`Invalid API base URL ${describeRefusedBaseUrl(normalizedBaseUrl)}: it must be host-only ` +
|
|
441
514
|
`(http/https, no path, query, fragment, or credentials). Endpoints ` +
|
|
442
|
-
`compose as {base}/v1/{endpoint}.`, {
|
|
515
|
+
`compose as {base}/v1/{endpoint}.`, { problem: buildProblem("client.not_configured", { setting: "base_url" }) });
|
|
443
516
|
}
|
|
444
517
|
this.baseUrl = normalizedBaseUrl;
|
|
445
518
|
this.originUrl = new URL("/", this.baseUrl).origin;
|
|
@@ -482,9 +555,11 @@ export class PipelexApiClient {
|
|
|
482
555
|
* Issue one HTTP request and return the raw status/headers/body. Wraps
|
|
483
556
|
* DNS/connect/TLS/timeout failures as `ApiUnreachableError`; a caller-driven
|
|
484
557
|
* abort (Ctrl-C / agent walk-away) propagates as-is so the poll loop can stop
|
|
485
|
-
* cleanly.
|
|
486
|
-
*
|
|
487
|
-
*
|
|
558
|
+
* cleanly. A body that arrived and could not be decoded, such as a `gzip` body that is not
|
|
559
|
+
* gzip, is an answer, so it throws the `ApiResponseError` of that answer, read as an answer
|
|
560
|
+
* with an empty body (see `isUndecodableBody`). Non-2xx interpretation is left to the caller.
|
|
561
|
+
* `url` is a fully resolved absolute URL. `mayStartRun` marks a request that may create a run,
|
|
562
|
+
* whose errors then say whether it may have (see `RunCreation`).
|
|
488
563
|
* `idempotencyKey`, already checked by `idempotencyKeyOf`, is sent as the
|
|
489
564
|
* `Idempotency-Key` header; only `start` and `copyMethod` pass one.
|
|
490
565
|
*/
|
|
@@ -537,7 +612,12 @@ export class PipelexApiClient {
|
|
|
537
612
|
// is classified from the controller rather than from `err`: a browser errors a
|
|
538
613
|
// body stream cut short by that abort with a generic AbortError instead.
|
|
539
614
|
const code = extractNetworkErrorCode(controller.signal.aborted ? controller.signal.reason : err);
|
|
540
|
-
|
|
615
|
+
// An answer came back, and its body could not be decoded: an answer the SDK cannot read,
|
|
616
|
+
// never an unreachable API.
|
|
617
|
+
if (response !== undefined && isUndecodableBody(code)) {
|
|
618
|
+
throw this.undecodableAnswer(method, url, response, code, err, options);
|
|
619
|
+
}
|
|
620
|
+
throw new ApiUnreachableError(`Could not reach Pipelex API at ${this.baseUrl} (${code ?? "network error"})`, this.baseUrl, code, { cause: err, mayStartRun: options.mayStartRun === true, timeoutMs });
|
|
541
621
|
}
|
|
542
622
|
finally {
|
|
543
623
|
clearTimeout(timer);
|
|
@@ -561,10 +641,22 @@ export class PipelexApiClient {
|
|
|
561
641
|
utf8,
|
|
562
642
|
};
|
|
563
643
|
}
|
|
644
|
+
/**
|
|
645
|
+
* The `ApiResponseError` of an answer whose body arrived and could not be decoded: the answer's
|
|
646
|
+
* status and headers, read as an answer with an empty body (`readHostedAnswer`), so a `2xx` is
|
|
647
|
+
* `client.response_unreadable` naming its status, or `client.outcome_unknown` on a request that
|
|
648
|
+
* may create a run, which the server accepted. `code` is the decompressor's, named in the
|
|
649
|
+
* message, and the decoding failure is its `cause`.
|
|
650
|
+
*/
|
|
651
|
+
undecodableAnswer(method, url, response, code, cause, run) {
|
|
652
|
+
const path = new URL(url).pathname;
|
|
653
|
+
return new ApiResponseError(`API ${method} ${path} answered ${response.status} with a body that could not be decoded ` +
|
|
654
|
+
`(${code ?? "decoding error"})`, this.baseUrl, response.status, response.statusText, "", { cause, headers: response.headers, mayStartRun: run.mayStartRun === true });
|
|
655
|
+
}
|
|
564
656
|
/**
|
|
565
657
|
* Issue a Pipelex-product request (`/v1/me`, `/v1/methods`, `/v1/billing/*`,
|
|
566
658
|
* …) and parse its JSON body, mapping a non-2xx response to the typed
|
|
567
|
-
* `ApiResponseError` so callers branch on its
|
|
659
|
+
* `ApiResponseError` so callers branch on its problem's `code`, not the
|
|
568
660
|
* HTTP status. Empty-body tolerant — DELETE / onboarding / updateRun
|
|
569
661
|
* answer 2xx with no body, returned as `undefined`. Uses the management-call
|
|
570
662
|
* timeout, not the blocking-execute ceiling.
|
|
@@ -639,8 +731,8 @@ export class PipelexApiClient {
|
|
|
639
731
|
* One page of a cursor-paged product route (`listMethods`, `listMethodVersions`,
|
|
640
732
|
* `listMethodExamples`, `listRuns`, `listFiles`), read as the platform always serializes it: `items` an
|
|
641
733
|
* array, `next_cursor` a string or `null`. A page breaking either is an answer the SDK cannot
|
|
642
|
-
* read, so it throws the `ApiResponseError` `unreadableAnswer` builds,
|
|
643
|
-
*
|
|
734
|
+
* read, so it throws the `ApiResponseError` `unreadableAnswer` builds,
|
|
735
|
+
* `client.response_unreadable`, rather than handing the iterators a page they cannot walk: `items` that is not
|
|
644
736
|
* iterable, or a missing cursor that is neither the end nor a next page, which `iterateRuns`
|
|
645
737
|
* would follow forever. A route reading its rows itself passes `rows`: `parse` reads the page's
|
|
646
738
|
* text, and `item` reads each row, throwing as `unreadableAnswer` does for one it cannot read.
|
|
@@ -773,42 +865,42 @@ export class PipelexApiClient {
|
|
|
773
865
|
/**
|
|
774
866
|
* The `ApiResponseError` of a 2xx answer the SDK cannot read, its message
|
|
775
867
|
* `API <method> <path> answered <status> with <what>`: the answer's status, its raw text as
|
|
776
|
-
* `responseBody`,
|
|
777
|
-
*
|
|
778
|
-
*
|
|
779
|
-
*
|
|
868
|
+
* `responseBody`, the failure that made it unreadable as `cause`, and the problem
|
|
869
|
+
* `client.response_unreadable` naming the status. On a request that may create a run, the run
|
|
870
|
+
* may exist, since the server accepted the request, so the problem is `client.outcome_unknown`
|
|
871
|
+
* instead, naming the run when the body names one. An answer that carries signed links
|
|
872
|
+
* (`RawResponse.carriesLinks`) is held with every link hidden, and with no `cause`, which could
|
|
873
|
+
* quote one.
|
|
780
874
|
*/
|
|
781
875
|
unreadableAnswer(method, path, res, what, cause, run = {}) {
|
|
782
876
|
const carriesLinks = res.carriesLinks === true;
|
|
783
|
-
return new ApiResponseError(`API ${method} ${path} answered ${res.status} with ${what}`, this.baseUrl, res.status, res.statusText, carriesLinks ? redactSignedLinks(res.body) : res.body,
|
|
877
|
+
return new ApiResponseError(`API ${method} ${path} answered ${res.status} with ${what}`, this.baseUrl, res.status, res.statusText, carriesLinks ? redactSignedLinks(res.body) : res.body, {
|
|
784
878
|
cause: carriesLinks ? undefined : cause,
|
|
785
|
-
|
|
879
|
+
problem: run.mayStartRun === true
|
|
880
|
+
? outcomeUnknownProblem(runIdNamedBy(res.body))
|
|
881
|
+
: buildProblem("client.response_unreadable", { status: res.status }),
|
|
786
882
|
headers: res.headers,
|
|
787
|
-
mayStartRun: run.mayStartRun === true,
|
|
788
883
|
});
|
|
789
884
|
}
|
|
790
885
|
/**
|
|
791
886
|
* Throw the `ApiResponseError` of a non-2xx answer, naming the route by its `path` from the
|
|
792
|
-
* origin (`/v1/...`, or `/health` for the liveness probe).
|
|
887
|
+
* origin (`/v1/...`, or `/health` for the liveness probe). The answer is read as its problem
|
|
888
|
+
* (`readHostedAnswer`): the body as received when it is a problem document, whose `detail` the
|
|
889
|
+
* message quotes, and otherwise the problem the edge's bare answer reads as, the message
|
|
890
|
+
* quoting the body as it came, or the status text for an empty one.
|
|
793
891
|
*/
|
|
794
892
|
throwApiResponseErrorAt(method, path, res, run = {}) {
|
|
795
|
-
const {
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
const requestId = details.requestId ?? nonEmptyHeader(res.headers, REQUEST_ID_HEADER);
|
|
799
|
-
throw new ApiResponseError(`API ${method} ${path} failed (${res.status}): ${serverMessage ?? (res.body || res.statusText)}`, this.baseUrl, res.status, res.statusText, res.body, errorType, serverMessage, validationErrors, code, {
|
|
800
|
-
details: { ...details, requestId },
|
|
801
|
-
problemDocument: document,
|
|
802
|
-
headers: res.headers,
|
|
803
|
-
mayStartRun: run.mayStartRun === true,
|
|
804
|
-
});
|
|
893
|
+
const { problem, fromBody } = readHostedAnswer({ status: res.status, headers: res.headers, body: res.body }, { createsRun: run.mayStartRun === true });
|
|
894
|
+
const said = fromBody ? problem.detail : res.body || res.statusText;
|
|
895
|
+
throw new ApiResponseError(`API ${method} ${path} failed (${res.status}): ${said}`, this.baseUrl, res.status, res.statusText, res.body, { problem, headers: res.headers });
|
|
805
896
|
}
|
|
806
897
|
/**
|
|
807
898
|
* Translate a "route absent" 404 (a bare pipelex-api with no platform block)
|
|
808
|
-
* into a clear `RunLifecycleUnavailableError`.
|
|
809
|
-
* not found
|
|
810
|
-
* and
|
|
811
|
-
* why the route is missing, for a route a
|
|
899
|
+
* into a clear `RunLifecycleUnavailableError`. A 404 a route answered on purpose (a run
|
|
900
|
+
* not found, cross-org included, or a method that does not exist) names its own `code`
|
|
901
|
+
* and is left for normal handling as `ApiResponseError` (`isMissingRoute404`).
|
|
902
|
+
* `explanation` replaces the sentence saying why the route is missing, for a route a
|
|
903
|
+
* hosted API may also lack.
|
|
812
904
|
*/
|
|
813
905
|
throwIfLifecycleUnavailable(res, url, explanation = "PIPELEX_BASE_URL points at a bare runner that does not serve it.") {
|
|
814
906
|
if (res.status !== 404)
|
|
@@ -848,9 +940,9 @@ export class PipelexApiClient {
|
|
|
848
940
|
* there this throws.
|
|
849
941
|
*
|
|
850
942
|
* It goes through the same transport as every route: an unreachable origin is an
|
|
851
|
-
* `ApiUnreachableError`, and a non-2xx answer an `ApiResponseError` whose
|
|
852
|
-
*
|
|
853
|
-
*
|
|
943
|
+
* `ApiUnreachableError`, and a non-2xx answer an `ApiResponseError` whose problem is read from
|
|
944
|
+
* the status when the probe answers no problem document. So is a 2xx whose body is not a JSON
|
|
945
|
+
* object, such as a gateway's HTML page: `client.response_unreadable`.
|
|
854
946
|
*/
|
|
855
947
|
async health() {
|
|
856
948
|
const res = await this.requestRaw("GET", `${this.originUrl}/health`, {
|
|
@@ -950,15 +1042,15 @@ export class PipelexApiClient {
|
|
|
950
1042
|
*
|
|
951
1043
|
* `options.idempotencyKey` is sent as the `Idempotency-Key` header, and nothing
|
|
952
1044
|
* is sent without it: the hosted API answers the same start sent again under the
|
|
953
|
-
* same key with the run the first one started, so a failure whose
|
|
954
|
-
* `
|
|
1045
|
+
* same key with the run the first one started, so a failure whose problem's
|
|
1046
|
+
* `retry` is `outcome_unknown` can be retried unchanged (see
|
|
955
1047
|
* `PipelexStartRequestOptions`). A malformed key is a `RequestArgumentError`
|
|
956
1048
|
* before anything is sent.
|
|
957
1049
|
*
|
|
958
1050
|
* The method's tree is prepared before the request as `execute` prepares it. The prepared body
|
|
959
1051
|
* is the same for the same tree, so a start retried under its idempotency key sends the body the
|
|
960
1052
|
* key was bound to. A start whose hash entries name a blob the API does not hold yet is refused
|
|
961
|
-
* with a `422` `
|
|
1053
|
+
* with a `422` `request.blobs_missing`, which releases its key: send it again unchanged, which asks the
|
|
962
1054
|
* blob route and uploads what is missing first.
|
|
963
1055
|
*/
|
|
964
1056
|
async start(options) {
|
|
@@ -1018,7 +1110,9 @@ export class PipelexApiClient {
|
|
|
1018
1110
|
* `/validate` is a diagnostic endpoint: every produced verdict rides a **200**,
|
|
1019
1111
|
* discriminated on `is_valid`. This returns the `PipelexValidationResult` union
|
|
1020
1112
|
* verbatim — `is_valid: true` ⇒ the typed `PipelexValidationReport` (structural
|
|
1021
|
-
* artifacts), `is_valid: false` ⇒ a `PipelexInvalidReport
|
|
1113
|
+
* artifacts), `is_valid: false` ⇒ a `PipelexInvalidReport`, whose `errors[]` are problem
|
|
1114
|
+
* items. Both arms' items, the invalid arm's `errors[]` and the valid arm's `warnings[]`, are
|
|
1115
|
+
* read member by member (`readProblemItems`).
|
|
1022
1116
|
* An invalid method is NOT thrown — the caller pattern-matches `is_valid`. Only a
|
|
1023
1117
|
* *no-verdict* condition (a malformed request, a tree the route refuses, auth, a
|
|
1024
1118
|
* server fault) is non-2xx and surfaces as `ApiResponseError`.
|
|
@@ -1048,8 +1142,8 @@ export class PipelexApiClient {
|
|
|
1048
1142
|
*
|
|
1049
1143
|
* A selector-resolution failure (fetch failure, no package at the address, an
|
|
1050
1144
|
* unknown or foreign-org id, a bare id of a method never published, `409`
|
|
1051
|
-
* `
|
|
1052
|
-
* `
|
|
1145
|
+
* `pipelex.catalog.not_published`, a version never published, `404`
|
|
1146
|
+
* `pipelex.catalog.not_found`, or a malformed suffix, `422`) is a non-2xx
|
|
1053
1147
|
* `ApiResponseError` — never an `is_valid: false` verdict, which is reserved
|
|
1054
1148
|
* for actual MTHDS content.
|
|
1055
1149
|
*
|
|
@@ -1085,7 +1179,7 @@ export class PipelexApiClient {
|
|
|
1085
1179
|
// null source — mirroring the server's strict tooling XOR instead of silently picking.
|
|
1086
1180
|
if (source === null || typeof source !== "object" || Array.isArray(source)) {
|
|
1087
1181
|
throw new RequestArgumentError("validate() takes the method as an object naming exactly one of directory, contents, " +
|
|
1088
|
-
"tree, files, method_ref or method_id.");
|
|
1182
|
+
"tree, files, method_ref or method_id.", { problem: argumentInvalidProblem("source", "not_an_object") });
|
|
1089
1183
|
}
|
|
1090
1184
|
const forms = protocolTreeForms("validate", source);
|
|
1091
1185
|
const methodRef = nonEmptyString(source.method_ref);
|
|
@@ -1097,7 +1191,7 @@ export class PipelexApiClient {
|
|
|
1097
1191
|
given.push("method_id");
|
|
1098
1192
|
if (given.length !== 1) {
|
|
1099
1193
|
throw new RequestArgumentError("validate() takes exactly one of the method's tree (directory, contents, tree or files), " +
|
|
1100
|
-
`method_ref or method_id; got ${given.length === 0 ? "none" : given.join(" and ")}
|
|
1194
|
+
`method_ref or method_id; got ${given.length === 0 ? "none" : given.join(" and ")}.`, { problem: argumentInvalidProblem("files", "not_exactly_one_selector") });
|
|
1101
1195
|
}
|
|
1102
1196
|
const signal = options.signal;
|
|
1103
1197
|
const sent = await this.treeToSend("validate", forms, REQUEST_SENDING, signal);
|
|
@@ -1123,7 +1217,7 @@ export class PipelexApiClient {
|
|
|
1123
1217
|
if (res.status < 200 || res.status >= 300) {
|
|
1124
1218
|
this.throwApiResponseError("POST", "validate", res);
|
|
1125
1219
|
}
|
|
1126
|
-
return this.readObjectAnswer("POST", "validate", res);
|
|
1220
|
+
return withVerdictItems(this.readObjectAnswer("POST", "validate", res));
|
|
1127
1221
|
}
|
|
1128
1222
|
// ── Tools extensions (Pipelex API — `/v1/lint`, `/v1/format`) ─────────
|
|
1129
1223
|
//
|
|
@@ -1263,10 +1357,9 @@ export class PipelexApiClient {
|
|
|
1263
1357
|
* Returns a **200 verdict** whatever the resolution: a reference that resolves nowhere is
|
|
1264
1358
|
* `resolution: "not_found"` with the names it may have meant, never a thrown error. Narrow
|
|
1265
1359
|
* on `kind` to read the shape of `matches`. A request that cannot produce a verdict throws
|
|
1266
|
-
* the typed `ApiResponseError`, a `422` whose `
|
|
1267
|
-
*
|
|
1268
|
-
*
|
|
1269
|
-
* here, so the runner's rule is the only one.
|
|
1360
|
+
* the typed `ApiResponseError`, a `422` problem whose `code` says why: a blank reference, a
|
|
1361
|
+
* sigil or a namespace alone, one past the runner's length limit, or an unknown `type`. None of
|
|
1362
|
+
* that is checked here, so the runner's rule is the only one.
|
|
1270
1363
|
*/
|
|
1271
1364
|
async checkModelReference(reference, category) {
|
|
1272
1365
|
const query = new URLSearchParams({ reference });
|
|
@@ -1313,9 +1406,9 @@ export class PipelexApiClient {
|
|
|
1313
1406
|
// serves both, verdict discipline intact (200 `is_valid:false`, 501, 422);
|
|
1314
1407
|
// api-dev.pipelex.com has since 2026-08-13. `lint`/`format` are the two still
|
|
1315
1408
|
// unexposed — see their section above for why that blocks nothing. `pipe-io` is
|
|
1316
|
-
// newer: a runner serves it from `pipelex-api` v0.33.0, and
|
|
1317
|
-
// refusals `
|
|
1318
|
-
//
|
|
1409
|
+
// newer: a runner serves it from `pipelex-api` v0.33.0, and codes its selection
|
|
1410
|
+
// refusals `request.pipe_not_found`, `request.pipe_unspecified` and
|
|
1411
|
+
// `request.pipe_ambiguous`, and a hosted origin
|
|
1319
1412
|
// serves it once the platform's proxy and the gateway list it (see
|
|
1320
1413
|
// `docs/crate-routes.md`).
|
|
1321
1414
|
//
|
|
@@ -1336,7 +1429,7 @@ export class PipelexApiClient {
|
|
|
1336
1429
|
*
|
|
1337
1430
|
* Returns a **200 verdict**: pattern-match `is_valid` before reading the arm — a
|
|
1338
1431
|
* closure that does not parse/load/validate comes back as `is_valid: false` with
|
|
1339
|
-
* `
|
|
1432
|
+
* problem items in `errors[]`, not as a thrown error. Only a no-verdict condition throws
|
|
1340
1433
|
* `ApiResponseError`: a malformed selector (none, or more than one, of
|
|
1341
1434
|
* `files` / `method_ref` / `method_id`) is a 422; a selector-resolution failure
|
|
1342
1435
|
* (fetch failure, no package at the address, an unknown or foreign-org id) is
|
|
@@ -1404,11 +1497,11 @@ export class PipelexApiClient {
|
|
|
1404
1497
|
* {@link resolve} (the request's members are posted as given, its `directory`, `tree` or
|
|
1405
1498
|
* `contents` as the `files` map, and the selector XOR is the server's to enforce).
|
|
1406
1499
|
* Only a no-verdict condition throws `ApiResponseError`: a malformed selector and an
|
|
1407
|
-
* over-limit file are `
|
|
1408
|
-
* `
|
|
1409
|
-
* pipe) or `
|
|
1410
|
-
* candidates in its `
|
|
1411
|
-
*
|
|
1500
|
+
* over-limit file are `422` problems; a selection the route refuses is a `422` whose
|
|
1501
|
+
* `code` is `request.pipe_not_found` (an unknown `pipe_ref`), `request.pipe_unspecified`
|
|
1502
|
+
* (no entry pipe) or `request.pipe_ambiguous` (an ambiguous code, several entry pipes), the
|
|
1503
|
+
* candidates in its `params`; a registry-form `method_ref` is a `501`; a pipe whose
|
|
1504
|
+
* artifacts cannot be derived is a `500`.
|
|
1412
1505
|
*
|
|
1413
1506
|
* A `method_ref` gets the fetch-sized budget, as on the other crate routes. On the
|
|
1414
1507
|
* hosted API the gateway caps a request at about 30 seconds whatever the client allows,
|
|
@@ -1459,7 +1552,7 @@ export class PipelexApiClient {
|
|
|
1459
1552
|
if (res.status < 200 || res.status >= 300) {
|
|
1460
1553
|
this.throwApiResponseError("POST", endpoint, res);
|
|
1461
1554
|
}
|
|
1462
|
-
return this.readObjectAnswer("POST", endpoint, res);
|
|
1555
|
+
return withVerdictItems(this.readObjectAnswer("POST", endpoint, res));
|
|
1463
1556
|
}
|
|
1464
1557
|
/**
|
|
1465
1558
|
* The request budget for a crate route's closure (`resolve`, `codegen`, `pipeIo`): the
|
|
@@ -1487,9 +1580,9 @@ export class PipelexApiClient {
|
|
|
1487
1580
|
* is live and the run is stuck, its workflow task failing until the platform
|
|
1488
1581
|
* ends it at `degradation.ends_at`. `retry_after_seconds` carries the server's
|
|
1489
1582
|
* `Retry-After` hint when present, and `request_id` the id to quote to support.
|
|
1490
|
-
* A failed run's
|
|
1491
|
-
*
|
|
1492
|
-
* neither ever fails the read. Throws `RunLifecycleUnavailableError` when the
|
|
1583
|
+
* A failed run's problem, `error`, is kept when it is a problem document and
|
|
1584
|
+
* read as `null` otherwise, and the `degradation` is checked field by field
|
|
1585
|
+
* (see `RunDegradation`), so neither ever fails the read. Throws `RunLifecycleUnavailableError` when the
|
|
1493
1586
|
* lifecycle routes are absent (a bare runner).
|
|
1494
1587
|
*/
|
|
1495
1588
|
async getRunStatus(runId, options = {}) {
|
|
@@ -1503,7 +1596,7 @@ export class PipelexApiClient {
|
|
|
1503
1596
|
if (res.status < 200 || res.status >= 300) {
|
|
1504
1597
|
this.throwApiResponseError("GET", endpoint, res);
|
|
1505
1598
|
}
|
|
1506
|
-
const run = withCheckedDegradation(
|
|
1599
|
+
const run = withCheckedDegradation(withCheckedProblem(this.readObjectAnswer("GET", endpoint, res)));
|
|
1507
1600
|
const retryAfter = parseRetryAfter(res.headers);
|
|
1508
1601
|
return retryAfter !== null ? { ...run, retry_after_seconds: retryAfter } : run;
|
|
1509
1602
|
}
|
|
@@ -1543,17 +1636,17 @@ export class PipelexApiClient {
|
|
|
1543
1636
|
if (fault !== undefined) {
|
|
1544
1637
|
throw this.unreadableAnswer("GET", `/${API_PREFIX}/${endpoint}`, res, `a snapshot whose ${fault}`);
|
|
1545
1638
|
}
|
|
1546
|
-
return
|
|
1639
|
+
return withCheckedProgressHealth(answer);
|
|
1547
1640
|
}
|
|
1548
1641
|
/**
|
|
1549
1642
|
* Single-shot result lookup — `GET /v1/runs/{pipeline_run_id}/results`.
|
|
1550
1643
|
* Maps the server's poll semantics to a discriminated union:
|
|
1551
1644
|
* - HTTP 202 → `running` (with the `Retry-After` hint)
|
|
1552
1645
|
* - HTTP 200 → `completed` (with the result artifacts)
|
|
1553
|
-
* - HTTP 409 → `failed` (terminal non-`COMPLETED`), carrying the problem
|
|
1554
|
-
* `
|
|
1555
|
-
*
|
|
1556
|
-
*
|
|
1646
|
+
* - HTTP 409 `run.failed` → `failed` (terminal non-`COMPLETED`), carrying the problem as
|
|
1647
|
+
* `problem`, its `detail` as `message`, the terminal status its `run_status` parameter names
|
|
1648
|
+
* as `status`, and the run's own problem, which it nests as `run_error`, as `error`; a `409`
|
|
1649
|
+
* `run.not_finished` → `running`, and any other `409` throws `ApiResponseError`
|
|
1557
1650
|
* - HTTP 503 → `running` (Temporal degraded — retry, never fail a poller)
|
|
1558
1651
|
*
|
|
1559
1652
|
* `options.artifacts` narrows the read to the named artifacts, sent as one
|
|
@@ -1593,7 +1686,10 @@ export class PipelexApiClient {
|
|
|
1593
1686
|
};
|
|
1594
1687
|
}
|
|
1595
1688
|
if (res.status === 409) {
|
|
1596
|
-
|
|
1689
|
+
const conflict = runResultConflict(runId, res);
|
|
1690
|
+
if (conflict !== undefined)
|
|
1691
|
+
return conflict;
|
|
1692
|
+
this.throwApiResponseError("GET", endpoint, res);
|
|
1597
1693
|
}
|
|
1598
1694
|
this.throwIfLifecycleUnavailable(res, url);
|
|
1599
1695
|
if (res.status < 200 || res.status >= 300) {
|
|
@@ -1625,8 +1721,8 @@ export class PipelexApiClient {
|
|
|
1625
1721
|
* real error; no answer at all propagates, uncached. An answer is anything the
|
|
1626
1722
|
* server sent back: a non-2xx status, a body that is not JSON, not UTF-8 or no
|
|
1627
1723
|
* version, and a body that arrived but could not be decoded, such as a broken
|
|
1628
|
-
* gzip stream, which `requestRaw` reports as an `
|
|
1629
|
-
*
|
|
1724
|
+
* gzip stream, which `requestRaw` reports as an `ApiResponseError` (see
|
|
1725
|
+
* `isUndecodableBody`).
|
|
1630
1726
|
*
|
|
1631
1727
|
* `signal` stops the handshake: its abort is thrown as is and nothing is cached.
|
|
1632
1728
|
*
|
|
@@ -1647,9 +1743,9 @@ export class PipelexApiClient {
|
|
|
1647
1743
|
// The caller's abort is no answer: it propagates, and nothing is cached.
|
|
1648
1744
|
if (signal?.aborted === true)
|
|
1649
1745
|
throw signal.reason;
|
|
1650
|
-
// No answer at all propagates.
|
|
1651
|
-
//
|
|
1652
|
-
if (error instanceof ApiUnreachableError
|
|
1746
|
+
// No answer at all propagates. Every other failure here is an answer: a non-2xx status, a
|
|
1747
|
+
// body that is no version, or one that arrived and could not be decoded.
|
|
1748
|
+
if (error instanceof ApiUnreachableError)
|
|
1653
1749
|
throw error;
|
|
1654
1750
|
this.lifecycleAvailable = true;
|
|
1655
1751
|
}
|
|
@@ -1682,11 +1778,13 @@ export class PipelexApiClient {
|
|
|
1682
1778
|
* rather than listing, reading and hashing the tree again.
|
|
1683
1779
|
*
|
|
1684
1780
|
* An error from the start, or from the blocking execute, says whether that request may still
|
|
1685
|
-
* have started a run whose id never reached the caller
|
|
1686
|
-
*
|
|
1687
|
-
* up in the run history first. An error from
|
|
1688
|
-
*
|
|
1689
|
-
*
|
|
1781
|
+
* have started a run whose id never reached the caller: its problem's `retry` is then
|
|
1782
|
+
* `outcome_unknown`, since calling this method again could pay for the run twice, unless it is
|
|
1783
|
+
* made with the same `idempotencyKey`: look the run up in the run history first. An error from
|
|
1784
|
+
* the wait, once `onStarted` has named the run, started nothing, and its problem is about that
|
|
1785
|
+
* run: `client.follow_failed` for a wait that ran out of time, whose step is to read the run
|
|
1786
|
+
* again with `waitForResult`, or `run.failed` for a run that ended without success, never a
|
|
1787
|
+
* reason to call this method again.
|
|
1690
1788
|
*
|
|
1691
1789
|
* `options.idempotencyKey` rides the durable start, so a call that failed while its start may
|
|
1692
1790
|
* have created a run can be made again with the same options and the same key: the hosted API
|
|
@@ -1753,7 +1851,7 @@ export class PipelexApiClient {
|
|
|
1753
1851
|
// The hosted catalog/account routes the webapp drives. Every one rides the
|
|
1754
1852
|
// same `{base}/v1/*` surface, `Authorization: Bearer`, org-from-JWT contract
|
|
1755
1853
|
// as the protocol routes, and maps a non-2xx `problem+json` to a typed
|
|
1756
|
-
// `ApiResponseError` (branch on
|
|
1854
|
+
// `ApiResponseError` (branch on its problem's `code`, not the status).
|
|
1757
1855
|
/** The authenticated user's profile — `GET /v1/me`. */
|
|
1758
1856
|
async getMe() {
|
|
1759
1857
|
return this.requestProduct("GET", "me");
|
|
@@ -1830,10 +1928,11 @@ export class PipelexApiClient {
|
|
|
1830
1928
|
* and for an id that is empty, `.` or `..`, which the URL would resolve into another route's
|
|
1831
1929
|
* path.
|
|
1832
1930
|
*
|
|
1833
|
-
* Throws `ApiResponseError`: `404` `not_found` for an unknown or foreign-org
|
|
1834
|
-
* `
|
|
1835
|
-
* method whose draft names a tree the platform holds no record of. An
|
|
1836
|
-
* not a listing is an answer the SDK cannot read, thrown as an
|
|
1931
|
+
* Throws `ApiResponseError`: `404` `pipelex.catalog.not_found` for an unknown or foreign-org
|
|
1932
|
+
* method, `409` `pipelex.catalog.being_deleted` while its erasure runs, and a `500`
|
|
1933
|
+
* `runtime.unexpected` for a method whose draft names a tree the platform holds no record of. An
|
|
1934
|
+
* answer whose `files` is not a listing is an answer the SDK cannot read, thrown as an
|
|
1935
|
+
* `ApiResponseError` too.
|
|
1837
1936
|
*/
|
|
1838
1937
|
async getMethod(methodId) {
|
|
1839
1938
|
const endpoint = methodPath(methodId, "getMethod");
|
|
@@ -1842,8 +1941,8 @@ export class PipelexApiClient {
|
|
|
1842
1941
|
}
|
|
1843
1942
|
/**
|
|
1844
1943
|
* Create a method — `POST /v1/methods`. The new method holds its tree as its draft and has no
|
|
1845
|
-
* published version, so its bare id answers `409
|
|
1846
|
-
* routes until its first `publishMethod`; its draft runs as `mt_…@draft` at once.
|
|
1944
|
+
* published version, so its bare id answers `409` `pipelex.catalog.not_published` on the run and
|
|
1945
|
+
* tooling routes until its first `publishMethod`; its draft runs as `mt_…@draft` at once.
|
|
1847
1946
|
*
|
|
1848
1947
|
* The tree is `files`, sent as given, or `tree` or `contents`, which the client sends inline
|
|
1849
1948
|
* when its inline map weighs 256 KiB or less, or when the platform keeps no blob store, and
|
|
@@ -1853,12 +1952,12 @@ export class PipelexApiClient {
|
|
|
1853
1952
|
*
|
|
1854
1953
|
* Throws `RequestArgumentError` for two forms of the tree, or `contents` holding no file, before
|
|
1855
1954
|
* any request; `MethodFileTransferError` when a file the organization lacks could not be
|
|
1856
|
-
* uploaded, and nothing is saved. Throws `ApiResponseError`: `422` `
|
|
1857
|
-
* map the standard refuses,
|
|
1858
|
-
*
|
|
1859
|
-
* stated at another size than the one held; `422`
|
|
1860
|
-
* the organization does not hold; `413`
|
|
1861
|
-
* room for a publish.
|
|
1955
|
+
* uploaded, and nothing is saved. Throws `ApiResponseError`: `422` `method.package_invalid` for
|
|
1956
|
+
* a map the standard refuses, naming the path in its `params`, and for a tree past one of the
|
|
1957
|
+
* deployment's limits, its `params` carrying `reason` `too_large` and the integer `limit`;
|
|
1958
|
+
* `422` `request.invalid` for a hash stated at another size than the one held; `422`
|
|
1959
|
+
* `request.blobs_missing` for a hash entry whose bytes the organization does not hold; `413`
|
|
1960
|
+
* `request.too_large` for a method that would leave no room for a publish.
|
|
1862
1961
|
*/
|
|
1863
1962
|
async createMethod(input) {
|
|
1864
1963
|
const source = await this.treeToSend("createMethod", input, SAVE_SENDING);
|
|
@@ -1881,23 +1980,23 @@ export class PipelexApiClient {
|
|
|
1881
1980
|
* `files: null` emptying the draft, or `tree` or `contents`, sent inline or by hash as
|
|
1882
1981
|
* `createMethod` sends them. `input_data` is three-way (see `MethodDraftInput`). With
|
|
1883
1982
|
* `expected_updated_at`, the write is a compare-and-swap on the draft token: a draft that
|
|
1884
|
-
* moved since is refused with a `409` whose `code` is `
|
|
1983
|
+
* moved since is refused with a `409` whose `code` is `pipelex.catalog.update_conflict`, and nothing is
|
|
1885
1984
|
* written. Without it, last writer wins.
|
|
1886
1985
|
*
|
|
1887
1986
|
* Takes a bare catalog id, as `getMethod` does. Throws `RequestArgumentError` for a draft that
|
|
1888
1987
|
* gives its tree in no form or in two, or `contents` holding no file, before any request;
|
|
1889
1988
|
* `MethodFileTransferError` when a file the organization lacks could not be uploaded. Throws
|
|
1890
|
-
* `ApiResponseError`: `404` `not_found` for an unknown or foreign-org method,
|
|
1891
|
-
* `
|
|
1892
|
-
* lists, `413` `
|
|
1893
|
-
* publish needs, `403` for a read-only key, and a `503` that wrote nothing, safe to retry,
|
|
1989
|
+
* `ApiResponseError`: `404` `pipelex.catalog.not_found` for an unknown or foreign-org method,
|
|
1990
|
+
* `409` `pipelex.catalog.being_deleted` while its erasure runs, the `422` refusals of a tree
|
|
1991
|
+
* `createMethod` lists, `413` `request.too_large` for a draft that would grow the method past the
|
|
1992
|
+
* room a publish needs, `403` for a read-only key, and a `503` that wrote nothing, safe to retry,
|
|
1894
1993
|
* when the method kept changing under the write.
|
|
1895
1994
|
*/
|
|
1896
1995
|
async writeDraft(methodId, input) {
|
|
1897
1996
|
const endpoint = `${methodPath(methodId, "writeDraft")}/draft`;
|
|
1898
1997
|
if (treeFormsGiven(input).length === 0) {
|
|
1899
1998
|
throw new RequestArgumentError("writeDraft() replaces the draft's whole tree: give it as directory, contents, tree or " +
|
|
1900
|
-
"files (files: null empties the draft).");
|
|
1999
|
+
"files (files: null empties the draft).", { problem: argumentInvalidProblem("files", "missing") });
|
|
1901
2000
|
}
|
|
1902
2001
|
const source = await this.treeToSend("writeDraft", input, SAVE_SENDING);
|
|
1903
2002
|
return this.saveMethodTree("PUT", endpoint, "writeDraft", source, (files) => {
|
|
@@ -2016,7 +2115,7 @@ export class PipelexApiClient {
|
|
|
2016
2115
|
const given = treeFormsGiven(input);
|
|
2017
2116
|
if (given.length > 1) {
|
|
2018
2117
|
throw new RequestArgumentError(`${call}() takes the method's tree in one form, directory, contents, tree or files; got ` +
|
|
2019
|
-
`${given.join(" and ")}
|
|
2118
|
+
`${given.join(" and ")}.`, { problem: argumentInvalidProblem("files", "mutually_exclusive") });
|
|
2020
2119
|
}
|
|
2021
2120
|
if (given[0] === "directory") {
|
|
2022
2121
|
const directory = input.directory;
|
|
@@ -2242,7 +2341,7 @@ export class PipelexApiClient {
|
|
|
2242
2341
|
if (bytes.length !== blob.size || bytesSha256 !== blob.sha256) {
|
|
2243
2342
|
throw new RequestArgumentError(`The tree read ${bytes.length} bytes hashing to ${bytesSha256} for '${file.path}', which ` +
|
|
2244
2343
|
`its listing names as ${blob.size} bytes hashing to ${blob.sha256}, so they were not ` +
|
|
2245
|
-
`uploaded and ${sending.unsent}
|
|
2344
|
+
`uploaded and ${sending.unsent}.`, { problem: argumentInvalidProblem("tree", "changed_since_listed") });
|
|
2246
2345
|
}
|
|
2247
2346
|
throwIfAborted(userSignal);
|
|
2248
2347
|
await uploadBlob(blob, bytes, {
|
|
@@ -2257,11 +2356,11 @@ export class PipelexApiClient {
|
|
|
2257
2356
|
* draft, so the `updated_at` a caller holds stays valid for its next `writeDraft` or
|
|
2258
2357
|
* `publishMethod`.
|
|
2259
2358
|
*
|
|
2260
|
-
* Takes a bare catalog id, as `getMethod` does. Throws `ApiResponseError`: `404`
|
|
2261
|
-
* `409` `
|
|
2262
|
-
* holding a lone surrogate, `413` `
|
|
2263
|
-
* method too large to publish, and a `503` that wrote nothing, safe
|
|
2264
|
-
* writes kept landing under the rename.
|
|
2359
|
+
* Takes a bare catalog id, as `getMethod` does. Throws `ApiResponseError`: `404`
|
|
2360
|
+
* `pipelex.catalog.not_found`, `409` `pipelex.catalog.being_deleted`, `403` for a read-only key,
|
|
2361
|
+
* `422` for an empty name or one holding a lone surrogate, `413` `request.too_large` for a name
|
|
2362
|
+
* so long it would leave the method too large to publish, and a `503` that wrote nothing, safe
|
|
2363
|
+
* to retry, when draft writes kept landing under the rename.
|
|
2265
2364
|
*/
|
|
2266
2365
|
async renameMethod(methodId, input) {
|
|
2267
2366
|
return this.requestMethodData("PATCH", methodPath(methodId, "renameMethod"), {
|
|
@@ -2279,16 +2378,17 @@ export class PipelexApiClient {
|
|
|
2279
2378
|
* The answer is a `MethodPublishResult` discriminated on `outcome` — branch on it:
|
|
2280
2379
|
* `published` with the new `version`; `unchanged` with the existing latest `version`;
|
|
2281
2380
|
* `refused` with a `reason` (`invalid`, or `not_runnable` for a draft that validates with
|
|
2282
|
-
* pending signatures), a `message` and the runner's `validation` verdict
|
|
2283
|
-
* `method`. A publish
|
|
2381
|
+
* pending signatures), a `message` and the runner's `validation` verdict, whose problem items
|
|
2382
|
+
* are read member by member as `validate` reads them. Every arm carries the `method`. A publish
|
|
2383
|
+
* moves no token.
|
|
2284
2384
|
*
|
|
2285
2385
|
* Takes a bare catalog id, as `getMethod` does. Throws `RequestArgumentError` when
|
|
2286
2386
|
* `expected_draft_updated_at` is not a string, before any request. Throws `ApiResponseError`
|
|
2287
|
-
* when no verdict was produced: `409` `
|
|
2288
|
-
* the token, `409` `
|
|
2289
|
-
* `.mthds` file or whose file names a run could not assemble (one name used by a
|
|
2290
|
-
* a Python file), `413` `
|
|
2291
|
-
* read-only key.
|
|
2387
|
+
* when no verdict was produced: `409` `pipelex.catalog.update_conflict` for a draft that moved since
|
|
2388
|
+
* the token, `409` `pipelex.catalog.being_deleted`, `404` `pipelex.catalog.not_found`, `422` for
|
|
2389
|
+
* a draft with no `.mthds` file or whose file names a run could not assemble (one name used by a
|
|
2390
|
+
* `.mthds` and a Python file), `413` `request.too_large` for a draft too large to publish, and
|
|
2391
|
+
* `403` for a read-only key.
|
|
2292
2392
|
*
|
|
2293
2393
|
* Only a draft that differs from the latest version reaches the runner, and a runner that
|
|
2294
2394
|
* cannot be reached, answers unusably or does not finish within the platform's deadline is a
|
|
@@ -2296,15 +2396,16 @@ export class PipelexApiClient {
|
|
|
2296
2396
|
* itself is relayed under its own status. A retry of a publish that landed while its answer was
|
|
2297
2397
|
* lost, as on a client timeout, answers `unchanged` with the version it wrote.
|
|
2298
2398
|
*
|
|
2299
|
-
* An answer whose `outcome` is not one of the outcomes above,
|
|
2300
|
-
*
|
|
2399
|
+
* An answer whose `outcome` is not one of the outcomes above, that carries no `method` object,
|
|
2400
|
+
* or a refusal whose `validation` is not a verdict object with a boolean `is_valid`, is an answer
|
|
2401
|
+
* the SDK cannot read, thrown as an `ApiResponseError` too.
|
|
2301
2402
|
*/
|
|
2302
2403
|
async publishMethod(methodId, input) {
|
|
2303
2404
|
const token = input
|
|
2304
2405
|
?.expected_draft_updated_at;
|
|
2305
2406
|
if (typeof token !== "string") {
|
|
2306
2407
|
throw new RequestArgumentError("publishMethod() needs expected_draft_updated_at: the draft token (the method's " +
|
|
2307
|
-
"updated_at) the caller last saw, so a publish never takes a draft it has not seen.");
|
|
2408
|
+
"updated_at) the caller last saw, so a publish never takes a draft it has not seen.", { problem: argumentInvalidProblem("expected_draft_updated_at", "not_a_string") });
|
|
2308
2409
|
}
|
|
2309
2410
|
const endpoint = `${methodPath(methodId, "publishMethod")}/publish`;
|
|
2310
2411
|
const res = await this.requestProductAnswer("POST", endpoint, {
|
|
@@ -2315,7 +2416,22 @@ export class PipelexApiClient {
|
|
|
2315
2416
|
outcomes: ["published", "unchanged", "refused"],
|
|
2316
2417
|
member: "method",
|
|
2317
2418
|
});
|
|
2318
|
-
|
|
2419
|
+
const method = subject;
|
|
2420
|
+
if (outcome !== "refused") {
|
|
2421
|
+
return { ...answer, outcome, method };
|
|
2422
|
+
}
|
|
2423
|
+
// The refused arm carries the runner's verdict, whose problem items are read member by
|
|
2424
|
+
// member as `validate` reads them, so one malformed item never costs the caller the others.
|
|
2425
|
+
const validation = answer.validation;
|
|
2426
|
+
if (!isPlainObject(validation) || typeof validation.is_valid !== "boolean") {
|
|
2427
|
+
throw this.unreadableAnswer("POST", `/${API_PREFIX}/${endpoint}`, res, "a publish result whose `validation` is not an object");
|
|
2428
|
+
}
|
|
2429
|
+
return {
|
|
2430
|
+
...answer,
|
|
2431
|
+
outcome,
|
|
2432
|
+
validation: withVerdictItems(validation),
|
|
2433
|
+
method,
|
|
2434
|
+
};
|
|
2319
2435
|
}
|
|
2320
2436
|
/**
|
|
2321
2437
|
* One page of a method's published versions, newest first, without their files —
|
|
@@ -2325,10 +2441,11 @@ export class PipelexApiClient {
|
|
|
2325
2441
|
* may be short while `nextCursor` is set, because the platform reads versions whole. A method
|
|
2326
2442
|
* never published answers an empty page.
|
|
2327
2443
|
*
|
|
2328
|
-
* Takes a bare catalog id, as `getMethod` does. Throws `ApiResponseError`: `404`
|
|
2329
|
-
* for an unknown method, `409` `
|
|
2330
|
-
*
|
|
2331
|
-
*
|
|
2444
|
+
* Takes a bare catalog id, as `getMethod` does. Throws `ApiResponseError`: `404`
|
|
2445
|
+
* `pipelex.catalog.not_found` for an unknown method, `409` `pipelex.catalog.being_deleted`, and
|
|
2446
|
+
* `422` `request.invalid` for a cursor the listing did not issue for this method or a `limit`
|
|
2447
|
+
* outside 1 to 100. A page whose `items` is not an array or whose `next_cursor` is neither a
|
|
2448
|
+
* string nor `null` is an answer the SDK cannot read, thrown as an `ApiResponseError` too.
|
|
2332
2449
|
*/
|
|
2333
2450
|
async listMethodVersions(methodId, query = {}) {
|
|
2334
2451
|
const search = productQuery({ limit: query.limit, cursor: query.cursor });
|
|
@@ -2342,14 +2459,14 @@ export class PipelexApiClient {
|
|
|
2342
2459
|
*
|
|
2343
2460
|
* Takes a bare catalog id, as `getMethod` does, and the version number. Throws
|
|
2344
2461
|
* `RequestArgumentError` for a `version` that is not a positive integer, before any request.
|
|
2345
|
-
* Throws `ApiResponseError`: `404` `
|
|
2346
|
-
* published
|
|
2347
|
-
* `
|
|
2348
|
-
*
|
|
2462
|
+
* Throws `ApiResponseError`: `404` `pipelex.catalog.not_found` for a version the method never
|
|
2463
|
+
* published and for an unknown method, `409` `pipelex.catalog.being_deleted`, and `500`
|
|
2464
|
+
* `runtime.unexpected` for a version stored before methods were trees and never rewritten as
|
|
2465
|
+
* one, which names no tree to serve.
|
|
2349
2466
|
*/
|
|
2350
2467
|
async getMethodVersion(methodId, version) {
|
|
2351
2468
|
if (!Number.isSafeInteger(version) || version < 1) {
|
|
2352
|
-
throw argumentRefusal("getMethodVersion", "a version number, a positive integer", describeNumberArgument(version));
|
|
2469
|
+
throw argumentRefusal("getMethodVersion", { name: "version", what: "a version number, a positive integer" }, describeNumberArgument(version), "not_a_positive_integer");
|
|
2353
2470
|
}
|
|
2354
2471
|
const endpoint = `${methodPath(methodId, "getMethodVersion")}/versions/${version}`;
|
|
2355
2472
|
const res = await this.requestProductAnswer("GET", endpoint, undefined);
|
|
@@ -2385,28 +2502,31 @@ export class PipelexApiClient {
|
|
|
2385
2502
|
* `destination_org_id` that is not a non-empty string, a `name` or an
|
|
2386
2503
|
* `expected_draft_updated_at` given that is not a string, and a malformed idempotency key.
|
|
2387
2504
|
* Throws `ApiResponseError` when no verdict was produced, each refusal writing nothing a reader
|
|
2388
|
-
* sees but for the `503` whose `detail` names the copy: `403` `forbidden` for a request
|
|
2389
|
-
* not from a web session
|
|
2390
|
-
*
|
|
2391
|
-
*
|
|
2392
|
-
*
|
|
2393
|
-
* `409` `
|
|
2394
|
-
* `expected_draft_updated_at`; `409` `
|
|
2395
|
-
* tree the method's organization holds no record of or no longer holds every file
|
|
2396
|
-
* `
|
|
2505
|
+
* sees but for the `503` whose `detail` names the copy: `403` `auth.forbidden` for a request
|
|
2506
|
+
* that is not from a web session, or for a role that may not change methods, in the method's
|
|
2507
|
+
* organization or in the destination; `404` `pipelex.org.not_found` for a destination the
|
|
2508
|
+
* caller does not belong to, still being set up or unknown, alike; `404`
|
|
2509
|
+
* `pipelex.catalog.not_found` for an unknown method; `409` `pipelex.catalog.being_deleted`;
|
|
2510
|
+
* `409` `pipelex.catalog.update_conflict` for a draft that moved since
|
|
2511
|
+
* `expected_draft_updated_at`; a `409` whose `domain` is `runtime` and `retry` `no`, for a
|
|
2512
|
+
* draft whose tree the method's organization holds no record of or no longer holds every file
|
|
2513
|
+
* of; `413`
|
|
2514
|
+
* `request.too_large` for a copied example, or the copy's row, too large for one stored item,
|
|
2397
2515
|
* the row keeping room for the summary a publish adds, so a `name` that would leave none is
|
|
2398
|
-
* refused; `422` `
|
|
2399
|
-
* lone surrogate; `422` `
|
|
2400
|
-
* it copies, named by `
|
|
2401
|
-
* platform could not read the caller's
|
|
2402
|
-
*
|
|
2403
|
-
*
|
|
2404
|
-
*
|
|
2405
|
-
*
|
|
2406
|
-
*
|
|
2407
|
-
* `
|
|
2408
|
-
*
|
|
2409
|
-
* `
|
|
2516
|
+
* refused; `422` `request.invalid` for a body that does not validate, such as a name holding a
|
|
2517
|
+
* lone surrogate; `422` `pipelex.catalog.copy_too_large` past one of the copy's bounds on the
|
|
2518
|
+
* stored files it copies, named by the problem's `limit` member (`MethodCopyLimit`); a `502`
|
|
2519
|
+
* `runtime.unavailable`, with `retry` `yes`, when the platform could not read the caller's
|
|
2520
|
+
* memberships; a `503` `pipelex.storage.unavailable`, with `retry` `yes`, when storage failed,
|
|
2521
|
+
* and a `500` `pipelex.storage.not_configured`, with `retry` `no`, when the platform has no
|
|
2522
|
+
* usable credentials for storage; and a `503` `runtime.unavailable`, with `retry` `yes`, past
|
|
2523
|
+
* the copy's deadline, when a stored file it copies is written over meanwhile, or when DynamoDB
|
|
2524
|
+
* fails. When DynamoDB failed both the copy's row write and the read that would confirm it,
|
|
2525
|
+
* that `503`'s `detail` names the copy's method id: the copy may stand in the destination, so
|
|
2526
|
+
* look for that id there before copying again, since a retry may make a second copy. An answer
|
|
2527
|
+
* whose `outcome` is neither arm's, a `copied` one whose `method` is not an object or a
|
|
2528
|
+
* `refused` one whose `references` is not an array, is an answer the SDK cannot read, thrown as
|
|
2529
|
+
* an `ApiResponseError` too.
|
|
2410
2530
|
*/
|
|
2411
2531
|
async copyMethod(methodId, input, options = {}) {
|
|
2412
2532
|
const call = "copyMethod";
|
|
@@ -2417,13 +2537,13 @@ export class PipelexApiClient {
|
|
|
2417
2537
|
};
|
|
2418
2538
|
if (name != null) {
|
|
2419
2539
|
if (typeof name !== "string") {
|
|
2420
|
-
throw argumentRefusal(call, COPY_NAME_ARGUMENT, describeNumberArgument(name));
|
|
2540
|
+
throw argumentRefusal(call, COPY_NAME_ARGUMENT, describeNumberArgument(name), "not_a_string");
|
|
2421
2541
|
}
|
|
2422
2542
|
body.name = name;
|
|
2423
2543
|
}
|
|
2424
2544
|
if (token != null) {
|
|
2425
2545
|
if (typeof token !== "string") {
|
|
2426
|
-
throw argumentRefusal(call, COPY_DRAFT_TOKEN_ARGUMENT, describeNumberArgument(token));
|
|
2546
|
+
throw argumentRefusal(call, COPY_DRAFT_TOKEN_ARGUMENT, describeNumberArgument(token), "not_a_string");
|
|
2427
2547
|
}
|
|
2428
2548
|
body.expected_draft_updated_at = token;
|
|
2429
2549
|
}
|
|
@@ -2463,11 +2583,11 @@ export class PipelexApiClient {
|
|
|
2463
2583
|
* and a copy itself, answer an empty page. The listing is open to every member of the method's
|
|
2464
2584
|
* organization, a runner included, as every read of a method is.
|
|
2465
2585
|
*
|
|
2466
|
-
* Takes a bare catalog id, as `getMethod` does. Throws `ApiResponseError`: `404`
|
|
2467
|
-
* an unknown method, `409` `
|
|
2468
|
-
* listing did not issue for this method
|
|
2469
|
-
* `items` is not an array or whose `next_cursor` is neither a
|
|
2470
|
-
* SDK cannot read, thrown as an `ApiResponseError` too.
|
|
2586
|
+
* Takes a bare catalog id, as `getMethod` does. Throws `ApiResponseError`: `404`
|
|
2587
|
+
* `pipelex.catalog.not_found` for an unknown method, `409` `pipelex.catalog.being_deleted`, and
|
|
2588
|
+
* `422` `request.invalid` for a cursor the listing did not issue for this method or a `limit`
|
|
2589
|
+
* outside 1 to 100. A page whose `items` is not an array or whose `next_cursor` is neither a
|
|
2590
|
+
* string nor `null` is an answer the SDK cannot read, thrown as an `ApiResponseError` too.
|
|
2471
2591
|
*/
|
|
2472
2592
|
async listMethodCopies(methodId, query = {}) {
|
|
2473
2593
|
const search = productQuery({ limit: query.limit, cursor: query.cursor });
|
|
@@ -2487,7 +2607,7 @@ export class PipelexApiClient {
|
|
|
2487
2607
|
* it with a `409`.
|
|
2488
2608
|
*
|
|
2489
2609
|
* A double-clicked delete is safe: the claim is a conditional write, so the
|
|
2490
|
-
* second call is an `ApiResponseError` (`409
|
|
2610
|
+
* second call is an `ApiResponseError` (`409` `pipelex.catalog.being_deleted`) rather than a
|
|
2491
2611
|
* second cascade over the same runs. An unknown or foreign-org id is a `404`. The
|
|
2492
2612
|
* erasure deletes the method's published versions with the rest. Takes a bare catalog id,
|
|
2493
2613
|
* as `getMethod` does: the whole method is erased, never one of its versions.
|
|
@@ -2498,8 +2618,9 @@ export class PipelexApiClient {
|
|
|
2498
2618
|
// ── Method examples (`/v1/methods/{id}/examples`) ──────────────────────
|
|
2499
2619
|
//
|
|
2500
2620
|
// Every example route takes a bare catalog id, as `getMethod` does, and answers `404`
|
|
2501
|
-
// `not_found` for an unknown method or example and `409`
|
|
2502
|
-
// method's erasure runs. The writes are `403` to a
|
|
2621
|
+
// `pipelex.catalog.not_found` for an unknown method or example and `409`
|
|
2622
|
+
// `pipelex.catalog.being_deleted` while the method's erasure runs. The writes are `403` to a
|
|
2623
|
+
// read-only key, which may still read.
|
|
2503
2624
|
//
|
|
2504
2625
|
// Every answer carrying an example is read with `parseExactJson`, so an integer past
|
|
2505
2626
|
// `Number.MAX_SAFE_INTEGER` in its inputs, which another client may have saved, is a `bigint`
|
|
@@ -2516,8 +2637,8 @@ export class PipelexApiClient {
|
|
|
2516
2637
|
* closed, and a `null` `run_id` reads as not given. Throws `RequestArgumentError` before any
|
|
2517
2638
|
* request when `name` or `inputs` is not given (`null` counting as not given), and for a
|
|
2518
2639
|
* `run_id` that is given but is not a non-empty string. Throws `ApiResponseError`: `404`
|
|
2519
|
-
* `
|
|
2520
|
-
* organization, with nothing written; `413` `
|
|
2640
|
+
* `run.not_found` for a `run_id` that is not one of this method's runs in the caller's
|
|
2641
|
+
* organization, with nothing written; `413` `request.too_large` for an example larger than one
|
|
2521
2642
|
* stored item holds; `422` for a name that is empty, blank or over 200 characters, or inputs
|
|
2522
2643
|
* that are not a JSON object; and the codes every example route answers.
|
|
2523
2644
|
*/
|
|
@@ -2539,12 +2660,12 @@ export class PipelexApiClient {
|
|
|
2539
2660
|
* may be short while `nextCursor` is set, because the platform reads examples whole and stops
|
|
2540
2661
|
* a read at 1 MB; `iterateMethodExamples` follows the cursor for a caller that wants them all.
|
|
2541
2662
|
*
|
|
2542
|
-
* Throws `ApiResponseError`: `
|
|
2543
|
-
*
|
|
2544
|
-
*
|
|
2545
|
-
*
|
|
2546
|
-
*
|
|
2547
|
-
*
|
|
2663
|
+
* Throws `ApiResponseError`: `422` `request.invalid` for a cursor the listing did not issue for
|
|
2664
|
+
* this method or a `limit` outside 1 to 100, and the codes every example route answers. A page
|
|
2665
|
+
* whose `items` is not an array, whose `next_cursor` is neither a string nor `null`, or one of
|
|
2666
|
+
* whose items is not an example `readMethodExample` can read, is an answer the SDK cannot read,
|
|
2667
|
+
* thrown as an `ApiResponseError` too. An example whose inputs hold an integer past the safe
|
|
2668
|
+
* range is read like any other, that integer as a `bigint`.
|
|
2548
2669
|
*/
|
|
2549
2670
|
async listMethodExamples(methodId, query = {}) {
|
|
2550
2671
|
const search = productQuery({ limit: query.limit, cursor: query.cursor });
|
|
@@ -2611,7 +2732,7 @@ export class PipelexApiClient {
|
|
|
2611
2732
|
*
|
|
2612
2733
|
* Throws `RequestArgumentError` before any request for an example id that is empty, `.` or
|
|
2613
2734
|
* `..`, or when neither `name` nor `inputs` is given. Throws `ApiResponseError`: `413`
|
|
2614
|
-
* `
|
|
2735
|
+
* `request.too_large` for inputs larger than one stored item holds, `422` for a name that is
|
|
2615
2736
|
* empty, blank or over 200 characters, or inputs that are not a JSON object, a `503` that wrote
|
|
2616
2737
|
* nothing, safe to retry, when concurrent edits kept landing under the write, and the codes
|
|
2617
2738
|
* every example route answers.
|
|
@@ -2625,7 +2746,7 @@ export class PipelexApiClient {
|
|
|
2625
2746
|
if (inputs != null)
|
|
2626
2747
|
body.inputs = inputs;
|
|
2627
2748
|
if (Object.keys(body).length === 0) {
|
|
2628
|
-
throw new RequestArgumentError("updateMethodExample() needs a new name, new inputs, or both; got neither.");
|
|
2749
|
+
throw new RequestArgumentError("updateMethodExample() needs a new name, new inputs, or both; got neither.", { problem: argumentInvalidProblem("name", "nothing_to_update") });
|
|
2629
2750
|
}
|
|
2630
2751
|
return this.requestMethodExample("PATCH", endpoint, body);
|
|
2631
2752
|
}
|
|
@@ -2641,13 +2762,13 @@ export class PipelexApiClient {
|
|
|
2641
2762
|
* since and nothing was written. A stale run is the expected end of a run that outlasted an
|
|
2642
2763
|
* edit, so it is answered, never thrown. Both arms carry the `example` as it stands.
|
|
2643
2764
|
*
|
|
2644
|
-
* Throws `RequestArgumentError` before any request, checking in this order: an example id that
|
|
2645
|
-
*
|
|
2646
|
-
*
|
|
2647
|
-
*
|
|
2648
|
-
* for a run that is not one of this method's, with nothing written, and the codes
|
|
2649
|
-
* route answers. An answer whose `outcome` is neither arm's, or whose `example` is
|
|
2650
|
-
* example `readMethodExample` can read, is an answer the SDK cannot read, thrown as an
|
|
2765
|
+
* Throws `RequestArgumentError` before any request, checking in this order: an example id that is
|
|
2766
|
+
* empty, `.` or `..`, or holds a lone surrogate, a `run_id` that is not a non-empty string, and
|
|
2767
|
+
* an `input_revision` that is not a positive integer. Throws `ApiResponseError` when no verdict
|
|
2768
|
+
* was produced: `404` `pipelex.catalog.not_found` for an example deleted since, `404`
|
|
2769
|
+
* `run.not_found` for a run that is not one of this method's, with nothing written, and the codes
|
|
2770
|
+
* every example route answers. An answer whose `outcome` is neither arm's, or whose `example` is
|
|
2771
|
+
* not an example `readMethodExample` can read, is an answer the SDK cannot read, thrown as an
|
|
2651
2772
|
* `ApiResponseError` too, read as `publishMethod` reads its verdict.
|
|
2652
2773
|
*/
|
|
2653
2774
|
async recordMethodExampleLatestRun(methodId, exampleId, input) {
|
|
@@ -2656,7 +2777,10 @@ export class PipelexApiClient {
|
|
|
2656
2777
|
const { run_id: runId, input_revision: revision } = input ?? {};
|
|
2657
2778
|
const run = requireNonEmptyId(runId, call, RECORD_RUN_ID_ARGUMENT);
|
|
2658
2779
|
if (typeof revision !== "number" || !Number.isSafeInteger(revision) || revision < 1) {
|
|
2659
|
-
throw argumentRefusal(call,
|
|
2780
|
+
throw argumentRefusal(call, {
|
|
2781
|
+
name: "input_revision",
|
|
2782
|
+
what: "an input_revision, the example's revision whose inputs the run used, a positive integer",
|
|
2783
|
+
}, describeNumberArgument(revision), "not_a_positive_integer");
|
|
2660
2784
|
}
|
|
2661
2785
|
const res = await this.requestProductAnswer("PUT", endpoint, {
|
|
2662
2786
|
run_id: run,
|
|
@@ -2694,8 +2818,8 @@ export class PipelexApiClient {
|
|
|
2694
2818
|
* the body is closed, and `null` reads as not given. Throws `RequestArgumentError` before any
|
|
2695
2819
|
* request when `name` or `inputs` is not given, for an `example_id` that is not a non-empty
|
|
2696
2820
|
* string or an `input_revision` that is not a positive integer, and when only one of the two is
|
|
2697
|
-
* given. Throws `ApiResponseError`: `403` `
|
|
2698
|
-
* methods, such as a runner; `413` `
|
|
2821
|
+
* given. Throws `ApiResponseError`: `403` `auth.forbidden` for a role that may not change
|
|
2822
|
+
* methods, such as a runner; `413` `request.too_large` for an example larger than one stored item
|
|
2699
2823
|
* holds; `422` for a name that is empty, blank or over 200 characters, or inputs that are not a
|
|
2700
2824
|
* JSON object; and the codes every example route answers. An answer whose `outcome` is neither
|
|
2701
2825
|
* arm's, or whose `example` is not an example `readMethodExample` can read, is an answer the SDK
|
|
@@ -2712,13 +2836,15 @@ export class PipelexApiClient {
|
|
|
2712
2836
|
}
|
|
2713
2837
|
if (revision != null) {
|
|
2714
2838
|
if (typeof revision !== "number" || !Number.isSafeInteger(revision) || revision < 1) {
|
|
2715
|
-
throw argumentRefusal(call, ASSISTANT_REVISION_ARGUMENT, describeNumberArgument(revision));
|
|
2839
|
+
throw argumentRefusal(call, ASSISTANT_REVISION_ARGUMENT, describeNumberArgument(revision), "not_a_positive_integer");
|
|
2716
2840
|
}
|
|
2717
2841
|
body.input_revision = revision;
|
|
2718
2842
|
}
|
|
2719
2843
|
if ((body.example_id === undefined) !== (body.input_revision === undefined)) {
|
|
2720
2844
|
throw new RequestArgumentError(`${call}() names the assistant's example by both its example_id and the input_revision ` +
|
|
2721
|
-
`it was read at, or neither; got only ${body.example_id === undefined ? "input_revision" : "example_id"}
|
|
2845
|
+
`it was read at, or neither; got only ${body.example_id === undefined ? "input_revision" : "example_id"}.`, {
|
|
2846
|
+
problem: argumentInvalidProblem(body.example_id === undefined ? "example_id" : "input_revision", "missing"),
|
|
2847
|
+
});
|
|
2722
2848
|
}
|
|
2723
2849
|
const res = await this.requestProductAnswer("POST", endpoint, body);
|
|
2724
2850
|
const { answer, outcome, subject } = this.readVerdict("POST", endpoint, res, {
|
|
@@ -2734,10 +2860,10 @@ export class PipelexApiClient {
|
|
|
2734
2860
|
* Delete an example — `DELETE /v1/methods/{id}/examples/{example_id}`. The platform answers
|
|
2735
2861
|
* `204`. Deleting the example leaves its latest run in the method's history.
|
|
2736
2862
|
*
|
|
2737
|
-
* Throws `RequestArgumentError` for an example id that is empty, `.` or `..`, before any
|
|
2738
|
-
*
|
|
2739
|
-
* `ApiResponseError`: `404` `not_found` when there is no such example, one
|
|
2740
|
-
* included, and the codes every example route answers.
|
|
2863
|
+
* Throws `RequestArgumentError` for an example id that is empty, `.` or `..`, before any request:
|
|
2864
|
+
* `..` would address the method, and its erasure would answer the delete. Throws
|
|
2865
|
+
* `ApiResponseError`: `404` `pipelex.catalog.not_found` when there is no such example, one
|
|
2866
|
+
* already deleted included, and the codes every example route answers.
|
|
2741
2867
|
*/
|
|
2742
2868
|
async deleteMethodExample(methodId, exampleId) {
|
|
2743
2869
|
await this.requestProduct("DELETE", methodExamplePath(methodId, exampleId, "deleteMethodExample"));
|
|
@@ -2780,17 +2906,17 @@ export class PipelexApiClient {
|
|
|
2780
2906
|
*
|
|
2781
2907
|
* Throws `RequestArgumentError` for an organization id that is empty, `.` or `..`, before any
|
|
2782
2908
|
* request. Throws `ApiResponseError`, refusing every change of the request when it refuses one:
|
|
2783
|
-
* `403` `
|
|
2784
|
-
* a caller who is not its admin, `404`
|
|
2785
|
-
*
|
|
2786
|
-
* not `#rrggbb` or a key the body does
|
|
2787
|
-
*
|
|
2788
|
-
*
|
|
2789
|
-
*
|
|
2790
|
-
* `
|
|
2791
|
-
*
|
|
2792
|
-
*
|
|
2793
|
-
* write may have landed or not: the same input sent again is safe.
|
|
2909
|
+
* `403` `pipelex.org.mismatch` when `orgId` is not the active organization, `403`
|
|
2910
|
+
* `auth.forbidden` for a caller who is not its admin, `404` `pipelex.org.not_found` for a missing
|
|
2911
|
+
* organization, `409` `pipelex.org.not_manageable` for a personal workspace, which has no
|
|
2912
|
+
* branding, and `422` `request.invalid` for a colour that is not `#rrggbb` or a key the body does
|
|
2913
|
+
* not know. A logo's refusal names its slot in its problem's `errors`, each item's
|
|
2914
|
+
* `location.field` being `square_logo` or `wide_logo` and its `detail` the sentence to show under
|
|
2915
|
+
* that slot: `422` `pipelex.storage.uri_invalid` for a URI that is not a storage URI, `403`
|
|
2916
|
+
* `pipelex.org.mismatch` for another organization's file, and `422` `request.invalid` with an
|
|
2917
|
+
* item per broken rule, its code one `LogoRefusalCode` names, for a file the platform refused. A
|
|
2918
|
+
* `503` `runtime.unavailable` is a failure of the platform's storage or its database, after which
|
|
2919
|
+
* the write may have landed or not: the same input sent again is safe.
|
|
2794
2920
|
*/
|
|
2795
2921
|
async updateOrganizationBranding(orgId, input) {
|
|
2796
2922
|
const segment = idPathSegment(orgId, "updateOrganizationBranding", ORG_ID_ARGUMENT);
|
|
@@ -2814,7 +2940,7 @@ export class PipelexApiClient {
|
|
|
2814
2940
|
}
|
|
2815
2941
|
/**
|
|
2816
2942
|
* Switch the existing subscription's plan — `POST /v1/billing/change-plan`.
|
|
2817
|
-
* A 409 `
|
|
2943
|
+
* A 409 `pipelex.billing.subscription_conflict` (`ApiResponseError.code`) means there is no subscription
|
|
2818
2944
|
* to change — start one via `createCheckout` first.
|
|
2819
2945
|
*/
|
|
2820
2946
|
async changePlan(input) {
|
|
@@ -2822,7 +2948,7 @@ export class PipelexApiClient {
|
|
|
2822
2948
|
}
|
|
2823
2949
|
/**
|
|
2824
2950
|
* A Stripe billing-portal session URL — `GET /v1/billing/portal`. A 409
|
|
2825
|
-
* `
|
|
2951
|
+
* `pipelex.billing.subscription_conflict` (`ApiResponseError.code`) means there is no subscription yet.
|
|
2826
2952
|
*/
|
|
2827
2953
|
async getBillingPortal() {
|
|
2828
2954
|
return this.requestProduct("GET", "billing/portal");
|
|
@@ -2833,7 +2959,7 @@ export class PipelexApiClient {
|
|
|
2833
2959
|
}
|
|
2834
2960
|
/**
|
|
2835
2961
|
* Mint a Pipelex API key — `POST /v1/pipelex-api-keys`. The plaintext
|
|
2836
|
-
* `api_key` is returned ONCE. A 409 `
|
|
2962
|
+
* `api_key` is returned ONCE. A 409 `pipelex.billing.plan_limit`
|
|
2837
2963
|
* (`ApiResponseError.code`) means the per-account key limit is hit.
|
|
2838
2964
|
*/
|
|
2839
2965
|
async createPipelexApiKey(input) {
|
|
@@ -2906,8 +3032,9 @@ export class PipelexApiClient {
|
|
|
2906
3032
|
/**
|
|
2907
3033
|
* Upload a base64 file — `POST /v1/upload`, answering the file's `uri`, the `filename` as sent
|
|
2908
3034
|
* and the `file_id` of its record. An answer with no non-empty string `uri` names no
|
|
2909
|
-
* stored file, so it is an answer the SDK cannot read: an `ApiResponseError
|
|
2910
|
-
*
|
|
3035
|
+
* stored file, so it is an answer the SDK cannot read: an `ApiResponseError` whose problem is
|
|
3036
|
+
* `client.response_unreadable`, which `uploadFile` wraps as an `UploadTransportError` with the
|
|
3037
|
+
* reason `unexpected`.
|
|
2911
3038
|
*/
|
|
2912
3039
|
async upload(input) {
|
|
2913
3040
|
const res = await this.requestProductAnswer("POST", "upload", input);
|
|
@@ -2935,9 +3062,9 @@ export class PipelexApiClient {
|
|
|
2935
3062
|
* target is short-lived and a bearer capability, so keep it out of logs. The route
|
|
2936
3063
|
* never replays a grant: ask again for a new one rather than retrying; with a
|
|
2937
3064
|
* `sha256`, a new grant answers the same file record. A declared `size` over the cap
|
|
2938
|
-
* is a `413` `ApiResponseError` (`code` `
|
|
2939
|
-
* a `size` other than the one the organization holds those bytes at are a `422
|
|
2940
|
-
* deployment without the route answers a `404`. See `docs/input-preparation.md`.
|
|
3065
|
+
* is a `413` `ApiResponseError` (`code` `request.too_large`), a malformed `sha256` and
|
|
3066
|
+
* a `size` other than the one the organization holds those bytes at are a `422`
|
|
3067
|
+
* (`request.invalid`), and a deployment without the route answers a `404`. See `docs/input-preparation.md`.
|
|
2941
3068
|
*/
|
|
2942
3069
|
async requestUploadGrant(input, options = {}) {
|
|
2943
3070
|
return this.requestProductObject("POST", "upload/grant", input, options);
|
|
@@ -3007,7 +3134,7 @@ export class PipelexApiClient {
|
|
|
3007
3134
|
*
|
|
3008
3135
|
* Throws `ApiResponseError`: `400` for a date bound that is not an instant with an offset, a
|
|
3009
3136
|
* range whose start is after its end, or a cursor of another list or other filters; `404`
|
|
3010
|
-
* `
|
|
3137
|
+
* `run.not_found` for a run that is not the organization's; `422` for a `limit` outside 1 to
|
|
3011
3138
|
* 200, an unknown `kind`, more than 100 values of `format` or `mediaType`, or an id that cannot
|
|
3012
3139
|
* be one. A page whose `items` is not an array or whose `next_cursor` is neither a string nor
|
|
3013
3140
|
* `null` is an `ApiResponseError` of an answer the SDK cannot read. See `docs/files.md`.
|
|
@@ -3061,10 +3188,11 @@ export class PipelexApiClient {
|
|
|
3061
3188
|
* `stored` when its bytes are there.
|
|
3062
3189
|
*
|
|
3063
3190
|
* Throws `RequestArgumentError` for a file id that is empty, `.` or `..`, or holds a lone
|
|
3064
|
-
* surrogate, before any request. Throws `ApiResponseError`: `404` `not_found` for
|
|
3065
|
-
* not the organization's; `422` for a `usesLimit` outside 1 to 200 or an id that
|
|
3066
|
-
* An answer that is not an object, or whose `uses` is not a page of `items` and
|
|
3067
|
-
* an answer the SDK cannot read, thrown as an `ApiResponseError` too. See
|
|
3191
|
+
* surrogate, before any request. Throws `ApiResponseError`: `404` `pipelex.storage.not_found` for
|
|
3192
|
+
* a file that is not the organization's; `422` for a `usesLimit` outside 1 to 200 or an id that
|
|
3193
|
+
* cannot be one. An answer that is not an object, or whose `uses` is not a page of `items` and
|
|
3194
|
+
* `next_cursor`, is an answer the SDK cannot read, thrown as an `ApiResponseError` too. See
|
|
3195
|
+
* `docs/files.md`.
|
|
3068
3196
|
*/
|
|
3069
3197
|
async getFile(fileId, query = {}) {
|
|
3070
3198
|
const search = productQuery({ uses_limit: query.usesLimit, uses_cursor: query.usesCursor });
|
|
@@ -3091,10 +3219,10 @@ export class PipelexApiClient {
|
|
|
3091
3219
|
* kept records.
|
|
3092
3220
|
*
|
|
3093
3221
|
* Throws `RequestArgumentError` for a `uri` that is not a non-empty string, before any request.
|
|
3094
|
-
* Throws `ApiResponseError`: `
|
|
3095
|
-
* `
|
|
3096
|
-
* organization's, and `422` for an address over 512 characters. An answer whose `items` is
|
|
3097
|
-
* array is an answer the SDK cannot read, thrown as an `ApiResponseError` too.
|
|
3222
|
+
* Throws `ApiResponseError`: `pipelex.storage.uri_invalid` for a malformed address, `403`
|
|
3223
|
+
* `pipelex.org.mismatch` for another organization's, `404` `run.not_found` for a run that is not
|
|
3224
|
+
* the organization's, and `422` for an address over 512 characters. An answer whose `items` is
|
|
3225
|
+
* not an array is an answer the SDK cannot read, thrown as an `ApiResponseError` too.
|
|
3098
3226
|
*/
|
|
3099
3227
|
async getFilesByUri(uri, options = {}) {
|
|
3100
3228
|
const checkedUri = requireNonEmptyId(uri, "getFilesByUri", STORAGE_URI_ARGUMENT);
|
|
@@ -3123,16 +3251,16 @@ export class PipelexApiClient {
|
|
|
3123
3251
|
*
|
|
3124
3252
|
* Sends `name` alone: the body is closed. Throws `RequestArgumentError` for a file id that is
|
|
3125
3253
|
* empty, `.` or `..`, or holds a lone surrogate, and for a `name` that is not a string, before
|
|
3126
|
-
* any request. Throws `ApiResponseError`: `403` `
|
|
3127
|
-
* `not_found` for a file that is not the organization's; `422` for a name over
|
|
3128
|
-
* with nothing left once cleaned. An answer that is not an object is an answer the SDK cannot
|
|
3254
|
+
* any request. Throws `ApiResponseError`: `403` `auth.forbidden` past the rule above; `404`
|
|
3255
|
+
* `pipelex.storage.not_found` for a file that is not the organization's; `422` for a name over
|
|
3256
|
+
* 512 characters or with nothing left once cleaned. An answer that is not an object is an answer the SDK cannot
|
|
3129
3257
|
* read, thrown as an `ApiResponseError` too.
|
|
3130
3258
|
*/
|
|
3131
3259
|
async renameFile(fileId, input) {
|
|
3132
3260
|
const segment = idPathSegment(fileId, "renameFile", FILE_ID_ARGUMENT);
|
|
3133
3261
|
const { name } = input ?? {};
|
|
3134
3262
|
if (typeof name !== "string") {
|
|
3135
|
-
throw argumentRefusal("renameFile", RENAME_NAME_ARGUMENT, describeNumberArgument(name));
|
|
3263
|
+
throw argumentRefusal("renameFile", RENAME_NAME_ARGUMENT, describeNumberArgument(name), "not_a_string");
|
|
3136
3264
|
}
|
|
3137
3265
|
return this.requestProductObject("PATCH", `files/${segment}`, { name });
|
|
3138
3266
|
}
|
|
@@ -3159,14 +3287,14 @@ export class PipelexApiClient {
|
|
|
3159
3287
|
* Sends each file as `{ file_id }` or `{ uri }` and nothing else: the body is closed. Throws
|
|
3160
3288
|
* `RequestArgumentError` before any request when `files` is not a list, or one of its items does
|
|
3161
3289
|
* not name exactly one of `file_id` and `uri` as a non-empty string. Throws `ApiResponseError`:
|
|
3162
|
-
* `422` for more than 100 files, or an id or address longer than one can be;
|
|
3163
|
-
* the platform cannot sign at all. An answer that is not one item per requested file, each an
|
|
3290
|
+
* `422` `request.invalid` for more than 100 files, or an id or address longer than one can be;
|
|
3291
|
+
* `500` `pipelex.storage.not_configured` when the platform cannot sign at all. An answer that is not one item per requested file, each an
|
|
3164
3292
|
* object with its `url` or its `error`, is an answer the SDK cannot read, thrown as an
|
|
3165
3293
|
* `ApiResponseError` whose body hides every link.
|
|
3166
3294
|
*/
|
|
3167
3295
|
async linkFiles(files) {
|
|
3168
3296
|
if (!Array.isArray(files)) {
|
|
3169
|
-
throw argumentRefusal("linkFiles", "a list of files to link", describeNumberArgument(files));
|
|
3297
|
+
throw argumentRefusal("linkFiles", { name: "files", what: "a list of files to link" }, describeNumberArgument(files), "not_a_list");
|
|
3170
3298
|
}
|
|
3171
3299
|
const targets = files.map((target) => fileLinkTarget(target));
|
|
3172
3300
|
const res = withLinks(await this.requestProductAnswer("POST", "files/links", { files: targets }));
|
|
@@ -3202,18 +3330,18 @@ export class PipelexApiClient {
|
|
|
3202
3330
|
* list of non-empty strings, when `run_id` is not a non-empty string, and when `name` is not a
|
|
3203
3331
|
* string. Throws `ApiResponseError`:
|
|
3204
3332
|
*
|
|
3205
|
-
* - `422` `
|
|
3206
|
-
* `
|
|
3207
|
-
* the selection is;
|
|
3208
|
-
* - `422` `
|
|
3209
|
-
* - `404` `not_found` for a file that is not the organization's, the `detail`
|
|
3210
|
-
* of them, or a run whose files hold nothing stored to zip; `404`
|
|
3211
|
-
* that is not the organization's;
|
|
3212
|
-
* - `409` `
|
|
3213
|
-
*
|
|
3214
|
-
*
|
|
3215
|
-
* - `503` `
|
|
3216
|
-
* sent again asks for a new archive.
|
|
3333
|
+
* - `422` `pipelex.files.archive_too_large` past a cap, at most 500 files or 2,000,000,000
|
|
3334
|
+
* bytes, whose problem's `limit` member names it (`FileArchiveLimit`), so the person can be told
|
|
3335
|
+
* how far over the selection is;
|
|
3336
|
+
* - `422` `request.invalid` for more than 1,000 ids listed, or an id longer than one can be;
|
|
3337
|
+
* - `404` `pipelex.storage.not_found` for a file that is not the organization's, the `detail`
|
|
3338
|
+
* naming up to five of them, or a run whose files hold nothing stored to zip; `404`
|
|
3339
|
+
* `run.not_found` for a run that is not the organization's;
|
|
3340
|
+
* - `409` `run.not_finished`, `retry` `yes`, for a run whose files are not all recorded yet,
|
|
3341
|
+
* which the same request clears a few seconds after the run ends, and a `409` with `retry` `no`
|
|
3342
|
+
* for a selected file whose upload has not finished;
|
|
3343
|
+
* - `503` `runtime.unavailable`, `retry` `yes`, when the archive could not be queued: the same
|
|
3344
|
+
* request sent again asks for a new archive.
|
|
3217
3345
|
*
|
|
3218
3346
|
* An answer that is not an archive, an object with a string `archive_id` and a known `state`, is
|
|
3219
3347
|
* an answer the SDK cannot read, thrown as an `ApiResponseError` too.
|
|
@@ -3221,7 +3349,7 @@ export class PipelexApiClient {
|
|
|
3221
3349
|
async requestFileArchive(request) {
|
|
3222
3350
|
const call = "requestFileArchive";
|
|
3223
3351
|
if (!isPlainObject(request)) {
|
|
3224
|
-
throw argumentRefusal(call, "an archive request, an object naming `file_ids` or `run_id`", describeNumberArgument(request));
|
|
3352
|
+
throw argumentRefusal(call, { name: "request", what: "an archive request, an object naming `file_ids` or `run_id`" }, describeNumberArgument(request), "not_an_object");
|
|
3225
3353
|
}
|
|
3226
3354
|
const raw = request;
|
|
3227
3355
|
const fileIds = raw.file_ids ?? undefined;
|
|
@@ -3229,12 +3357,12 @@ export class PipelexApiClient {
|
|
|
3229
3357
|
const name = raw.name ?? undefined;
|
|
3230
3358
|
if ((fileIds === undefined) === (runId === undefined)) {
|
|
3231
3359
|
throw new RequestArgumentError(`${call}() names the files to zip by exactly one of file_ids and run_id; got ` +
|
|
3232
|
-
`${fileIds === undefined ? "neither" : "both"}
|
|
3360
|
+
`${fileIds === undefined ? "neither" : "both"}.`, { problem: argumentInvalidProblem("file_ids", "not_exactly_one") });
|
|
3233
3361
|
}
|
|
3234
3362
|
const body = {};
|
|
3235
3363
|
if (fileIds !== undefined) {
|
|
3236
3364
|
if (!Array.isArray(fileIds) || fileIds.length === 0) {
|
|
3237
|
-
throw argumentRefusal(call, "file_ids, a non-empty list of the file ids to zip", Array.isArray(fileIds) ? "an empty list" : describeNumberArgument(fileIds));
|
|
3365
|
+
throw argumentRefusal(call, { name: "file_ids", what: "file_ids, a non-empty list of the file ids to zip" }, Array.isArray(fileIds) ? "an empty list" : describeNumberArgument(fileIds), Array.isArray(fileIds) ? "empty" : "not_a_list");
|
|
3238
3366
|
}
|
|
3239
3367
|
body.file_ids = fileIds.map((id) => requireNonEmptyId(id, call, ARCHIVE_FILE_ID_ARGUMENT));
|
|
3240
3368
|
}
|
|
@@ -3243,7 +3371,7 @@ export class PipelexApiClient {
|
|
|
3243
3371
|
}
|
|
3244
3372
|
if (name !== undefined) {
|
|
3245
3373
|
if (typeof name !== "string") {
|
|
3246
|
-
throw argumentRefusal(call, ARCHIVE_NAME_ARGUMENT, describeNumberArgument(name));
|
|
3374
|
+
throw argumentRefusal(call, ARCHIVE_NAME_ARGUMENT, describeNumberArgument(name), "not_a_string");
|
|
3247
3375
|
}
|
|
3248
3376
|
body.name = name;
|
|
3249
3377
|
}
|
|
@@ -3263,10 +3391,10 @@ export class PipelexApiClient {
|
|
|
3263
3391
|
* still `queued` or `building` an hour after it was asked for reads as `failed`.
|
|
3264
3392
|
*
|
|
3265
3393
|
* Throws `RequestArgumentError` for an archive id that is empty, `.` or `..`, or holds a lone
|
|
3266
|
-
* surrogate, before any request. Throws `ApiResponseError`: `404` `not_found` for
|
|
3267
|
-
* is not the organization's, never existed, or has expired, a day after
|
|
3268
|
-
* the link cannot be signed. An answer that is not an archive is an
|
|
3269
|
-
* thrown as an `ApiResponseError` whose body hides the link.
|
|
3394
|
+
* surrogate, before any request. Throws `ApiResponseError`: `404` `pipelex.storage.not_found` for
|
|
3395
|
+
* an archive that is not the organization's, never existed, or has expired, a day after
|
|
3396
|
+
* `created_at`; `500` when the link cannot be signed. An answer that is not an archive is an
|
|
3397
|
+
* answer the SDK cannot read, thrown as an `ApiResponseError` whose body hides the link.
|
|
3270
3398
|
*/
|
|
3271
3399
|
async getFileArchive(archiveId) {
|
|
3272
3400
|
const segment = idPathSegment(archiveId, "getFileArchive", ARCHIVE_ID_ARGUMENT);
|
|
@@ -3312,14 +3440,15 @@ export class PipelexApiClient {
|
|
|
3312
3440
|
* `expected_updated_at`, and nothing else: the body is closed, and a `null` token reads as not
|
|
3313
3441
|
* given. Throws `RequestArgumentError` before any request for a `file_id` or a `path` that is not
|
|
3314
3442
|
* a non-empty string, and an `expected_updated_at` that is not a string. Throws
|
|
3315
|
-
* `ApiResponseError`: `403` `
|
|
3316
|
-
* for an unknown method or a file that is not the organization's; `409` `
|
|
3317
|
-
* `409` `
|
|
3318
|
-
* file whose upload has not finished or whose stored bytes do not match its record; `422`
|
|
3319
|
-
* `
|
|
3320
|
-
*
|
|
3321
|
-
*
|
|
3322
|
-
*
|
|
3443
|
+
* `ApiResponseError`: `403` `auth.forbidden` for a role that may not change methods; `404`
|
|
3444
|
+
* for an unknown method or a file that is not the organization's; `409` `pipelex.catalog.being_deleted`,
|
|
3445
|
+
* `409` `pipelex.catalog.update_conflict` for a stale `expected_updated_at`, and `409` `conflict` for a
|
|
3446
|
+
* file whose upload has not finished or whose stored bytes do not match its record; and `422`
|
|
3447
|
+
* `method.package_invalid` for a path the draft cannot hold, such as `.env` or `../logo.png`,
|
|
3448
|
+
* naming the path in its `params`, and past the deployment's limits, its `params` carrying
|
|
3449
|
+
* `reason` `too_large` and the integer `limit`. An answer whose `blob` is neither arm's, or
|
|
3450
|
+
* whose `file` or `method` is not an object, is an answer the SDK cannot read, thrown as an
|
|
3451
|
+
* `ApiResponseError` too.
|
|
3323
3452
|
*/
|
|
3324
3453
|
async copyFileIntoDraft(methodId, input) {
|
|
3325
3454
|
const call = "copyFileIntoDraft";
|
|
@@ -3331,7 +3460,7 @@ export class PipelexApiClient {
|
|
|
3331
3460
|
};
|
|
3332
3461
|
if (token != null) {
|
|
3333
3462
|
if (typeof token !== "string") {
|
|
3334
|
-
throw argumentRefusal(call, DRAFT_TOKEN_ARGUMENT, describeNumberArgument(token));
|
|
3463
|
+
throw argumentRefusal(call, DRAFT_TOKEN_ARGUMENT, describeNumberArgument(token), "not_a_string");
|
|
3335
3464
|
}
|
|
3336
3465
|
body.expected_updated_at = token;
|
|
3337
3466
|
}
|
|
@@ -3365,7 +3494,7 @@ export class PipelexApiClient {
|
|
|
3365
3494
|
* `iterateRuns` does the latter for you.
|
|
3366
3495
|
*
|
|
3367
3496
|
* Each item is a `RunHistoryItem` — the id, status, timestamps, pipe and,
|
|
3368
|
-
* for a failed run, its
|
|
3497
|
+
* for a failed run, its own problem. The run's organization, creator,
|
|
3369
3498
|
* method and workflow id are not on the list; `getRunDetail` returns them.
|
|
3370
3499
|
*
|
|
3371
3500
|
* `createdFrom` / `createdTo` are applied server-side as index key
|
|
@@ -3398,7 +3527,7 @@ export class PipelexApiClient {
|
|
|
3398
3527
|
cursor: query.cursor,
|
|
3399
3528
|
});
|
|
3400
3529
|
const page = await this.requestPage(`runs${search}`);
|
|
3401
|
-
return { items: page.items.map(
|
|
3530
|
+
return { items: page.items.map(withCheckedProblem), nextCursor: page.nextCursor };
|
|
3402
3531
|
}
|
|
3403
3532
|
/**
|
|
3404
3533
|
* Every run of a method, streamed — follows the cursor for you.
|
|
@@ -3436,8 +3565,8 @@ export class PipelexApiClient {
|
|
|
3436
3565
|
* The whole record for one run — `GET /v1/runs/{id}`.
|
|
3437
3566
|
*
|
|
3438
3567
|
* The ONLY call that returns what the run actually executed and its
|
|
3439
|
-
* `inputs`. Kept off the status read, which pollers hit every few seconds. A failed run's
|
|
3440
|
-
*
|
|
3568
|
+
* `inputs`. Kept off the status read, which pollers hit every few seconds. A failed run's problem, `error`, is kept when it is a
|
|
3569
|
+
* problem document and read as `null` otherwise.
|
|
3441
3570
|
*
|
|
3442
3571
|
* Read with `parseExactJson`, so an integer past `Number.MAX_SAFE_INTEGER` in the inputs is a
|
|
3443
3572
|
* `bigint` holding its exact digits, and the inputs passed back to `start` or saved as an
|
|
@@ -3446,7 +3575,7 @@ export class PipelexApiClient {
|
|
|
3446
3575
|
async getRunDetail(runId) {
|
|
3447
3576
|
const endpoint = `runs/${idPathSegment(runId, "getRunDetail", RUN_ID_ARGUMENT)}`;
|
|
3448
3577
|
const res = await this.requestProductAnswer("GET", endpoint, undefined);
|
|
3449
|
-
return
|
|
3578
|
+
return withCheckedProblem(this.readObjectAnswer("GET", endpoint, res, {}, parseExactJson));
|
|
3450
3579
|
}
|
|
3451
3580
|
/**
|
|
3452
3581
|
* One page of a run's files, sorted by where they sit in the run — `GET /v1/runs/{id}/files`.
|
|
@@ -3516,7 +3645,7 @@ export class PipelexApiClient {
|
|
|
3516
3645
|
Object.keys(api).length === 0 &&
|
|
3517
3646
|
Object.keys(hosted).length === 0 &&
|
|
3518
3647
|
Object.keys(extensions).length === 0) {
|
|
3519
|
-
throw new RequestArgumentError(`Either pipe_code, the method's tree (directory, contents, tree or files), a method_ref, a hosted method_id or a server-specific extension arg (extra) must be provided to ${call}()
|
|
3648
|
+
throw new RequestArgumentError(`Either pipe_code, the method's tree (directory, contents, tree or files), a method_ref, a hosted method_id or a server-specific extension arg (extra) must be provided to ${call}().`, { problem: argumentInvalidProblem("pipe_code", "nothing_to_run") });
|
|
3520
3649
|
}
|
|
3521
3650
|
assertMethodRefPairsWithNothing(options, treeForms);
|
|
3522
3651
|
assertLinkageMethodIdIsBare(options, treeForms);
|
|
@@ -3676,7 +3805,7 @@ function buildExtensions(extra) {
|
|
|
3676
3805
|
const snapshot = { ...extra };
|
|
3677
3806
|
const reserved = Object.keys(snapshot).filter((key) => RESERVED_EXTRA_KEYS.has(key));
|
|
3678
3807
|
if (reserved.length > 0) {
|
|
3679
|
-
throw new RequestArgumentError(`extra carries reserved request args [${reserved.sort().join(", ")}] — pass them as named options instead
|
|
3808
|
+
throw new RequestArgumentError(`extra carries reserved request args [${reserved.sort().join(", ")}] — pass them as named options instead.`, { problem: argumentInvalidProblem("extra", "reserved_argument") });
|
|
3680
3809
|
}
|
|
3681
3810
|
const result = {};
|
|
3682
3811
|
for (const [key, value] of Object.entries(snapshot)) {
|
|
@@ -3758,7 +3887,7 @@ function carriesExtraBundle(extensions) {
|
|
|
3758
3887
|
}
|
|
3759
3888
|
/** The refusal of a `directory` that is not a path, `call` naming the caller's method. */
|
|
3760
3889
|
function directoryRefusal(call) {
|
|
3761
|
-
return new RequestArgumentError(`${call}() got a directory that is not a path: give the method's directory as a string
|
|
3890
|
+
return new RequestArgumentError(`${call}() got a directory that is not a path: give the method's directory as a string.`, { problem: argumentInvalidProblem("directory", "not_a_string") });
|
|
3762
3891
|
}
|
|
3763
3892
|
/**
|
|
3764
3893
|
* `value` as a listed tree, or the refusal of one that is not, `call` naming the caller's method:
|
|
@@ -3769,7 +3898,7 @@ function checkedTreeSource(call, value) {
|
|
|
3769
3898
|
const fault = treeShapeFault(value);
|
|
3770
3899
|
if (fault !== undefined) {
|
|
3771
3900
|
throw new RequestArgumentError(`${call}() got a tree that is not a listed method tree — its files, its digest and a ` +
|
|
3772
|
-
`readFile — such as the MethodTree listMethodTree returns; got ${fault}
|
|
3901
|
+
`readFile — such as the MethodTree listMethodTree returns; got ${fault}.`, { problem: argumentInvalidProblem("tree", "not_a_tree") });
|
|
3773
3902
|
}
|
|
3774
3903
|
return value;
|
|
3775
3904
|
}
|
|
@@ -3797,7 +3926,7 @@ async function inlineWithin(tree, call, sending, maxInlineBytes, read, signal) {
|
|
|
3797
3926
|
function inlineLimitRefusal(call, sending, measured, weight, limit) {
|
|
3798
3927
|
return new RequestArgumentError(`${call}() cannot send the method's tree: the API keeps no blob store, so the tree travels ` +
|
|
3799
3928
|
`inline, and ${measured} ${weight} bytes, past the ${limit} bytes it takes inline (its ` +
|
|
3800
|
-
`max_inline_bytes), so ${sending.unsent}
|
|
3929
|
+
`max_inline_bytes), so ${sending.unsent}.`, { problem: argumentInvalidProblem("files", "past_inline_limit") });
|
|
3801
3930
|
}
|
|
3802
3931
|
/**
|
|
3803
3932
|
* Copy the Pipelex API's own run arguments onto the wire body. Named options,
|
|
@@ -3828,9 +3957,9 @@ function buildHostedRunExtensions(options) {
|
|
|
3828
3957
|
* The forms of the method's tree a protocol route (a run, `validate`) was given, checked as the
|
|
3829
3958
|
* MTHDS standard checks them: its `assertOneMethodSource` on `files` and `tree` (both at once, or
|
|
3830
3959
|
* either holding no file, refused whatever else the request carries), its refusal rethrown as a
|
|
3831
|
-
* `RequestArgumentError` with the same message
|
|
3832
|
-
*
|
|
3833
|
-
* the same way. A `null` member and an empty `directory` are absent.
|
|
3960
|
+
* `RequestArgumentError` with the same message, the standard's error as `cause` and its problem
|
|
3961
|
+
* (`client.argument_invalid`, naming the argument), like every other argument the client refuses;
|
|
3962
|
+
* and `contents` holding no file refused the same way. A `null` member and an empty `directory` are absent.
|
|
3834
3963
|
*/
|
|
3835
3964
|
function protocolTreeForms(call, options) {
|
|
3836
3965
|
// A `tree` the standard's check would read as a tree is checked as one first, so a value that
|
|
@@ -3841,8 +3970,8 @@ function protocolTreeForms(call, options) {
|
|
|
3841
3970
|
assertOneMethodSource({ files: options.files ?? null, tree: options.tree ?? null });
|
|
3842
3971
|
}
|
|
3843
3972
|
catch (err) {
|
|
3844
|
-
if (err instanceof
|
|
3845
|
-
throw new RequestArgumentError(err.message, { cause: err });
|
|
3973
|
+
if (err instanceof ProblemError) {
|
|
3974
|
+
throw new RequestArgumentError(err.message, { cause: err, problem: err.problem });
|
|
3846
3975
|
}
|
|
3847
3976
|
throw err;
|
|
3848
3977
|
}
|
|
@@ -3898,11 +4027,11 @@ function assertMethodRefPairsWithNothing(options, treeForms) {
|
|
|
3898
4027
|
return;
|
|
3899
4028
|
if (treeForms.length > 0) {
|
|
3900
4029
|
throw new RequestArgumentError(`method_ref and the method's tree (${treeForms.join(", ")}) are mutually exclusive: a ` +
|
|
3901
|
-
"published method's tree is its package's own. Send one or the other.");
|
|
4030
|
+
"published method's tree is its package's own. Send one or the other.", { problem: argumentInvalidProblem("method_ref", "mutually_exclusive") });
|
|
3902
4031
|
}
|
|
3903
4032
|
if (nonEmptyString(options.method_id) !== undefined) {
|
|
3904
4033
|
throw new RequestArgumentError("method_ref and method_id are mutually exclusive: an address run carries its own provenance " +
|
|
3905
|
-
"and takes no run-history linkage id. Send exactly one method selector.");
|
|
4034
|
+
"and takes no run-history linkage id. Send exactly one method selector.", { problem: argumentInvalidProblem("method_ref", "mutually_exclusive") });
|
|
3906
4035
|
}
|
|
3907
4036
|
}
|
|
3908
4037
|
/**
|
|
@@ -3924,7 +4053,7 @@ function assertLinkageMethodIdIsBare(options, treeForms) {
|
|
|
3924
4053
|
throw new RequestArgumentError(`method_id "${methodId}" beside the method's tree is run-history linkage and must be a bare ` +
|
|
3925
4054
|
"catalog id: the tree is what runs, so a version suffix would claim a version that did " +
|
|
3926
4055
|
"not. Send the bare id (parseMethodSelector(...).method_id), or drop the tree to run the " +
|
|
3927
|
-
"version the selector names.");
|
|
4056
|
+
"version the selector names.", { problem: argumentInvalidProblem("method_id", "version_suffix_beside_inline_source") });
|
|
3928
4057
|
}
|
|
3929
4058
|
function nonEmptyString(value) {
|
|
3930
4059
|
return value != null && value.length > 0 ? value : undefined;
|
|
@@ -3959,24 +4088,27 @@ function idempotencyKeyOf(options, request = "start the run") {
|
|
|
3959
4088
|
const fault = idempotencyKeyFault(key);
|
|
3960
4089
|
if (fault !== undefined) {
|
|
3961
4090
|
throw new RequestArgumentError(`The idempotency key must be 1 to ${MAX_IDEMPOTENCY_KEY_LENGTH} visible ASCII characters ` +
|
|
3962
|
-
`(\`!\` to \`~\`), and this one ${fault}. Leave it out to ${request} without one
|
|
4091
|
+
`(\`!\` to \`~\`), and this one ${fault.words}. Leave it out to ${request} without one.`, { problem: argumentInvalidProblem("idempotency_key", fault.reason) });
|
|
3963
4092
|
}
|
|
3964
4093
|
return key;
|
|
3965
4094
|
}
|
|
3966
4095
|
/**
|
|
3967
|
-
* What is wrong with an idempotency key, or `undefined` when it is a valid one
|
|
3968
|
-
*
|
|
3969
|
-
*
|
|
4096
|
+
* What is wrong with an idempotency key, or `undefined` when it is a valid one: in words for the
|
|
4097
|
+
* message, and as the problem's reason. The characters are checked before the length, which then
|
|
4098
|
+
* counts ASCII characters alone, so the two SDKs, which count a character outside ASCII
|
|
4099
|
+
* differently, word every refusal alike.
|
|
3970
4100
|
*/
|
|
3971
4101
|
function idempotencyKeyFault(key) {
|
|
3972
4102
|
if (typeof key !== "string")
|
|
3973
|
-
return "is not a string";
|
|
4103
|
+
return { words: "is not a string", reason: "not_a_string" };
|
|
3974
4104
|
if (key.length === 0)
|
|
3975
|
-
return "is empty";
|
|
3976
|
-
if (!VISIBLE_ASCII.test(key))
|
|
3977
|
-
return "holds a character outside visible ASCII";
|
|
3978
|
-
|
|
3979
|
-
|
|
4105
|
+
return { words: "is empty", reason: "empty" };
|
|
4106
|
+
if (!VISIBLE_ASCII.test(key)) {
|
|
4107
|
+
return { words: "holds a character outside visible ASCII", reason: "not_visible_ascii" };
|
|
4108
|
+
}
|
|
4109
|
+
if (key.length > MAX_IDEMPOTENCY_KEY_LENGTH) {
|
|
4110
|
+
return { words: `is ${key.length} characters long`, reason: "too_long" };
|
|
4111
|
+
}
|
|
3980
4112
|
return undefined;
|
|
3981
4113
|
}
|
|
3982
4114
|
function withValidateMarkdownRender(render) {
|
|
@@ -3993,28 +4125,32 @@ const GATEWAY_TIMEOUT_THRESHOLD_MS = 28_000;
|
|
|
3993
4125
|
* Whether a request that failed `elapsedMs` after it was sent was cut off by the hosted gateway's
|
|
3994
4126
|
* ~30-second limit on a request it waits on, rather than refused.
|
|
3995
4127
|
*
|
|
3996
|
-
* It is when, after at least ~28 seconds, the failure is a `503` or `504` answer
|
|
3997
|
-
* own time limit (`ApiUnreachableError` with the code
|
|
3998
|
-
* saying the request was not handled,
|
|
3999
|
-
*
|
|
4128
|
+
* It is when, after at least ~28 seconds, the failure is a `503` or `504` answer whose body is no
|
|
4129
|
+
* problem document, or the client's own time limit (`ApiUnreachableError` with the code
|
|
4130
|
+
* `ABORT_TIMEOUT`). A fast `503` is the API saying the request was not handled, a problem document
|
|
4131
|
+
* is the API's own answer however long it took, and any other unreachable host is never the
|
|
4132
|
+
* gateway's cut-off. The fetch API cannot say whether `ABORT_TIMEOUT` ran out before
|
|
4000
4133
|
* the connection was made, so it is read as the gateway's; with Node's own dispatcher it never
|
|
4001
4134
|
* does past the threshold, since undici gives up a connection that does not come after ten
|
|
4002
4135
|
* seconds (`UND_ERR_CONNECT_TIMEOUT`), the TLS handshake included. The Python SDK's
|
|
4003
4136
|
* `is_gateway_cut_off` reads the same failures, its `ABORT_TIMEOUT` being httpx's read or write
|
|
4004
4137
|
* timeout.
|
|
4005
4138
|
*
|
|
4006
|
-
* The blocking `execute` turns such a failure into a `PipelineExecuteTimeoutError
|
|
4007
|
-
* failed `start` or `execute` may have started a run needs no timing: the
|
|
4008
|
-
*
|
|
4009
|
-
*
|
|
4139
|
+
* The blocking `execute` turns such a failure into a `PipelineExecuteTimeoutError`, for its
|
|
4140
|
+
* message. Whether a failed `start` or `execute` may have started a run needs no timing: the
|
|
4141
|
+
* error's problem says it, its `retry` being `outcome_unknown` for every answer from the edge
|
|
4142
|
+
* that is no problem document past a `5xx` but a `501`, however quickly it came back, while a
|
|
4143
|
+
* `503` problem document from the platform says as received whether it started one.
|
|
4010
4144
|
*/
|
|
4011
4145
|
export function isGatewayCutOff(error, elapsedMs) {
|
|
4012
4146
|
if (elapsedMs < GATEWAY_TIMEOUT_THRESHOLD_MS)
|
|
4013
4147
|
return false;
|
|
4014
|
-
if (error instanceof ApiResponseError)
|
|
4015
|
-
return error.status === 503 || error.status === 504
|
|
4148
|
+
if (error instanceof ApiResponseError) {
|
|
4149
|
+
return ((error.status === 503 || error.status === 504) &&
|
|
4150
|
+
readProblemBody(error.responseBody) === undefined);
|
|
4151
|
+
}
|
|
4016
4152
|
if (error instanceof ApiUnreachableError)
|
|
4017
|
-
return error.
|
|
4153
|
+
return error.networkCode === ABORT_TIMEOUT;
|
|
4018
4154
|
return false;
|
|
4019
4155
|
}
|
|
4020
4156
|
/**
|
|
@@ -4025,12 +4161,12 @@ export function isGatewayCutOff(error, elapsedMs) {
|
|
|
4025
4161
|
*/
|
|
4026
4162
|
const UNDECODABLE_BODY_CODE = /^(Z_|ERR__ERROR_|ZSTD_error_)/;
|
|
4027
4163
|
/**
|
|
4028
|
-
* Whether
|
|
4029
|
-
*
|
|
4164
|
+
* Whether the network code of a failure met while a body was read says the body arrived and could
|
|
4165
|
+
* not be decoded, rather than that it never arrived. The Python SDK's twin is httpx's
|
|
4030
4166
|
* `DecodingError`.
|
|
4031
4167
|
*/
|
|
4032
|
-
function isUndecodableBody(
|
|
4033
|
-
return
|
|
4168
|
+
function isUndecodableBody(code) {
|
|
4169
|
+
return code !== undefined && UNDECODABLE_BODY_CODE.test(code);
|
|
4034
4170
|
}
|
|
4035
4171
|
/** A SHA-256 as the platform names a blob: 64 lowercase hexadecimal digits. */
|
|
4036
4172
|
const SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
@@ -4095,21 +4231,23 @@ function extractNetworkErrorCode(err) {
|
|
|
4095
4231
|
return undefined;
|
|
4096
4232
|
}
|
|
4097
4233
|
/**
|
|
4098
|
-
* Whether a 404
|
|
4099
|
-
*
|
|
4100
|
-
* a
|
|
4101
|
-
*
|
|
4234
|
+
* Whether a 404 says that no route serves the path (no run store deployed), rather than that a
|
|
4235
|
+
* route answered it on purpose, such as a run or a method that does not exist. A route that
|
|
4236
|
+
* answers a 404 of its own names its failure by a `code`, whether in a problem document
|
|
4237
|
+
* (`run.not_found`, or a runner's `method.not_found` for a `method_ref` it cannot find) or in
|
|
4238
|
+
* an older native body the platform still answers (`run_not_found`, `not_found`), and one defect
|
|
4239
|
+
* elsewhere in the document does not make the route missing. So the 404 is a missing route when
|
|
4240
|
+
* its body is not a JSON object naming a non-empty string `code`, as a bare runner's framework
|
|
4241
|
+
* default `{"detail": "Not Found"}` and an empty or non-JSON body are not, or when that code is
|
|
4242
|
+
* `request.not_found`, the problem a runner answers for a path no route serves (pipelex-api 0.90
|
|
4243
|
+
* and later). The Python SDK's `_is_missing_route_404` reads the same rule.
|
|
4102
4244
|
*/
|
|
4103
4245
|
function isMissingRoute404(body) {
|
|
4104
|
-
if (!body)
|
|
4105
|
-
return true;
|
|
4106
4246
|
const reading = parseJsonBody(body);
|
|
4107
|
-
if (!reading.ok)
|
|
4247
|
+
if (!reading.ok || !isPlainObject(reading.value))
|
|
4108
4248
|
return true;
|
|
4109
|
-
const
|
|
4110
|
-
|
|
4111
|
-
return true;
|
|
4112
|
-
return !("code" in parsed);
|
|
4249
|
+
const code = reading.value["code"];
|
|
4250
|
+
return typeof code !== "string" || code === "" || code === "request.not_found";
|
|
4113
4251
|
}
|
|
4114
4252
|
/**
|
|
4115
4253
|
* Whether a 2xx answer to `execute` is the completed run the route returns, as far as the SDK reads
|
|
@@ -4138,43 +4276,72 @@ function parseRetryAfter(headers) {
|
|
|
4138
4276
|
const seconds = Number(raw);
|
|
4139
4277
|
return Number.isFinite(seconds) && seconds >= 0 ? seconds : null;
|
|
4140
4278
|
}
|
|
4141
|
-
/** The response header the platform and the runner stamp the request's correlation id on. */
|
|
4142
|
-
const REQUEST_ID_HEADER = "x-request-id";
|
|
4143
4279
|
/**
|
|
4144
|
-
*
|
|
4280
|
+
* Read the results read's `409` answer. The platform answers it with a problem document:
|
|
4281
|
+
*
|
|
4282
|
+
* - `run.failed`: the run ended without success, and the answer is the failed arm, its `problem`
|
|
4283
|
+
* that document as received, its `status` the terminal status its `run_status` parameter names
|
|
4284
|
+
* (`FAILED` when it names none the SDK knows), its `message` the document's `detail`, and its
|
|
4285
|
+
* `error` the run's own problem, which the document nests as `run_error`, or `null` when it nests
|
|
4286
|
+
* none that is a problem document;
|
|
4287
|
+
* - `run.not_finished`: the run is still going, and the answer is the running arm, waiting the
|
|
4288
|
+
* document's `retry_after`, rounded up to whole seconds, else the `Retry-After` header's (or the
|
|
4289
|
+
* default) before the next read.
|
|
4145
4290
|
*
|
|
4146
|
-
*
|
|
4147
|
-
*
|
|
4148
|
-
* `run_status`, the run's terminal status — named so because a problem's own `status` is the
|
|
4149
|
-
* HTTP status — and `error`, the run's stored error report or `null`. The status is read from
|
|
4150
|
-
* `run_status`. A platform that predates the member sends only `detail`, whose leading
|
|
4151
|
-
* `Run finished with status <STATUS>` the platform keeps for exactly this reader, so the status
|
|
4152
|
-
* word is recovered from it then; a `409` that yields no known status either way (the one this
|
|
4153
|
-
* route answers for a stored result it refuses to read) reads as `FAILED`, and its `detail`
|
|
4154
|
-
* still says what happened. The report is checked field by field by `readRunErrorReport`: an
|
|
4155
|
-
* object is taken as the report, each named field kept when it has its declared type, and
|
|
4156
|
-
* anything that is not an object reads as no report.
|
|
4291
|
+
* Any other `409`, a problem document of another code or a body that is none, is no answer about
|
|
4292
|
+
* the run's end, so it reads as `undefined` and the caller throws its `ApiResponseError`.
|
|
4157
4293
|
*/
|
|
4158
|
-
function
|
|
4159
|
-
const
|
|
4160
|
-
|
|
4294
|
+
function runResultConflict(runId, res) {
|
|
4295
|
+
const problem = readProblemBody(res.body);
|
|
4296
|
+
if (problem === undefined)
|
|
4297
|
+
return undefined;
|
|
4298
|
+
if (problem.code === "run.not_finished") {
|
|
4299
|
+
// The run's own delay, in whole seconds as the running arm counts them, else the header's.
|
|
4300
|
+
const retryAfter = problem.retry_after !== undefined
|
|
4301
|
+
? Math.ceil(problem.retry_after)
|
|
4302
|
+
: parseRetryAfter(res.headers);
|
|
4303
|
+
return {
|
|
4304
|
+
state: "running",
|
|
4305
|
+
pipeline_run_id: runId,
|
|
4306
|
+
retry_after_seconds: retryAfter ?? DEFAULT_DEGRADED_RETRY_SECONDS,
|
|
4307
|
+
};
|
|
4308
|
+
}
|
|
4309
|
+
if (problem.code !== "run.failed")
|
|
4310
|
+
return undefined;
|
|
4311
|
+
const nested = problem.run_error;
|
|
4161
4312
|
return {
|
|
4162
4313
|
state: "failed",
|
|
4163
4314
|
pipeline_run_id: runId,
|
|
4164
|
-
status: knownRunStatus(
|
|
4165
|
-
message,
|
|
4166
|
-
|
|
4315
|
+
status: knownRunStatus(problem.params?.run_status) ?? "FAILED",
|
|
4316
|
+
message: problem.detail,
|
|
4317
|
+
problem: problem.status === undefined ? { ...problem, status: res.status } : problem,
|
|
4318
|
+
error: isProblem(nested) ? nested : null,
|
|
4167
4319
|
};
|
|
4168
4320
|
}
|
|
4169
4321
|
/**
|
|
4170
|
-
* A
|
|
4171
|
-
*
|
|
4172
|
-
* that is not an object is returned as it came.
|
|
4322
|
+
* A verdict with its problem items read member by member (`readProblemItems`): the `errors` of an
|
|
4323
|
+
* invalid arm and the `warnings` of a valid one, each only when the answer carries the key. An
|
|
4324
|
+
* answer that is not an object is returned as it came.
|
|
4325
|
+
*/
|
|
4326
|
+
function withVerdictItems(answer) {
|
|
4327
|
+
if (!isPlainObject(answer))
|
|
4328
|
+
return answer;
|
|
4329
|
+
const read = { ...answer };
|
|
4330
|
+
if (answer.is_valid === false)
|
|
4331
|
+
read.errors = readProblemItems(answer.errors);
|
|
4332
|
+
else if (Object.hasOwn(answer, "warnings"))
|
|
4333
|
+
read.warnings = readProblemItems(answer.warnings);
|
|
4334
|
+
return read;
|
|
4335
|
+
}
|
|
4336
|
+
/**
|
|
4337
|
+
* A run record with its `error` kept only when it is a problem document, and read as `null`
|
|
4338
|
+
* otherwise. Only when the record carries the `error` key, so a server that does not serve it
|
|
4339
|
+
* still leaves it absent; a record that is not an object is returned as it came.
|
|
4173
4340
|
*/
|
|
4174
|
-
function
|
|
4341
|
+
function withCheckedProblem(record) {
|
|
4175
4342
|
if (!isPlainObject(record) || !Object.hasOwn(record, "error"))
|
|
4176
4343
|
return record;
|
|
4177
|
-
return { ...record, error:
|
|
4344
|
+
return { ...record, error: isProblem(record.error) ? record.error : null };
|
|
4178
4345
|
}
|
|
4179
4346
|
/**
|
|
4180
4347
|
* A status read with its `degradation` checked by `readRunDegradation`. Only when the read carries
|
|
@@ -4190,110 +4357,4 @@ function knownRunStatus(value) {
|
|
|
4190
4357
|
? value
|
|
4191
4358
|
: undefined;
|
|
4192
4359
|
}
|
|
4193
|
-
/** The status word of a `detail` reading `Run finished with status <STATUS>…`, for a platform without `run_status`. */
|
|
4194
|
-
function statusFromDetail(detail) {
|
|
4195
|
-
return knownRunStatus(/status\s+([A-Z_]+)/.exec(detail)?.[1]);
|
|
4196
|
-
}
|
|
4197
|
-
/**
|
|
4198
|
-
* Extract the members of an error body.
|
|
4199
|
-
*
|
|
4200
|
-
* The API serializes errors as RFC 9457 problem documents — the platform's (`type`, `title`,
|
|
4201
|
-
* `status`, `code`, `detail`, `instance`, `request_id`, `errors[]`, plus an extension member
|
|
4202
|
-
* such as a failed run's `run_status` and `error`) and the runner's (the same standard slots
|
|
4203
|
-
* plus `error_type`, `error_domain`, `error_category`, `retryable`, `user_action`, `model`,
|
|
4204
|
-
* `provider`, `provider_metadata`, `validation_errors`, `migration`) — and, on older routes, as
|
|
4205
|
-
* `{"detail": {"error_type": ..., "message": ...}}` (HTTPException with dict detail). Both
|
|
4206
|
-
* shapes are handled, with top-level `error_type` / `message` fallbacks. Falls through to empty
|
|
4207
|
-
* on a non-JSON or non-object body.
|
|
4208
|
-
*
|
|
4209
|
-
* Each typed member is kept only when it has the type the problem document gives it, so a
|
|
4210
|
-
* malformed member reads as absent rather than as a wrong value, and the nested members
|
|
4211
|
-
* (`provider_metadata`, `migration`, `validation_errors`, `errors`) are checked field by field as a
|
|
4212
|
-
* stored report's are; `document` keeps the decoded object whole, members named or not.
|
|
4213
|
-
*/
|
|
4214
|
-
function parseErrorBody(body) {
|
|
4215
|
-
const empty = {
|
|
4216
|
-
errorType: undefined,
|
|
4217
|
-
serverMessage: undefined,
|
|
4218
|
-
validationErrors: undefined,
|
|
4219
|
-
code: undefined,
|
|
4220
|
-
details: {},
|
|
4221
|
-
document: undefined,
|
|
4222
|
-
};
|
|
4223
|
-
if (!body)
|
|
4224
|
-
return empty;
|
|
4225
|
-
// Read by the protocol's rules, as every body is: JSON nested deeper than `MAX_BODY_NESTING`, or
|
|
4226
|
-
// holding a number past the range of a double, carries no member, as `pipelex-sdk` reads it.
|
|
4227
|
-
const reading = parseJsonBody(body);
|
|
4228
|
-
if (!reading.ok || !isPlainObject(reading.value)) {
|
|
4229
|
-
return empty;
|
|
4230
|
-
}
|
|
4231
|
-
const root = reading.value;
|
|
4232
|
-
const detail = root.detail;
|
|
4233
|
-
let errorType;
|
|
4234
|
-
let serverMessage;
|
|
4235
|
-
if (detail && typeof detail === "object") {
|
|
4236
|
-
const d = detail;
|
|
4237
|
-
if (typeof d.error_type === "string")
|
|
4238
|
-
errorType = d.error_type;
|
|
4239
|
-
if (typeof d.message === "string")
|
|
4240
|
-
serverMessage = d.message;
|
|
4241
|
-
}
|
|
4242
|
-
else if (typeof detail === "string") {
|
|
4243
|
-
serverMessage = detail;
|
|
4244
|
-
}
|
|
4245
|
-
if (errorType === undefined && typeof root.error_type === "string")
|
|
4246
|
-
errorType = root.error_type;
|
|
4247
|
-
if (serverMessage === undefined && typeof root.message === "string")
|
|
4248
|
-
serverMessage = root.message;
|
|
4249
|
-
// `validation_errors` rides the problem envelope as a top-level array (the
|
|
4250
|
-
// VERBOSE projection of `ErrorReport.validation_errors`, retained under STRICT
|
|
4251
|
-
// too — it describes the caller's own bundle, not server internals). Each item is
|
|
4252
|
-
// kept when it is an object with a string `category` and `message`, as on a stored report.
|
|
4253
|
-
const validationErrors = Array.isArray(root.validation_errors)
|
|
4254
|
-
? root.validation_errors.filter(isValidationItem)
|
|
4255
|
-
: undefined;
|
|
4256
|
-
// The platform's closed native code (`conflict`, `not_found`, …), one-to-one with `type`.
|
|
4257
|
-
const code = typeof root.code === "string" ? root.code : undefined;
|
|
4258
|
-
const details = {
|
|
4259
|
-
type: stringMember(root.type),
|
|
4260
|
-
title: stringMember(root.title),
|
|
4261
|
-
instance: stringMember(root.instance),
|
|
4262
|
-
requestId: stringMember(root.request_id),
|
|
4263
|
-
errorDomain: stringMember(root.error_domain),
|
|
4264
|
-
errorCategory: stringMember(root.error_category),
|
|
4265
|
-
retryable: typeof root.retryable === "boolean" ? root.retryable : undefined,
|
|
4266
|
-
userAction: parseUserAction(root.user_action),
|
|
4267
|
-
model: stringMember(root.model),
|
|
4268
|
-
provider: stringMember(root.provider),
|
|
4269
|
-
// The nested members are checked one level down by the readers a stored report goes
|
|
4270
|
-
// through, so a misfit field reads as absent here exactly as it does there.
|
|
4271
|
-
providerMetadata: isPlainObject(root.provider_metadata)
|
|
4272
|
-
? readProviderMetadata(root.provider_metadata)
|
|
4273
|
-
: undefined,
|
|
4274
|
-
migration: isPlainObject(root.migration) ? readMigration(root.migration) : undefined,
|
|
4275
|
-
errors: Array.isArray(root.errors)
|
|
4276
|
-
? root.errors.filter(isPlainObject).map(readFieldError)
|
|
4277
|
-
: undefined,
|
|
4278
|
-
};
|
|
4279
|
-
return { errorType, serverMessage, validationErrors, code, details, document: root };
|
|
4280
|
-
}
|
|
4281
|
-
/** A problem member kept only when it is a non-empty string. */
|
|
4282
|
-
function stringMember(value) {
|
|
4283
|
-
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
4284
|
-
}
|
|
4285
|
-
/** A `user_action` member is kept only whole: an object with a string `kind` and a non-empty `detail`. */
|
|
4286
|
-
function parseUserAction(value) {
|
|
4287
|
-
if (!isPlainObject(value))
|
|
4288
|
-
return undefined;
|
|
4289
|
-
const { kind, detail } = value;
|
|
4290
|
-
if (typeof kind !== "string" || typeof detail !== "string" || detail.length === 0) {
|
|
4291
|
-
return undefined;
|
|
4292
|
-
}
|
|
4293
|
-
return { kind, detail };
|
|
4294
|
-
}
|
|
4295
|
-
function nonEmptyHeader(headers, name) {
|
|
4296
|
-
const value = headers.get(name)?.trim();
|
|
4297
|
-
return value ? value : undefined;
|
|
4298
|
-
}
|
|
4299
4360
|
//# sourceMappingURL=client.js.map
|