deepline 0.3.257 → 0.3.259
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/dist/bundling-sources/sdk/src/api-error.ts +45 -0
- package/dist/bundling-sources/sdk/src/client.ts +98 -459
- package/dist/bundling-sources/sdk/src/http.ts +1 -34
- package/dist/bundling-sources/sdk/src/index.ts +8 -0
- package/dist/bundling-sources/sdk/src/plays/csv-data-input.ts +139 -0
- package/dist/bundling-sources/sdk/src/plays/run-request.ts +855 -0
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/runs/overview.ts +45 -2
- package/dist/bundling-sources/sdk/src/types.ts +5 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +8 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +42 -10
- package/dist/bundling-sources/shared_libs/play-runtime/launch-identity.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/launch-source-policy.ts +23 -0
- package/dist/bundling-sources/shared_libs/play-runtime/live-state-contract.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-admission-outcome.ts +23 -5
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/projection/gateway-adapter.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/run-execution-state.ts +845 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +106 -6
- package/dist/bundling-sources/shared_libs/play-runtime/run-observation.ts +440 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +35 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +66 -7
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +0 -29
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-row-transition.ts +47 -2
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +6 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +475 -36
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +78 -11
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +550 -42
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +181 -8
- package/dist/bundling-sources/shared_libs/play-runtime/step-lifecycle-tracker.ts +0 -18
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +80 -7
- package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +37 -2
- package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +148 -18
- package/dist/bundling-sources/shared_libs/plays/cascade-falloff.ts +86 -0
- package/dist/bundling-sources/shared_libs/plays/docflow-unreached-reason.ts +611 -0
- package/dist/bundling-sources/shared_libs/plays/docflow.ts +21 -0
- package/dist/bundling-sources/shared_libs/plays/play-exports.ts +28 -5
- package/dist/bundling-sources/shared_libs/plays/play-grep-search.ts +40 -2
- package/dist/bundling-sources/shared_libs/plays/settings-contract.ts +92 -10
- package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +5 -0
- package/dist/bundling-sources/shared_libs/plays/trigger-config-contract.ts +222 -0
- package/dist/bundling-sources/shared_libs/plays/trigger-config-route.ts +15 -0
- package/dist/cli/index.js +4957 -771
- package/dist/cli/index.mjs +4957 -771
- package/dist/client-B5XbKrrs.d.mts +6564 -0
- package/dist/client-C9Y49yya.d.ts +6564 -0
- package/dist/{compiler-manifest-C4mdRJOP.d.mts → compiler-manifest-CUuh8C3b.d.mts} +14 -1
- package/dist/{compiler-manifest-C4mdRJOP.d.ts → compiler-manifest-CUuh8C3b.d.ts} +14 -1
- package/dist/index.d.mts +7 -6275
- package/dist/index.d.ts +7 -6275
- package/dist/index.js +360 -326
- package/dist/index.mjs +360 -326
- package/dist/install-integrity.json +21 -2
- package/dist/plays/bundle-play-file.d.mts +2 -2
- package/dist/plays/bundle-play-file.d.ts +2 -2
- package/dist/plays/bundle-play-file.mjs +117 -18
- package/dist/plays/csv-data-input.d.mts +37 -0
- package/dist/plays/csv-data-input.d.ts +37 -0
- package/dist/plays/csv-data-input.js +3135 -0
- package/dist/plays/csv-data-input.mjs +3108 -0
- package/dist/plays/run-request.d.mts +6 -0
- package/dist/plays/run-request.d.ts +6 -0
- package/dist/plays/run-request.js +664 -0
- package/dist/plays/run-request.mjs +614 -0
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +1 -1
- package/dist/release.mjs +1 -1
- package/package.json +12 -1
|
@@ -0,0 +1,855 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one client-side construction of a Play run start.
|
|
3
|
+
*
|
|
4
|
+
* `deepline plays run <name>` (CLI), `DeeplineClient.startPlayRun` (SDK), and
|
|
5
|
+
* the dashboard's "Run in UI" form all start a run through this module: the
|
|
6
|
+
* same file-input bindings, the same staged-file upload
|
|
7
|
+
* (`/api/v2/plays/files/stage/mint`, direct PUT, multipart fallback), the same
|
|
8
|
+
* `POST /api/v2/plays/run` body, and the same start-response normalization.
|
|
9
|
+
* The strict CSV check on the `csv` data input lives in `./csv-data-input`. Only the transport differs: the SDK's
|
|
10
|
+
* `HttpClient` carries an API key, the dashboard's same-origin transport
|
|
11
|
+
* carries the browser session cookie, and one route handler accepts both.
|
|
12
|
+
*
|
|
13
|
+
* Browser-safe by construction: no `node:` imports, no `process`, no global
|
|
14
|
+
* `Buffer`. Environment-derived options (eval integration mode, preview
|
|
15
|
+
* runtime selection, test policy overrides) are resolved by the caller and
|
|
16
|
+
* passed in as explicit request fields.
|
|
17
|
+
*/
|
|
18
|
+
import { DeeplineError } from '../../../shared_libs/plays/tool-execution-error';
|
|
19
|
+
import { isTerminalPlayRunLifecycleStatus } from '../../../shared_libs/play-runtime/run-lifecycle-policy.js';
|
|
20
|
+
import { apiErrorCodeFromResponse, apiErrorMessage } from '../api-error';
|
|
21
|
+
import type {
|
|
22
|
+
PlayRunPackage,
|
|
23
|
+
PlayRunStart,
|
|
24
|
+
StartPlayRunRequest,
|
|
25
|
+
} from '../types.js';
|
|
26
|
+
import type { PlayStagedFileRef } from './local-file-discovery.js';
|
|
27
|
+
|
|
28
|
+
export { DeeplineError };
|
|
29
|
+
|
|
30
|
+
/** The single run-start route. CLI, SDK, API, and dashboard all POST here. */
|
|
31
|
+
export const PLAY_RUN_START_PATH = '/api/v2/plays/run';
|
|
32
|
+
export const PLAY_RUN_START_STREAM_PATH = `${PLAY_RUN_START_PATH}?stream=true`;
|
|
33
|
+
export const PLAY_FILES_STAGE_PATH = '/api/v2/plays/files/stage';
|
|
34
|
+
export const PLAY_FILES_STAGE_MINT_PATH = `${PLAY_FILES_STAGE_PATH}/mint`;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The HTTP surface a run start needs. `HttpClient` satisfies it structurally;
|
|
38
|
+
* the dashboard implements it over same-origin `fetch` with session cookies.
|
|
39
|
+
* Implementations throw `DeeplineError` (with `statusCode`) for non-2xx
|
|
40
|
+
* responses so the staging fallbacks below classify failures identically.
|
|
41
|
+
*/
|
|
42
|
+
export interface PlayRunHttp {
|
|
43
|
+
post<T = unknown>(
|
|
44
|
+
path: string,
|
|
45
|
+
body: unknown,
|
|
46
|
+
headers?: Record<string, string>,
|
|
47
|
+
options?: { maxRetries?: number; exactUrlOnly?: boolean },
|
|
48
|
+
): Promise<T>;
|
|
49
|
+
postFormData<T = unknown>(
|
|
50
|
+
path: string,
|
|
51
|
+
formData: FormData | (() => FormData),
|
|
52
|
+
): Promise<T>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Run-start body and response ─────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A run-start request whose environment-derived fields are already resolved.
|
|
59
|
+
* `buildPlayRunStartBody` never reads the environment; the SDK client resolves
|
|
60
|
+
* `integrationMode`, `runtime`, and `testPolicyOverrides` before calling it.
|
|
61
|
+
*/
|
|
62
|
+
export type ResolvedStartPlayRunRequest = StartPlayRunRequest;
|
|
63
|
+
|
|
64
|
+
/** The exact JSON body `POST /api/v2/plays/run` receives. Key order is part of the contract. */
|
|
65
|
+
export function buildPlayRunStartBody(
|
|
66
|
+
request: ResolvedStartPlayRunRequest,
|
|
67
|
+
): Record<string, unknown> {
|
|
68
|
+
return {
|
|
69
|
+
...(request.name ? { name: request.name } : {}),
|
|
70
|
+
...(request.revisionId ? { revisionId: request.revisionId } : {}),
|
|
71
|
+
...(request.artifactStorageKey
|
|
72
|
+
? { artifactStorageKey: request.artifactStorageKey }
|
|
73
|
+
: {}),
|
|
74
|
+
...(request.sourceCode ? { sourceCode: request.sourceCode } : {}),
|
|
75
|
+
...(request.sourceFiles ? { sourceFiles: request.sourceFiles } : {}),
|
|
76
|
+
...(request.description ? { description: request.description } : {}),
|
|
77
|
+
...('staticPipeline' in request
|
|
78
|
+
? { staticPipeline: request.staticPipeline }
|
|
79
|
+
: {}),
|
|
80
|
+
...(request.artifactHash ? { artifactHash: request.artifactHash } : {}),
|
|
81
|
+
...(request.graphHash ? { graphHash: request.graphHash } : {}),
|
|
82
|
+
...(request.runtimeArtifact
|
|
83
|
+
? { runtimeArtifact: request.runtimeArtifact }
|
|
84
|
+
: {}),
|
|
85
|
+
...(request.compilerManifest
|
|
86
|
+
? { compilerManifest: request.compilerManifest }
|
|
87
|
+
: {}),
|
|
88
|
+
...(request.inputFileUpload
|
|
89
|
+
? { inputFileUpload: request.inputFileUpload }
|
|
90
|
+
: {}),
|
|
91
|
+
...(request.packagedFileUploads?.length
|
|
92
|
+
? { packagedFileUploads: request.packagedFileUploads }
|
|
93
|
+
: {}),
|
|
94
|
+
...(request.input ? { input: request.input } : {}),
|
|
95
|
+
...(request.inputFile ? { inputFile: request.inputFile } : {}),
|
|
96
|
+
...(request.packagedFiles?.length
|
|
97
|
+
? { packagedFiles: request.packagedFiles }
|
|
98
|
+
: {}),
|
|
99
|
+
...(request.force ? { force: true } : {}),
|
|
100
|
+
...(request.forceToolRefresh === true ? { forceToolRefresh: true } : {}),
|
|
101
|
+
...(typeof request.maxConcurrentExternalCalls === 'number'
|
|
102
|
+
? { maxConcurrentExternalCalls: request.maxConcurrentExternalCalls }
|
|
103
|
+
: {}),
|
|
104
|
+
...(typeof request.maxConcurrentRows === 'number'
|
|
105
|
+
? { maxConcurrentRows: request.maxConcurrentRows }
|
|
106
|
+
: {}),
|
|
107
|
+
...(typeof request.waitForCompletionMs === 'number'
|
|
108
|
+
? { waitForCompletionMs: request.waitForCompletionMs }
|
|
109
|
+
: {}),
|
|
110
|
+
// Profile selection is the API's job, not the caller's. The server
|
|
111
|
+
// defaults to absurd; callers normally omit this field.
|
|
112
|
+
...(request.profile ? { profile: request.profile } : {}),
|
|
113
|
+
...(request.integrationMode
|
|
114
|
+
? { integrationMode: request.integrationMode }
|
|
115
|
+
: {}),
|
|
116
|
+
...(request.fixtureBehavior
|
|
117
|
+
? { fixtureBehavior: request.fixtureBehavior }
|
|
118
|
+
: {}),
|
|
119
|
+
...(request.runtime ? { runtime: request.runtime } : {}),
|
|
120
|
+
...(request.testPolicyOverrides
|
|
121
|
+
? { testPolicyOverrides: request.testPolicyOverrides }
|
|
122
|
+
: {}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Start a run: one POST of {@link buildPlayRunStartBody} to
|
|
128
|
+
* {@link PLAY_RUN_START_PATH}, normalized to a {@link PlayRunStart}.
|
|
129
|
+
*
|
|
130
|
+
* Never retried: a start can reach the server even when its response is lost
|
|
131
|
+
* or times out, and retrying without an idempotency key can create a second
|
|
132
|
+
* root run, so callers resolve ambiguous failures explicitly.
|
|
133
|
+
*/
|
|
134
|
+
export async function startPlayRun(
|
|
135
|
+
http: PlayRunHttp,
|
|
136
|
+
request: ResolvedStartPlayRunRequest,
|
|
137
|
+
headers?: Record<string, string>,
|
|
138
|
+
): Promise<PlayRunStart> {
|
|
139
|
+
const response = await http.post<Record<string, unknown>>(
|
|
140
|
+
PLAY_RUN_START_PATH,
|
|
141
|
+
buildPlayRunStartBody(request),
|
|
142
|
+
headers,
|
|
143
|
+
{ maxRetries: 0, exactUrlOnly: true },
|
|
144
|
+
);
|
|
145
|
+
return normalizePlayRunStart(response);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function isPlayRunPackage(value: unknown): value is PlayRunPackage {
|
|
149
|
+
return Boolean(
|
|
150
|
+
value &&
|
|
151
|
+
typeof value === 'object' &&
|
|
152
|
+
!Array.isArray(value) &&
|
|
153
|
+
(value as Record<string, unknown>).kind === 'play_run' &&
|
|
154
|
+
(value as Record<string, unknown>).run &&
|
|
155
|
+
typeof (value as { run?: { id?: unknown } }).run?.id === 'string',
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Normalize any run-start response shape to `{ workflowId, … }`. */
|
|
160
|
+
export function normalizePlayRunStart(
|
|
161
|
+
raw: Record<string, unknown>,
|
|
162
|
+
): PlayRunStart {
|
|
163
|
+
const runPackage = isPlayRunPackage(raw)
|
|
164
|
+
? raw
|
|
165
|
+
: isPlayRunPackage(raw.package)
|
|
166
|
+
? raw.package
|
|
167
|
+
: null;
|
|
168
|
+
if (!runPackage) {
|
|
169
|
+
const workflowId =
|
|
170
|
+
typeof raw.workflowId === 'string' && raw.workflowId
|
|
171
|
+
? raw.workflowId
|
|
172
|
+
: typeof raw.runId === 'string'
|
|
173
|
+
? raw.runId
|
|
174
|
+
: '';
|
|
175
|
+
return {
|
|
176
|
+
...(raw as unknown as Omit<PlayRunStart, 'workflowId'>),
|
|
177
|
+
workflowId,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const status =
|
|
181
|
+
typeof runPackage.run.status === 'string'
|
|
182
|
+
? runPackage.run.status
|
|
183
|
+
: 'running';
|
|
184
|
+
return {
|
|
185
|
+
workflowId: runPackage.run.id,
|
|
186
|
+
name: runPackage.run.playName,
|
|
187
|
+
status,
|
|
188
|
+
...(runPackage.run.dashboardUrl
|
|
189
|
+
? { dashboardUrl: runPackage.run.dashboardUrl }
|
|
190
|
+
: {}),
|
|
191
|
+
...(isTerminalPlayRunLifecycleStatus(status)
|
|
192
|
+
? { finalStatus: runPackage }
|
|
193
|
+
: {}),
|
|
194
|
+
package: runPackage,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The run request for a Play started by name. `extra` carries caller options
|
|
200
|
+
* (CLI flags such as `--force`); the dashboard passes none.
|
|
201
|
+
*/
|
|
202
|
+
export function buildNamedPlayRunRequest(input: {
|
|
203
|
+
name: string;
|
|
204
|
+
revisionId?: string | null;
|
|
205
|
+
runtimeInput: Record<string, unknown>;
|
|
206
|
+
stagedFileInputs: StagedFileInputs;
|
|
207
|
+
extra?: Omit<
|
|
208
|
+
StartPlayRunRequest,
|
|
209
|
+
'name' | 'revisionId' | 'input' | 'inputFile' | 'packagedFiles'
|
|
210
|
+
>;
|
|
211
|
+
}): StartPlayRunRequest {
|
|
212
|
+
return {
|
|
213
|
+
name: input.name,
|
|
214
|
+
...(input.revisionId ? { revisionId: input.revisionId } : {}),
|
|
215
|
+
...(Object.keys(input.runtimeInput).length > 0
|
|
216
|
+
? { input: input.runtimeInput }
|
|
217
|
+
: {}),
|
|
218
|
+
...(input.stagedFileInputs.inputFile
|
|
219
|
+
? { inputFile: input.stagedFileInputs.inputFile }
|
|
220
|
+
: {}),
|
|
221
|
+
...(input.stagedFileInputs.packagedFiles.length
|
|
222
|
+
? { packagedFiles: input.stagedFileInputs.packagedFiles }
|
|
223
|
+
: {}),
|
|
224
|
+
...input.extra,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── File-input bindings ─────────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
/** A declared input whose value is a file the run stages before starting. */
|
|
231
|
+
export type FileInputBinding = { inputPath: string };
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The declared property name that carries the play's CSV row data (the row
|
|
235
|
+
* source). Only this key triggers strict CSV parsing; every other file input
|
|
236
|
+
* is staged without parsing its contents.
|
|
237
|
+
*/
|
|
238
|
+
export const CSV_DATA_INPUT_KEY = 'csv';
|
|
239
|
+
|
|
240
|
+
function schemaMetadata(
|
|
241
|
+
schema: Record<string, unknown> | null | undefined,
|
|
242
|
+
key: string,
|
|
243
|
+
): Record<string, unknown> | null {
|
|
244
|
+
if (!schema || typeof schema !== 'object' || Array.isArray(schema))
|
|
245
|
+
return null;
|
|
246
|
+
const value = schema[key];
|
|
247
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
248
|
+
? (value as Record<string, unknown>)
|
|
249
|
+
: null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function stringMetadata(
|
|
253
|
+
metadata: Record<string, unknown> | null | undefined,
|
|
254
|
+
key: string,
|
|
255
|
+
): string | null {
|
|
256
|
+
const value = metadata?.[key];
|
|
257
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function inputFieldFromCsvArg(csvArg: unknown): string | null {
|
|
261
|
+
if (typeof csvArg !== 'string') return null;
|
|
262
|
+
// The static pipeline preserves the authored expression, not just the
|
|
263
|
+
// resolved source. A very common, valid form is
|
|
264
|
+
// `ctx.csv(input.csv ?? 'default.csv')`: when the caller supplies --csv, the
|
|
265
|
+
// input field still owns the staged file and must be rewritten to its logical
|
|
266
|
+
// path before the runner starts.
|
|
267
|
+
const match =
|
|
268
|
+
/^\(?\s*input\.([A-Za-z_$][\w$]*)\s*\)?(?:\s*\?\?[\s\S]+)?$/.exec(
|
|
269
|
+
csvArg.trim(),
|
|
270
|
+
);
|
|
271
|
+
return match?.[1] ?? null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** The `csvInput` binding a `CsvInput<Row>` field compiles to. */
|
|
275
|
+
export function fileInputBindingsFromPlaySchema(
|
|
276
|
+
inputSchema: Record<string, unknown> | null | undefined,
|
|
277
|
+
): FileInputBinding[] {
|
|
278
|
+
const csvInput = schemaMetadata(inputSchema, 'csvInput');
|
|
279
|
+
if (!csvInput) return [];
|
|
280
|
+
return [
|
|
281
|
+
{
|
|
282
|
+
inputPath: stringMetadata(csvInput, 'inputField') ?? CSV_DATA_INPUT_KEY,
|
|
283
|
+
},
|
|
284
|
+
];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** The input field a `ctx.csv(input.x)` call reads. */
|
|
288
|
+
export function fileInputBindingsFromStaticPipeline(
|
|
289
|
+
staticPipeline: unknown,
|
|
290
|
+
): FileInputBinding[] {
|
|
291
|
+
if (
|
|
292
|
+
!staticPipeline ||
|
|
293
|
+
typeof staticPipeline !== 'object' ||
|
|
294
|
+
Array.isArray(staticPipeline)
|
|
295
|
+
) {
|
|
296
|
+
return [];
|
|
297
|
+
}
|
|
298
|
+
const inputField = inputFieldFromCsvArg(
|
|
299
|
+
(staticPipeline as Record<string, unknown>).csvArg,
|
|
300
|
+
);
|
|
301
|
+
return inputField ? [{ inputPath: inputField }] : [];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Every file-input binding of a Play definition, in staging order. */
|
|
305
|
+
export function playRunFileInputBindings(play: {
|
|
306
|
+
inputSchema?: Record<string, unknown> | null;
|
|
307
|
+
staticPipeline?: unknown;
|
|
308
|
+
}): FileInputBinding[] {
|
|
309
|
+
return [
|
|
310
|
+
...fileInputBindingsFromPlaySchema(play.inputSchema),
|
|
311
|
+
...fileInputBindingsFromStaticPipeline(play.staticPipeline),
|
|
312
|
+
];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function getDottedInputValue(
|
|
316
|
+
input: Record<string, unknown>,
|
|
317
|
+
path: string,
|
|
318
|
+
): unknown {
|
|
319
|
+
const parts = path
|
|
320
|
+
.split('.')
|
|
321
|
+
.map((part) => part.trim())
|
|
322
|
+
.filter(Boolean);
|
|
323
|
+
let cursor: unknown = input;
|
|
324
|
+
for (const part of parts) {
|
|
325
|
+
if (!cursor || typeof cursor !== 'object' || Array.isArray(cursor)) {
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
cursor = (cursor as Record<string, unknown>)[part];
|
|
329
|
+
}
|
|
330
|
+
return cursor;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function setDottedInputValue(
|
|
334
|
+
input: Record<string, unknown>,
|
|
335
|
+
path: string,
|
|
336
|
+
value: unknown,
|
|
337
|
+
): void {
|
|
338
|
+
const parts = path
|
|
339
|
+
.split('.')
|
|
340
|
+
.map((part) => part.trim())
|
|
341
|
+
.filter(Boolean);
|
|
342
|
+
if (parts.length === 0) {
|
|
343
|
+
throw new Error(`Invalid play input flag path: ${path}`);
|
|
344
|
+
}
|
|
345
|
+
let cursor: Record<string, unknown> = input;
|
|
346
|
+
for (const part of parts.slice(0, -1)) {
|
|
347
|
+
const existing = cursor[part];
|
|
348
|
+
if (
|
|
349
|
+
existing !== undefined &&
|
|
350
|
+
(!existing || typeof existing !== 'object' || Array.isArray(existing))
|
|
351
|
+
) {
|
|
352
|
+
throw new Error(
|
|
353
|
+
`Cannot set --${path}; input.${part} is already a non-object value.`,
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
if (!existing) {
|
|
357
|
+
cursor[part] = {};
|
|
358
|
+
}
|
|
359
|
+
cursor = cursor[part] as Record<string, unknown>;
|
|
360
|
+
}
|
|
361
|
+
cursor[parts[parts.length - 1]!] = value;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ── Staged file uploads ─────────────────────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
/** One file's bytes, encoded the way the staging routes receive them. */
|
|
367
|
+
export type StagedFileUpload = {
|
|
368
|
+
logicalPath: string;
|
|
369
|
+
contentBase64: string;
|
|
370
|
+
contentHash: string;
|
|
371
|
+
contentType: string;
|
|
372
|
+
bytes: number;
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Byte encoders. The default uses Web Crypto and `btoa` (browser and Node
|
|
377
|
+
* 18+); the CLI passes Node's `createHash`/`Buffer`, which produce identical
|
|
378
|
+
* output faster for large files.
|
|
379
|
+
*/
|
|
380
|
+
export type StagedFileCodec = {
|
|
381
|
+
sha256Hex(bytes: Uint8Array): Promise<string> | string;
|
|
382
|
+
base64(bytes: Uint8Array): string;
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
export const webStagedFileCodec: StagedFileCodec = {
|
|
386
|
+
async sha256Hex(bytes) {
|
|
387
|
+
const digest = await globalThis.crypto.subtle.digest(
|
|
388
|
+
'SHA-256',
|
|
389
|
+
bytes as Uint8Array<ArrayBuffer>,
|
|
390
|
+
);
|
|
391
|
+
return Array.from(new Uint8Array(digest), (byte) =>
|
|
392
|
+
byte.toString(16).padStart(2, '0'),
|
|
393
|
+
).join('');
|
|
394
|
+
},
|
|
395
|
+
base64(bytes) {
|
|
396
|
+
let binary = '';
|
|
397
|
+
for (let offset = 0; offset < bytes.length; offset += 0x8000) {
|
|
398
|
+
binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000));
|
|
399
|
+
}
|
|
400
|
+
return btoa(binary);
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
export function stagedFileContentType(logicalPath: string): string {
|
|
405
|
+
const lower = logicalPath.toLowerCase();
|
|
406
|
+
return lower.endsWith('.csv')
|
|
407
|
+
? 'text/csv'
|
|
408
|
+
: lower.endsWith('.json')
|
|
409
|
+
? 'application/json'
|
|
410
|
+
: 'application/octet-stream';
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export async function createStagedFileUpload(
|
|
414
|
+
logicalPath: string,
|
|
415
|
+
bytes: Uint8Array,
|
|
416
|
+
codec: StagedFileCodec = webStagedFileCodec,
|
|
417
|
+
): Promise<StagedFileUpload> {
|
|
418
|
+
return {
|
|
419
|
+
logicalPath,
|
|
420
|
+
contentBase64: codec.base64(bytes),
|
|
421
|
+
contentHash: await codec.sha256Hex(bytes),
|
|
422
|
+
contentType: stagedFileContentType(logicalPath),
|
|
423
|
+
bytes: bytes.byteLength,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* A staged-file upload target minted by POST /api/v2/plays/files/stage/mint.
|
|
429
|
+
* When `alreadyStaged` is true the server already holds the content-addressed
|
|
430
|
+
* object and `uploadUrl` is null; otherwise the client PUTs the body to
|
|
431
|
+
* `uploadUrl` with `uploadHeaders`.
|
|
432
|
+
*/
|
|
433
|
+
export type MintStagedFileUpload = {
|
|
434
|
+
logicalPath: string;
|
|
435
|
+
contentHash: string;
|
|
436
|
+
ref: PlayStagedFileRef;
|
|
437
|
+
alreadyStaged: boolean;
|
|
438
|
+
uploadUrl: string | null;
|
|
439
|
+
uploadHeaders: Record<string, string> | null;
|
|
440
|
+
uploadExpiresAt: string | null;
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// Legacy multipart staging proxies the body through the serverless function,
|
|
444
|
+
// which caps request bodies near 4.5MB. Stay conservatively below that so the
|
|
445
|
+
// fail-loud guidance fires before the platform returns an opaque 413.
|
|
446
|
+
export const STAGE_LEGACY_MULTIPART_MAX_BYTES = 4_000_000;
|
|
447
|
+
|
|
448
|
+
const STAGE_DIRECT_UPLOAD_MAX_ATTEMPTS = 3;
|
|
449
|
+
|
|
450
|
+
function sleep(ms: number): Promise<void> {
|
|
451
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function stagedUploadIdentity(
|
|
455
|
+
logicalPath: string,
|
|
456
|
+
contentHash: string,
|
|
457
|
+
): string {
|
|
458
|
+
return `${contentHash}:${logicalPath}`;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function isStagedUploadMintUnsupported(error: unknown): boolean {
|
|
462
|
+
// A server without the mint route returns 404. Treat only that as
|
|
463
|
+
// "unsupported, fall back"; every other failure propagates.
|
|
464
|
+
return error instanceof DeeplineError && error.statusCode === 404;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function isStagedUploadDirectEgressDenied(error: unknown): boolean {
|
|
468
|
+
return (
|
|
469
|
+
error instanceof DeeplineError &&
|
|
470
|
+
error.code === 'STAGED_FILE_UPLOAD_EGRESS_DENIED'
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function formatMegabytes(bytes: number): string {
|
|
475
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function decodeBase64Bytes(value: string): Uint8Array {
|
|
479
|
+
const binary = atob(value);
|
|
480
|
+
const bytes = new Uint8Array(binary.length);
|
|
481
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
482
|
+
bytes[index] = binary.charCodeAt(index);
|
|
483
|
+
}
|
|
484
|
+
return bytes;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function uploadStagedFileToPresignedUrl(input: {
|
|
488
|
+
url: string;
|
|
489
|
+
headers: Record<string, string>;
|
|
490
|
+
body: Uint8Array;
|
|
491
|
+
logicalPath: string;
|
|
492
|
+
}): Promise<void> {
|
|
493
|
+
// Wrap the bytes in a fresh Blob so the body is a BodyInit and the upload is
|
|
494
|
+
// re-runnable across retries (a consumed stream would not be).
|
|
495
|
+
const arrayBuffer = input.body.buffer.slice(
|
|
496
|
+
input.body.byteOffset,
|
|
497
|
+
input.body.byteOffset + input.body.byteLength,
|
|
498
|
+
) as ArrayBuffer;
|
|
499
|
+
let lastError: unknown;
|
|
500
|
+
for (
|
|
501
|
+
let attempt = 1;
|
|
502
|
+
attempt <= STAGE_DIRECT_UPLOAD_MAX_ATTEMPTS;
|
|
503
|
+
attempt += 1
|
|
504
|
+
) {
|
|
505
|
+
try {
|
|
506
|
+
const response = await fetch(input.url, {
|
|
507
|
+
method: 'PUT',
|
|
508
|
+
headers: input.headers,
|
|
509
|
+
body: new Blob([arrayBuffer]),
|
|
510
|
+
});
|
|
511
|
+
if (response.ok) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const text = await response.text().catch(() => '');
|
|
515
|
+
const egressDenied =
|
|
516
|
+
response.status === 403 &&
|
|
517
|
+
response.headers.get('x-deny-reason')?.trim().toLowerCase() ===
|
|
518
|
+
'host_not_allowed';
|
|
519
|
+
lastError = new DeeplineError(
|
|
520
|
+
`Direct storage upload of ${input.logicalPath} failed: ${response.status} ${text.slice(0, 200)}`,
|
|
521
|
+
response.status,
|
|
522
|
+
egressDenied
|
|
523
|
+
? 'STAGED_FILE_UPLOAD_EGRESS_DENIED'
|
|
524
|
+
: 'STAGED_FILE_UPLOAD_FAILED',
|
|
525
|
+
{ logicalPath: input.logicalPath },
|
|
526
|
+
);
|
|
527
|
+
// 4xx (other than throttling) will not recover on retry.
|
|
528
|
+
if (response.status < 500 && response.status !== 429) {
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
} catch (error) {
|
|
532
|
+
// fetch rejects network-level failures instead of returning a Response.
|
|
533
|
+
// Sandboxed egress proxies commonly reject the TLS CONNECT this way, and
|
|
534
|
+
// a browser rejects a cross-origin PUT the storage bucket's CORS policy
|
|
535
|
+
// does not allow the same way, so both take the multipart fallback after
|
|
536
|
+
// direct retries finish.
|
|
537
|
+
lastError =
|
|
538
|
+
error instanceof TypeError
|
|
539
|
+
? new DeeplineError(
|
|
540
|
+
`Direct storage upload of ${input.logicalPath} could not reach the storage endpoint: ${error.message}`,
|
|
541
|
+
undefined,
|
|
542
|
+
'STAGED_FILE_UPLOAD_EGRESS_DENIED',
|
|
543
|
+
{ logicalPath: input.logicalPath },
|
|
544
|
+
)
|
|
545
|
+
: error;
|
|
546
|
+
}
|
|
547
|
+
if (attempt < STAGE_DIRECT_UPLOAD_MAX_ATTEMPTS) {
|
|
548
|
+
await sleep(250 * attempt);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
throw lastError instanceof Error
|
|
552
|
+
? lastError
|
|
553
|
+
: new DeeplineError(
|
|
554
|
+
`Direct storage upload of ${input.logicalPath} failed.`,
|
|
555
|
+
undefined,
|
|
556
|
+
'STAGED_FILE_UPLOAD_FAILED',
|
|
557
|
+
{ logicalPath: input.logicalPath },
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Mint short-lived presigned upload targets for staged play files. The server
|
|
563
|
+
* returns an already-staged ref (no upload needed) for content-addressed files
|
|
564
|
+
* it already holds, or a presigned PUT URL the caller uploads the body to.
|
|
565
|
+
*/
|
|
566
|
+
export async function mintStagedPlayFileUploads(
|
|
567
|
+
http: PlayRunHttp,
|
|
568
|
+
files: Array<{
|
|
569
|
+
logicalPath: string;
|
|
570
|
+
contentHash: string;
|
|
571
|
+
contentType: string;
|
|
572
|
+
bytes: number;
|
|
573
|
+
}>,
|
|
574
|
+
): Promise<MintStagedFileUpload[]> {
|
|
575
|
+
const response = await http.post<{
|
|
576
|
+
uploads: MintStagedFileUpload[];
|
|
577
|
+
}>(PLAY_FILES_STAGE_MINT_PATH, { files });
|
|
578
|
+
return response.uploads ?? [];
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
async function stagePlayFilesViaMultipart(
|
|
582
|
+
http: PlayRunHttp,
|
|
583
|
+
files: StagedFileUpload[],
|
|
584
|
+
reason: 'mint-unsupported' | 'direct-egress-denied' = 'mint-unsupported',
|
|
585
|
+
): Promise<PlayStagedFileRef[]> {
|
|
586
|
+
for (const file of files) {
|
|
587
|
+
if (file.bytes > STAGE_LEGACY_MULTIPART_MAX_BYTES) {
|
|
588
|
+
const reasonMessage =
|
|
589
|
+
reason === 'direct-egress-denied'
|
|
590
|
+
? "direct storage is blocked by this environment's network egress policy"
|
|
591
|
+
: 'the connected Deepline server does not support direct-to-storage uploads';
|
|
592
|
+
throw new DeeplineError(
|
|
593
|
+
`Cannot stage ${file.logicalPath} (${formatMegabytes(file.bytes)}): ${reasonMessage}, and this file exceeds the ~4.5MB request-body limit the legacy upload path is subject to.`,
|
|
594
|
+
413,
|
|
595
|
+
'STAGED_FILE_TOO_LARGE_FOR_LEGACY_UPLOAD',
|
|
596
|
+
{
|
|
597
|
+
logicalPath: file.logicalPath,
|
|
598
|
+
bytes: file.bytes,
|
|
599
|
+
maxBytes: STAGE_LEGACY_MULTIPART_MAX_BYTES,
|
|
600
|
+
},
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
const buildFormData = () => {
|
|
605
|
+
const formData = new FormData();
|
|
606
|
+
formData.set(
|
|
607
|
+
'metadata',
|
|
608
|
+
JSON.stringify({
|
|
609
|
+
files: files.map((file, index) => ({
|
|
610
|
+
index,
|
|
611
|
+
logicalPath: file.logicalPath,
|
|
612
|
+
contentHash: file.contentHash,
|
|
613
|
+
contentType: file.contentType,
|
|
614
|
+
bytes: file.bytes,
|
|
615
|
+
})),
|
|
616
|
+
}),
|
|
617
|
+
);
|
|
618
|
+
for (const [index, file] of files.entries()) {
|
|
619
|
+
const bytes = decodeBase64Bytes(file.contentBase64);
|
|
620
|
+
const body = bytes.buffer.slice(
|
|
621
|
+
bytes.byteOffset,
|
|
622
|
+
bytes.byteOffset + bytes.byteLength,
|
|
623
|
+
) as ArrayBuffer;
|
|
624
|
+
formData.set(
|
|
625
|
+
`file:${index}`,
|
|
626
|
+
new Blob([body], { type: file.contentType }),
|
|
627
|
+
file.logicalPath,
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
return formData;
|
|
631
|
+
};
|
|
632
|
+
const response = await http.postFormData<{
|
|
633
|
+
files: PlayStagedFileRef[];
|
|
634
|
+
}>(PLAY_FILES_STAGE_PATH, buildFormData);
|
|
635
|
+
return response.files;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Stage play files. Primary path: mint presigned R2 PUT targets and upload each
|
|
640
|
+
* body straight to storage, bypassing the ~4.5MB serverless request-body limit.
|
|
641
|
+
* Falls back to the multipart route when the server predates minting, or per
|
|
642
|
+
* file when direct storage is unreachable from this environment.
|
|
643
|
+
*/
|
|
644
|
+
export async function stagePlayFiles(
|
|
645
|
+
http: PlayRunHttp,
|
|
646
|
+
files: StagedFileUpload[],
|
|
647
|
+
): Promise<PlayStagedFileRef[]> {
|
|
648
|
+
let uploads: MintStagedFileUpload[];
|
|
649
|
+
try {
|
|
650
|
+
uploads = await mintStagedPlayFileUploads(
|
|
651
|
+
http,
|
|
652
|
+
files.map((file) => ({
|
|
653
|
+
logicalPath: file.logicalPath,
|
|
654
|
+
contentHash: file.contentHash,
|
|
655
|
+
contentType: file.contentType,
|
|
656
|
+
bytes: file.bytes,
|
|
657
|
+
})),
|
|
658
|
+
);
|
|
659
|
+
} catch (error) {
|
|
660
|
+
if (isStagedUploadMintUnsupported(error)) {
|
|
661
|
+
// The connected server predates direct-to-storage staging. Fall back to
|
|
662
|
+
// the legacy multipart proxy, but only for files small enough to clear
|
|
663
|
+
// the serverless body limit; larger files fail loud with guidance.
|
|
664
|
+
return stagePlayFilesViaMultipart(http, files);
|
|
665
|
+
}
|
|
666
|
+
throw error;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const uploadByIdentity = new Map<string, MintStagedFileUpload>();
|
|
670
|
+
for (const upload of uploads) {
|
|
671
|
+
uploadByIdentity.set(
|
|
672
|
+
stagedUploadIdentity(upload.logicalPath, upload.contentHash),
|
|
673
|
+
upload,
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
type DirectStageResult =
|
|
678
|
+
| { ref: PlayStagedFileRef }
|
|
679
|
+
| { fallbackFile: StagedFileUpload };
|
|
680
|
+
const directResults: DirectStageResult[] = await Promise.all(
|
|
681
|
+
files.map(async (file) => {
|
|
682
|
+
const upload = uploadByIdentity.get(
|
|
683
|
+
stagedUploadIdentity(file.logicalPath, file.contentHash),
|
|
684
|
+
);
|
|
685
|
+
if (!upload) {
|
|
686
|
+
throw new DeeplineError(
|
|
687
|
+
`The staging server did not return an upload target for ${file.logicalPath}.`,
|
|
688
|
+
undefined,
|
|
689
|
+
'STAGED_FILE_MINT_INCOMPLETE',
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
if (upload.alreadyStaged || !upload.uploadUrl) {
|
|
693
|
+
return { ref: upload.ref };
|
|
694
|
+
}
|
|
695
|
+
try {
|
|
696
|
+
await uploadStagedFileToPresignedUrl({
|
|
697
|
+
url: upload.uploadUrl,
|
|
698
|
+
headers: upload.uploadHeaders ?? {
|
|
699
|
+
'content-type': file.contentType,
|
|
700
|
+
},
|
|
701
|
+
body: decodeBase64Bytes(file.contentBase64),
|
|
702
|
+
logicalPath: file.logicalPath,
|
|
703
|
+
});
|
|
704
|
+
return { ref: upload.ref };
|
|
705
|
+
} catch (error) {
|
|
706
|
+
if (isStagedUploadDirectEgressDenied(error)) {
|
|
707
|
+
return { fallbackFile: file };
|
|
708
|
+
}
|
|
709
|
+
throw error;
|
|
710
|
+
}
|
|
711
|
+
}),
|
|
712
|
+
);
|
|
713
|
+
|
|
714
|
+
return await Promise.all(
|
|
715
|
+
directResults.map(async (result) => {
|
|
716
|
+
if ('fallbackFile' in result) {
|
|
717
|
+
const [ref] = await stagePlayFilesViaMultipart(
|
|
718
|
+
http,
|
|
719
|
+
[result.fallbackFile],
|
|
720
|
+
'direct-egress-denied',
|
|
721
|
+
);
|
|
722
|
+
if (!ref) {
|
|
723
|
+
throw new DeeplineError(
|
|
724
|
+
`The staging server did not return an upload target for ${result.fallbackFile.logicalPath}.`,
|
|
725
|
+
undefined,
|
|
726
|
+
'STAGED_FILE_MINT_INCOMPLETE',
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
return ref;
|
|
730
|
+
}
|
|
731
|
+
return result.ref;
|
|
732
|
+
}),
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
export type StagedFileInputs = {
|
|
737
|
+
inputFile: PlayStagedFileRef | null;
|
|
738
|
+
packagedFiles: PlayStagedFileRef[];
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
/** A bound input value that resolves to local bytes (a CLI path or a dropped file). */
|
|
742
|
+
export type LocalFileInput = {
|
|
743
|
+
logicalPath: string;
|
|
744
|
+
read(): Promise<Uint8Array>;
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Stage every bound file input whose value resolves to local bytes, rewrite
|
|
749
|
+
* that input to the staged logical path, and return the staged refs: the first
|
|
750
|
+
* is the run's `inputFile`, the rest are `packagedFiles`.
|
|
751
|
+
*/
|
|
752
|
+
export async function stageFileInputs(input: {
|
|
753
|
+
/** `stagePlayFiles` bound to the caller's transport (the SDK client's `stagePlayFiles`, or {@link stagePlayFiles} over the session transport). */
|
|
754
|
+
stage: (files: StagedFileUpload[]) => Promise<PlayStagedFileRef[]>;
|
|
755
|
+
runtimeInput: Record<string, unknown>;
|
|
756
|
+
bindings: readonly FileInputBinding[];
|
|
757
|
+
resolveLocalFile: (
|
|
758
|
+
value: unknown,
|
|
759
|
+
binding: FileInputBinding,
|
|
760
|
+
) => LocalFileInput | null;
|
|
761
|
+
codec?: StagedFileCodec;
|
|
762
|
+
onStaging?: (fileCount: number) => void;
|
|
763
|
+
}): Promise<StagedFileInputs> {
|
|
764
|
+
const uniqueBindings = [
|
|
765
|
+
...new Map(
|
|
766
|
+
input.bindings.map((binding) => [binding.inputPath, binding]),
|
|
767
|
+
).values(),
|
|
768
|
+
];
|
|
769
|
+
const localFiles = uniqueBindings.flatMap((binding) => {
|
|
770
|
+
const file = input.resolveLocalFile(
|
|
771
|
+
getDottedInputValue(input.runtimeInput, binding.inputPath),
|
|
772
|
+
binding,
|
|
773
|
+
);
|
|
774
|
+
return file ? [{ binding, file }] : [];
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
if (localFiles.length === 0) {
|
|
778
|
+
return { inputFile: null, packagedFiles: [] };
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
input.onStaging?.(localFiles.length);
|
|
782
|
+
const uploads = await Promise.all(
|
|
783
|
+
localFiles.map(async ({ file }) =>
|
|
784
|
+
createStagedFileUpload(file.logicalPath, await file.read(), input.codec),
|
|
785
|
+
),
|
|
786
|
+
);
|
|
787
|
+
const staged = await input.stage(uploads);
|
|
788
|
+
for (const [index, { binding, file }] of localFiles.entries()) {
|
|
789
|
+
setDottedInputValue(
|
|
790
|
+
input.runtimeInput,
|
|
791
|
+
binding.inputPath,
|
|
792
|
+
file.logicalPath,
|
|
793
|
+
);
|
|
794
|
+
const stagedFile = staged[index];
|
|
795
|
+
if (stagedFile && stagedFile.logicalPath !== file.logicalPath) {
|
|
796
|
+
setDottedInputValue(
|
|
797
|
+
input.runtimeInput,
|
|
798
|
+
binding.inputPath,
|
|
799
|
+
stagedFile.logicalPath,
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
return {
|
|
805
|
+
inputFile: staged[0] ?? null,
|
|
806
|
+
packagedFiles: staged.slice(1),
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// ── Same-origin session transport ───────────────────────────────────────────
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* {@link PlayRunHttp} for a signed-in dashboard: relative same-origin paths,
|
|
814
|
+
* authenticated by the browser session cookie instead of an API key. It never
|
|
815
|
+
* retries; a start is never retried by the SDK client either.
|
|
816
|
+
*/
|
|
817
|
+
export function createSessionPlayRunHttp(
|
|
818
|
+
fetchImpl: typeof fetch = (...args) => fetch(...args),
|
|
819
|
+
): PlayRunHttp {
|
|
820
|
+
const send = async <T>(path: string, init: RequestInit): Promise<T> => {
|
|
821
|
+
const response = await fetchImpl(path, {
|
|
822
|
+
...init,
|
|
823
|
+
credentials: 'include',
|
|
824
|
+
});
|
|
825
|
+
const text = await response.text();
|
|
826
|
+
let parsed: unknown = text;
|
|
827
|
+
try {
|
|
828
|
+
parsed = text ? JSON.parse(text) : null;
|
|
829
|
+
} catch {
|
|
830
|
+
// Keep the raw text; a non-JSON error body still surfaces below.
|
|
831
|
+
}
|
|
832
|
+
if (!response.ok) {
|
|
833
|
+
throw new DeeplineError(
|
|
834
|
+
apiErrorMessage(parsed, response.status),
|
|
835
|
+
response.status,
|
|
836
|
+
apiErrorCodeFromResponse(parsed),
|
|
837
|
+
{ response: parsed },
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
return parsed as T;
|
|
841
|
+
};
|
|
842
|
+
return {
|
|
843
|
+
post: (path, body, headers) =>
|
|
844
|
+
send(path, {
|
|
845
|
+
method: 'POST',
|
|
846
|
+
headers: { 'content-type': 'application/json', ...headers },
|
|
847
|
+
body: JSON.stringify(body),
|
|
848
|
+
}),
|
|
849
|
+
postFormData: (path, formData) =>
|
|
850
|
+
send(path, {
|
|
851
|
+
method: 'POST',
|
|
852
|
+
body: typeof formData === 'function' ? formData() : formData,
|
|
853
|
+
}),
|
|
854
|
+
};
|
|
855
|
+
}
|