@transloadit/node 4.1.9 → 4.3.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 +81 -1
- package/dist/Transloadit.d.ts +36 -5
- package/dist/Transloadit.d.ts.map +1 -1
- package/dist/Transloadit.js +228 -39
- package/dist/Transloadit.js.map +1 -1
- package/dist/alphalib/assembly-linter.d.ts +123 -0
- package/dist/alphalib/assembly-linter.d.ts.map +1 -0
- package/dist/alphalib/assembly-linter.js +1142 -0
- package/dist/alphalib/assembly-linter.js.map +1 -0
- package/dist/alphalib/assembly-linter.lang.en.d.ts +87 -0
- package/dist/alphalib/assembly-linter.lang.en.d.ts.map +1 -0
- package/dist/alphalib/assembly-linter.lang.en.js +326 -0
- package/dist/alphalib/assembly-linter.lang.en.js.map +1 -0
- package/dist/alphalib/mcache.d.ts.map +1 -1
- package/dist/alphalib/mcache.js +22 -7
- package/dist/alphalib/mcache.js.map +1 -1
- package/dist/alphalib/object.d.ts +20 -0
- package/dist/alphalib/object.d.ts.map +1 -0
- package/dist/alphalib/object.js +23 -0
- package/dist/alphalib/object.js.map +1 -0
- package/dist/alphalib/stepParsing.d.ts +93 -0
- package/dist/alphalib/stepParsing.d.ts.map +1 -0
- package/dist/alphalib/stepParsing.js +1154 -0
- package/dist/alphalib/stepParsing.js.map +1 -0
- package/dist/alphalib/templateMerge.d.ts +4 -0
- package/dist/alphalib/templateMerge.d.ts.map +1 -0
- package/dist/alphalib/templateMerge.js +22 -0
- package/dist/alphalib/templateMerge.js.map +1 -0
- package/dist/alphalib/types/assemblyReplay.d.ts +56 -0
- package/dist/alphalib/types/assemblyReplay.d.ts.map +1 -1
- package/dist/alphalib/types/assemblyReplayNotification.d.ts +56 -0
- package/dist/alphalib/types/assemblyReplayNotification.d.ts.map +1 -1
- package/dist/alphalib/types/assemblyStatus.d.ts +63 -57
- package/dist/alphalib/types/assemblyStatus.d.ts.map +1 -1
- package/dist/alphalib/types/assemblyStatus.js +9 -1
- package/dist/alphalib/types/assemblyStatus.js.map +1 -1
- package/dist/alphalib/types/assemblyUrls.d.ts +1 -1
- package/dist/alphalib/types/assemblyUrls.d.ts.map +1 -1
- package/dist/alphalib/types/assemblyUrls.js.map +1 -1
- package/dist/alphalib/types/robots/_index.d.ts +608 -81
- package/dist/alphalib/types/robots/_index.d.ts.map +1 -1
- package/dist/alphalib/types/robots/_index.js +4 -0
- package/dist/alphalib/types/robots/_index.js.map +1 -1
- package/dist/alphalib/types/robots/_instructions-primitives.d.ts +4 -4
- package/dist/alphalib/types/robots/_instructions-primitives.d.ts.map +1 -1
- package/dist/alphalib/types/robots/_instructions-primitives.js +1 -0
- package/dist/alphalib/types/robots/_instructions-primitives.js.map +1 -1
- package/dist/alphalib/types/robots/document-optimize.d.ts +489 -0
- package/dist/alphalib/types/robots/document-optimize.d.ts.map +1 -0
- package/dist/alphalib/types/robots/document-optimize.js +151 -0
- package/dist/alphalib/types/robots/document-optimize.js.map +1 -0
- package/dist/alphalib/types/template.d.ts +1050 -174
- package/dist/alphalib/types/template.d.ts.map +1 -1
- package/dist/cli/commands/assemblies.d.ts +20 -1
- package/dist/cli/commands/assemblies.d.ts.map +1 -1
- package/dist/cli/commands/assemblies.js +137 -2
- package/dist/cli/commands/assemblies.js.map +1 -1
- package/dist/cli/commands/auth.d.ts.map +1 -1
- package/dist/cli/commands/auth.js +19 -19
- package/dist/cli/commands/auth.js.map +1 -1
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +2 -1
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/docs/assemblyLintingExamples.d.ts +2 -0
- package/dist/cli/docs/assemblyLintingExamples.d.ts.map +1 -0
- package/dist/cli/docs/assemblyLintingExamples.js +10 -0
- package/dist/cli/docs/assemblyLintingExamples.js.map +1 -0
- package/dist/cli/helpers.d.ts +11 -0
- package/dist/cli/helpers.d.ts.map +1 -1
- package/dist/cli/helpers.js +29 -0
- package/dist/cli/helpers.js.map +1 -1
- package/dist/lintAssemblyInput.d.ts +10 -0
- package/dist/lintAssemblyInput.d.ts.map +1 -0
- package/dist/lintAssemblyInput.js +73 -0
- package/dist/lintAssemblyInput.js.map +1 -0
- package/dist/lintAssemblyInstructions.d.ts +29 -0
- package/dist/lintAssemblyInstructions.d.ts.map +1 -0
- package/dist/lintAssemblyInstructions.js +33 -0
- package/dist/lintAssemblyInstructions.js.map +1 -0
- package/dist/tus.d.ts +2 -1
- package/dist/tus.d.ts.map +1 -1
- package/dist/tus.js +2 -1
- package/dist/tus.js.map +1 -1
- package/package.json +5 -2
- package/src/Transloadit.ts +318 -49
- package/src/alphalib/assembly-linter.lang.en.ts +393 -0
- package/src/alphalib/assembly-linter.ts +1475 -0
- package/src/alphalib/mcache.ts +26 -7
- package/src/alphalib/object.ts +27 -0
- package/src/alphalib/stepParsing.ts +1465 -0
- package/src/alphalib/templateMerge.ts +32 -0
- package/src/alphalib/types/assemblyStatus.ts +9 -1
- package/src/alphalib/types/assemblyUrls.ts +2 -5
- package/src/alphalib/types/robots/_index.ts +14 -0
- package/src/alphalib/types/robots/_instructions-primitives.ts +1 -0
- package/src/alphalib/types/robots/document-optimize.ts +180 -0
- package/src/alphalib/typings/json-to-ast.d.ts +34 -0
- package/src/cli/commands/assemblies.ts +161 -2
- package/src/cli/commands/auth.ts +19 -22
- package/src/cli/commands/index.ts +2 -0
- package/src/cli/docs/assemblyLintingExamples.ts +9 -0
- package/src/cli/helpers.ts +50 -0
- package/src/lintAssemblyInput.ts +89 -0
- package/src/lintAssemblyInstructions.ts +72 -0
- package/src/tus.ts +3 -0
package/src/Transloadit.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as assert from 'node:assert'
|
|
2
2
|
import { randomUUID } from 'node:crypto'
|
|
3
3
|
import { constants, createReadStream } from 'node:fs'
|
|
4
|
-
import { access } from 'node:fs/promises'
|
|
4
|
+
import { access, stat } from 'node:fs/promises'
|
|
5
|
+
import { basename } from 'node:path'
|
|
5
6
|
import type { Readable } from 'node:stream'
|
|
6
7
|
import { setTimeout as delay } from 'node:timers/promises'
|
|
7
8
|
import { getSignedSmartCdnUrl, signParamsSync } from '@transloadit/utils/node'
|
|
@@ -44,6 +45,11 @@ import type {
|
|
|
44
45
|
TemplateResponse,
|
|
45
46
|
} from './apiTypes.ts'
|
|
46
47
|
import InconsistentResponseError from './InconsistentResponseError.ts'
|
|
48
|
+
import type {
|
|
49
|
+
LintAssemblyInstructionsInput,
|
|
50
|
+
LintAssemblyInstructionsResult,
|
|
51
|
+
} from './lintAssemblyInstructions.ts'
|
|
52
|
+
import { lintAssemblyInstructions as lintAssemblyInstructionsInternal } from './lintAssemblyInstructions.ts'
|
|
47
53
|
import PaginationStream from './PaginationStream.ts'
|
|
48
54
|
import PollingTimeoutError from './PollingTimeoutError.ts'
|
|
49
55
|
import type { Stream } from './tus.ts'
|
|
@@ -64,6 +70,7 @@ export {
|
|
|
64
70
|
export type { AssemblyStatus } from './alphalib/types/assemblyStatus.ts'
|
|
65
71
|
export * from './apiTypes.ts'
|
|
66
72
|
export { InconsistentResponseError, ApiError }
|
|
73
|
+
export type { LintAssemblyInstructionsResult, LintFatalLevel } from './lintAssemblyInstructions.ts'
|
|
67
74
|
|
|
68
75
|
const log = debug('transloadit')
|
|
69
76
|
const logWarn = debug('transloadit:warn')
|
|
@@ -77,8 +84,73 @@ const { version } = packageJson
|
|
|
77
84
|
|
|
78
85
|
export type AssemblyProgress = (assembly: AssemblyStatus) => void
|
|
79
86
|
|
|
80
|
-
|
|
81
|
-
|
|
87
|
+
type UploadDescriptor = {
|
|
88
|
+
label: string
|
|
89
|
+
filename: string
|
|
90
|
+
size?: number
|
|
91
|
+
path?: string
|
|
92
|
+
value?: Readable | IntoStreamInput
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const getUploadKey = (
|
|
96
|
+
fieldname: string | null | undefined,
|
|
97
|
+
filename: string | null | undefined,
|
|
98
|
+
size: number | null | undefined,
|
|
99
|
+
): string | null => {
|
|
100
|
+
if (!fieldname || !filename || size == null) return null
|
|
101
|
+
return JSON.stringify([fieldname, filename, size])
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const getSizeFromValue = (value: Readable | IntoStreamInput): number | undefined => {
|
|
105
|
+
if (typeof value === 'string') return Buffer.byteLength(value)
|
|
106
|
+
if (Buffer.isBuffer(value)) return value.length
|
|
107
|
+
if (value instanceof ArrayBuffer) return value.byteLength
|
|
108
|
+
if (ArrayBuffer.isView(value)) return value.byteLength
|
|
109
|
+
return undefined
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const toReadableUpload = (label: string, value: Readable | IntoStreamInput): Readable => {
|
|
113
|
+
const readable = isReadableStream(value)
|
|
114
|
+
if (!readable && isStream(value)) {
|
|
115
|
+
throw new Error(`Upload named "${label}" is not a Readable stream`)
|
|
116
|
+
}
|
|
117
|
+
return readable ? value : intoStream(value)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const buildStreamsMap = (descriptors: UploadDescriptor[]): Record<string, Stream> =>
|
|
121
|
+
Object.fromEntries(
|
|
122
|
+
descriptors.map((descriptor) => {
|
|
123
|
+
if (descriptor.path) {
|
|
124
|
+
const stream = createReadStream(descriptor.path)
|
|
125
|
+
return [descriptor.label, { stream, path: descriptor.path }]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const value = descriptor.value
|
|
129
|
+
if (value == null) {
|
|
130
|
+
throw new Error(`Upload named "${descriptor.label}" has no data`)
|
|
131
|
+
}
|
|
132
|
+
const stream = toReadableUpload(descriptor.label, value)
|
|
133
|
+
return [descriptor.label, { stream }]
|
|
134
|
+
}),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
const pauseStreams = (streamsMap: Record<string, Stream>): void => {
|
|
138
|
+
for (const { stream } of Object.values(streamsMap)) {
|
|
139
|
+
stream.pause()
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const createStreamErrorPromise = (streamsMap: Record<string, Stream>): Promise<never> => {
|
|
144
|
+
const promise = new Promise<never>((_resolve, reject) => {
|
|
145
|
+
for (const { stream } of Object.values(streamsMap)) {
|
|
146
|
+
stream.on('error', reject)
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
promise.catch(() => {})
|
|
150
|
+
return promise
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface AssemblyUploadOptions {
|
|
82
154
|
files?: {
|
|
83
155
|
[name: string]: string
|
|
84
156
|
}
|
|
@@ -91,19 +163,32 @@ export interface CreateAssemblyOptions {
|
|
|
91
163
|
timeout?: number
|
|
92
164
|
onUploadProgress?: (uploadProgress: UploadProgress) => void
|
|
93
165
|
onAssemblyProgress?: AssemblyProgress
|
|
94
|
-
assemblyId?: string
|
|
95
166
|
/**
|
|
96
|
-
* Optional AbortSignal to cancel the
|
|
167
|
+
* Optional AbortSignal to cancel the upload and any follow-up polling.
|
|
97
168
|
* When aborted, any in-flight HTTP requests and TUS uploads will be cancelled.
|
|
98
169
|
*/
|
|
99
170
|
signal?: AbortSignal
|
|
100
171
|
}
|
|
101
172
|
|
|
173
|
+
export interface CreateAssemblyOptions extends AssemblyUploadOptions {
|
|
174
|
+
params?: CreateAssemblyParams
|
|
175
|
+
assemblyId?: string
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface ResumeAssemblyUploadsOptions extends AssemblyUploadOptions {
|
|
179
|
+
assemblyUrl: string
|
|
180
|
+
}
|
|
181
|
+
|
|
102
182
|
export interface AwaitAssemblyCompletionOptions {
|
|
103
183
|
onAssemblyProgress?: AssemblyProgress
|
|
104
184
|
timeout?: number
|
|
105
185
|
interval?: number
|
|
106
186
|
startTimeMs?: number
|
|
187
|
+
/**
|
|
188
|
+
* Optional assembly URL to poll instead of the configured client endpoint.
|
|
189
|
+
* Useful when resuming an Assembly created on a different host/region.
|
|
190
|
+
*/
|
|
191
|
+
assemblyUrl?: string
|
|
107
192
|
/**
|
|
108
193
|
* Optional AbortSignal to cancel polling.
|
|
109
194
|
* When aborted, the polling loop will stop and throw an AbortError.
|
|
@@ -117,6 +202,14 @@ export interface AwaitAssemblyCompletionOptions {
|
|
|
117
202
|
onPoll?: () => boolean | undefined
|
|
118
203
|
}
|
|
119
204
|
|
|
205
|
+
export interface LintAssemblyInstructionsOptions
|
|
206
|
+
extends Omit<LintAssemblyInstructionsInput, 'template'> {
|
|
207
|
+
/**
|
|
208
|
+
* Template ID to merge with the provided instructions before linting.
|
|
209
|
+
*/
|
|
210
|
+
templateId?: string
|
|
211
|
+
}
|
|
212
|
+
|
|
120
213
|
export interface SmartCDNUrlOptions {
|
|
121
214
|
/**
|
|
122
215
|
* Workspace slug
|
|
@@ -159,6 +252,14 @@ function getHrTimeMs(): number {
|
|
|
159
252
|
return Number(process.hrtime.bigint() / 1000000n)
|
|
160
253
|
}
|
|
161
254
|
|
|
255
|
+
function getAssemblyIdFromUrl(assemblyUrl: string): string {
|
|
256
|
+
const match = assemblyUrl.match(/\/assemblies\/([^/?#]+)/)
|
|
257
|
+
if (!match) {
|
|
258
|
+
throw new Error(`Invalid assembly URL: ${assemblyUrl}`)
|
|
259
|
+
}
|
|
260
|
+
return match[1] ?? ''
|
|
261
|
+
}
|
|
262
|
+
|
|
162
263
|
function checkResult<T>(result: T | { error: string }): asserts result is T {
|
|
163
264
|
// In case server returned a successful HTTP status code, but an `error` in the JSON object
|
|
164
265
|
// This happens sometimes, for example when createAssembly with an invalid file (IMPORT_FILE_ERROR)
|
|
@@ -276,36 +377,25 @@ export class Transloadit {
|
|
|
276
377
|
{ concurrency: 5 },
|
|
277
378
|
)
|
|
278
379
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}),
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const allStreamsMap = Object.fromEntries<Stream>(
|
|
294
|
-
Object.entries(streamsMap).map(([label, stream]) => [label, { stream }]),
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
// Create streams from files too
|
|
298
|
-
for (const [label, path] of Object.entries(files)) {
|
|
299
|
-
const stream = createReadStream(path)
|
|
300
|
-
allStreamsMap[label] = { stream, path } // File streams have path
|
|
301
|
-
}
|
|
380
|
+
const descriptors: UploadDescriptor[] = [
|
|
381
|
+
...Object.entries(files).map(([label, path]) => ({
|
|
382
|
+
label,
|
|
383
|
+
path,
|
|
384
|
+
filename: basename(path),
|
|
385
|
+
})),
|
|
386
|
+
...Object.entries(uploads).map(([label, value]) => ({
|
|
387
|
+
label,
|
|
388
|
+
filename: label,
|
|
389
|
+
value,
|
|
390
|
+
})),
|
|
391
|
+
]
|
|
392
|
+
|
|
393
|
+
const allStreamsMap = buildStreamsMap(descriptors)
|
|
302
394
|
|
|
303
395
|
const allStreams = Object.values(allStreamsMap)
|
|
304
396
|
|
|
305
397
|
// Pause all streams
|
|
306
|
-
|
|
307
|
-
stream.pause()
|
|
308
|
-
}
|
|
398
|
+
pauseStreams(allStreamsMap)
|
|
309
399
|
|
|
310
400
|
// If any stream emits error, we want to handle this and exit with error.
|
|
311
401
|
// This promise races against createAssemblyAndUpload() below via Promise.race().
|
|
@@ -313,12 +403,7 @@ export class Transloadit {
|
|
|
313
403
|
// it's no longer awaited, but stream error handlers remain attached.
|
|
314
404
|
// The no-op catch prevents Node's unhandled rejection warning if a stream
|
|
315
405
|
// errors after the race is already won.
|
|
316
|
-
const streamErrorPromise =
|
|
317
|
-
for (const { stream } of allStreams) {
|
|
318
|
-
stream.on('error', reject)
|
|
319
|
-
}
|
|
320
|
-
})
|
|
321
|
-
streamErrorPromise.catch(() => {})
|
|
406
|
+
const streamErrorPromise = createStreamErrorPromise(allStreamsMap)
|
|
322
407
|
|
|
323
408
|
const createAssemblyAndUpload = async () => {
|
|
324
409
|
const result: AssemblyStatus = await this._remoteJson({
|
|
@@ -368,6 +453,149 @@ export class Transloadit {
|
|
|
368
453
|
return Object.assign(promise, { assemblyId: effectiveAssemblyId })
|
|
369
454
|
}
|
|
370
455
|
|
|
456
|
+
/**
|
|
457
|
+
* Lint Assembly Instructions locally.
|
|
458
|
+
*
|
|
459
|
+
* If a templateId is provided, the template content is merged with the instructions,
|
|
460
|
+
* just like the API. When a template sets `allow_steps_override=false`, providing
|
|
461
|
+
* `steps` will throw a TEMPLATE_DENIES_STEPS_OVERRIDE error.
|
|
462
|
+
*
|
|
463
|
+
* The `assemblyInstructions` input may be a JSON string, a full instructions object,
|
|
464
|
+
* or a steps-only object (missing the `steps` property).
|
|
465
|
+
*/
|
|
466
|
+
async lintAssemblyInstructions(
|
|
467
|
+
options: LintAssemblyInstructionsOptions,
|
|
468
|
+
): Promise<LintAssemblyInstructionsResult> {
|
|
469
|
+
const { templateId, ...rest } = options
|
|
470
|
+
if (!templateId) {
|
|
471
|
+
return await lintAssemblyInstructionsInternal(rest)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const template = await this.getTemplate(templateId)
|
|
475
|
+
return await lintAssemblyInstructionsInternal({
|
|
476
|
+
...rest,
|
|
477
|
+
template: template.content,
|
|
478
|
+
})
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async resumeAssemblyUploads(opts: ResumeAssemblyUploadsOptions): Promise<AssemblyStatus> {
|
|
482
|
+
const {
|
|
483
|
+
assemblyUrl,
|
|
484
|
+
files = {},
|
|
485
|
+
uploads = {},
|
|
486
|
+
chunkSize: requestedChunkSize = Number.POSITIVE_INFINITY,
|
|
487
|
+
uploadConcurrency = 10,
|
|
488
|
+
timeout = 24 * 60 * 60 * 1000, // 1 day
|
|
489
|
+
waitForCompletion = false,
|
|
490
|
+
onUploadProgress = () => {},
|
|
491
|
+
onAssemblyProgress = () => {},
|
|
492
|
+
signal,
|
|
493
|
+
} = opts
|
|
494
|
+
|
|
495
|
+
const startTimeMs = getHrTimeMs()
|
|
496
|
+
|
|
497
|
+
getAssemblyIdFromUrl(assemblyUrl)
|
|
498
|
+
const assembly = await this._fetchAssemblyStatus({ url: assemblyUrl, signal })
|
|
499
|
+
const statusUrl = assembly.assembly_ssl_url ?? assembly.assembly_url ?? assemblyUrl
|
|
500
|
+
|
|
501
|
+
const finishedKeys = new Set<string>()
|
|
502
|
+
for (const upload of assembly.uploads ?? []) {
|
|
503
|
+
const key = getUploadKey(upload.field ?? null, upload.basename ?? null, upload.size)
|
|
504
|
+
if (key) finishedKeys.add(key)
|
|
505
|
+
}
|
|
506
|
+
for (const upload of assembly.tus_uploads ?? []) {
|
|
507
|
+
if (!upload.finished) continue
|
|
508
|
+
const key = getUploadKey(upload.fieldname, upload.filename, upload.size)
|
|
509
|
+
if (key) finishedKeys.add(key)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const resumeUrls = new Map<string, string>()
|
|
513
|
+
for (const upload of assembly.tus_uploads ?? []) {
|
|
514
|
+
if (upload.finished) continue
|
|
515
|
+
if (!upload.upload_url) continue
|
|
516
|
+
const key = getUploadKey(upload.fieldname, upload.filename, upload.size)
|
|
517
|
+
if (key) resumeUrls.set(key, upload.upload_url)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const descriptors: UploadDescriptor[] = []
|
|
521
|
+
|
|
522
|
+
await pMap(
|
|
523
|
+
Object.entries(files),
|
|
524
|
+
async ([label, path]) => {
|
|
525
|
+
await access(path, constants.F_OK | constants.R_OK)
|
|
526
|
+
const info = await stat(path)
|
|
527
|
+
descriptors.push({
|
|
528
|
+
label,
|
|
529
|
+
path,
|
|
530
|
+
filename: basename(path),
|
|
531
|
+
size: info.size,
|
|
532
|
+
})
|
|
533
|
+
},
|
|
534
|
+
{ concurrency: 5 },
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
for (const [label, value] of Object.entries(uploads)) {
|
|
538
|
+
descriptors.push({
|
|
539
|
+
label,
|
|
540
|
+
filename: label,
|
|
541
|
+
size: isReadableStream(value) ? undefined : getSizeFromValue(value),
|
|
542
|
+
value,
|
|
543
|
+
})
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const descriptorsToUpload = descriptors.filter((descriptor) => {
|
|
547
|
+
const key = getUploadKey(descriptor.label, descriptor.filename, descriptor.size ?? null)
|
|
548
|
+
return key ? !finishedKeys.has(key) : true
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
const uploadUrlsByLabel: Record<string, string> = {}
|
|
552
|
+
for (const descriptor of descriptorsToUpload) {
|
|
553
|
+
if (!descriptor.path) continue
|
|
554
|
+
const key = getUploadKey(descriptor.label, descriptor.filename, descriptor.size ?? null)
|
|
555
|
+
if (!key) continue
|
|
556
|
+
const uploadUrl = resumeUrls.get(key)
|
|
557
|
+
if (uploadUrl) uploadUrlsByLabel[descriptor.label] = uploadUrl
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const streamsMap = buildStreamsMap(descriptorsToUpload)
|
|
561
|
+
pauseStreams(streamsMap)
|
|
562
|
+
|
|
563
|
+
if (Object.keys(streamsMap).length > 0) {
|
|
564
|
+
const streamErrorPromise = createStreamErrorPromise(streamsMap)
|
|
565
|
+
|
|
566
|
+
const uploadPromise = sendTusRequest({
|
|
567
|
+
streamsMap,
|
|
568
|
+
assembly,
|
|
569
|
+
requestedChunkSize,
|
|
570
|
+
uploadConcurrency,
|
|
571
|
+
onProgress: onUploadProgress,
|
|
572
|
+
signal,
|
|
573
|
+
uploadUrls: uploadUrlsByLabel,
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
await Promise.race([uploadPromise, streamErrorPromise])
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const latestAssembly = await this._fetchAssemblyStatus({ url: statusUrl, signal })
|
|
580
|
+
if (!waitForCompletion) return latestAssembly
|
|
581
|
+
|
|
582
|
+
if (latestAssembly.assembly_id == null) {
|
|
583
|
+
throw new InconsistentResponseError(
|
|
584
|
+
'Server returned an assembly response without an assembly_id after resuming uploads',
|
|
585
|
+
)
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const awaitResult = await this.awaitAssemblyCompletion(latestAssembly.assembly_id, {
|
|
589
|
+
timeout,
|
|
590
|
+
onAssemblyProgress,
|
|
591
|
+
startTimeMs,
|
|
592
|
+
assemblyUrl: statusUrl,
|
|
593
|
+
signal,
|
|
594
|
+
})
|
|
595
|
+
checkResult(awaitResult)
|
|
596
|
+
return awaitResult
|
|
597
|
+
}
|
|
598
|
+
|
|
371
599
|
async awaitAssemblyCompletion(
|
|
372
600
|
assemblyId: string,
|
|
373
601
|
{
|
|
@@ -375,6 +603,7 @@ export class Transloadit {
|
|
|
375
603
|
timeout,
|
|
376
604
|
startTimeMs = getHrTimeMs(),
|
|
377
605
|
interval = 1000,
|
|
606
|
+
assemblyUrl,
|
|
378
607
|
signal,
|
|
379
608
|
onPoll,
|
|
380
609
|
}: AwaitAssemblyCompletionOptions = {},
|
|
@@ -383,6 +612,12 @@ export class Transloadit {
|
|
|
383
612
|
|
|
384
613
|
let lastResult: AssemblyStatus | undefined
|
|
385
614
|
|
|
615
|
+
const fetchAssemblyStatus = (): Promise<AssemblyStatus> => {
|
|
616
|
+
return assemblyUrl
|
|
617
|
+
? this._fetchAssemblyStatus({ url: assemblyUrl, signal })
|
|
618
|
+
: this.getAssembly(assemblyId, { signal })
|
|
619
|
+
}
|
|
620
|
+
|
|
386
621
|
while (true) {
|
|
387
622
|
// Check if caller wants to stop polling early
|
|
388
623
|
if (onPoll?.() === false && lastResult) {
|
|
@@ -394,7 +629,7 @@ export class Transloadit {
|
|
|
394
629
|
throw signal.reason ?? new DOMException('Aborted', 'AbortError')
|
|
395
630
|
}
|
|
396
631
|
|
|
397
|
-
const result = await
|
|
632
|
+
const result = await fetchAssemblyStatus()
|
|
398
633
|
lastResult = result
|
|
399
634
|
|
|
400
635
|
// If 'ok' is not in result, it implies a terminal state (e.g., error, completed, canceled).
|
|
@@ -576,16 +811,33 @@ export class Transloadit {
|
|
|
576
811
|
assemblyId: string,
|
|
577
812
|
options?: { signal?: AbortSignal },
|
|
578
813
|
): Promise<AssemblyStatus> {
|
|
579
|
-
|
|
580
|
-
|
|
814
|
+
return await this._fetchAssemblyStatus({
|
|
815
|
+
assemblyId,
|
|
581
816
|
signal: options?.signal,
|
|
582
817
|
})
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
private async _fetchAssemblyStatus({
|
|
821
|
+
assemblyId,
|
|
822
|
+
url,
|
|
823
|
+
signal,
|
|
824
|
+
}: {
|
|
825
|
+
assemblyId?: string
|
|
826
|
+
url?: string
|
|
827
|
+
signal?: AbortSignal
|
|
828
|
+
}): Promise<AssemblyStatus> {
|
|
829
|
+
const rawResult = await this._remoteJson<Record<string, unknown>, OptionalAuthParams>({
|
|
830
|
+
url,
|
|
831
|
+
urlSuffix: url ? undefined : `/assemblies/${assemblyId}`,
|
|
832
|
+
signal,
|
|
833
|
+
})
|
|
583
834
|
|
|
584
835
|
const parsedResult = zodParseWithContext(assemblyStatusSchema, rawResult)
|
|
585
836
|
|
|
586
837
|
if (!parsedResult.success) {
|
|
838
|
+
const label = assemblyId ?? url ?? 'unknown'
|
|
587
839
|
this.maybeThrowInconsistentResponseError(
|
|
588
|
-
`The API responded with data that does not match the expected schema while getting Assembly: ${
|
|
840
|
+
`The API responded with data that does not match the expected schema while getting Assembly: ${label}.\n${parsedResult.humanReadable}`,
|
|
589
841
|
)
|
|
590
842
|
}
|
|
591
843
|
|
|
@@ -915,23 +1167,40 @@ export class Transloadit {
|
|
|
915
1167
|
|
|
916
1168
|
// check whether we should retry
|
|
917
1169
|
// https://transloadit.com/blog/2012/04/introducing-rate-limiting/
|
|
918
|
-
|
|
1170
|
+
const retryAfterHeader = err.response?.headers?.['retry-after']
|
|
1171
|
+
const retryAfterSeconds =
|
|
1172
|
+
typeof retryAfterHeader === 'string' ? Number(retryAfterHeader) : undefined
|
|
1173
|
+
const retryInFromInfo =
|
|
919
1174
|
typeof body === 'object' &&
|
|
920
1175
|
body != null &&
|
|
921
|
-
'error' in body &&
|
|
922
1176
|
'info' in body &&
|
|
923
1177
|
typeof body.info === 'object' &&
|
|
924
1178
|
body.info != null &&
|
|
925
1179
|
'retryIn' in body.info &&
|
|
926
1180
|
typeof body.info.retryIn === 'number' &&
|
|
927
|
-
|
|
1181
|
+
body.info.retryIn > 0
|
|
1182
|
+
? body.info.retryIn
|
|
1183
|
+
: undefined
|
|
1184
|
+
const retryInSec =
|
|
1185
|
+
retryInFromInfo ??
|
|
1186
|
+
(typeof retryAfterSeconds === 'number' && retryAfterSeconds > 0
|
|
1187
|
+
? retryAfterSeconds
|
|
1188
|
+
: undefined)
|
|
1189
|
+
const shouldRetry =
|
|
928
1190
|
retryCount < this._maxRetries && // 413 taken from https://transloadit.com/blog/2012/04/introducing-rate-limiting/
|
|
929
1191
|
// todo can 413 be removed?
|
|
930
|
-
((statusCode === 413 &&
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1192
|
+
((statusCode === 413 &&
|
|
1193
|
+
body &&
|
|
1194
|
+
typeof body === 'object' &&
|
|
1195
|
+
body.error === 'RATE_LIMIT_REACHED') ||
|
|
1196
|
+
statusCode === 429)
|
|
1197
|
+
|
|
1198
|
+
if (shouldRetry) {
|
|
1199
|
+
const retryDelaySec = retryInSec ?? 1
|
|
1200
|
+
logWarn(
|
|
1201
|
+
`Rate limit reached, retrying request in approximately ${retryDelaySec} seconds.`,
|
|
1202
|
+
)
|
|
1203
|
+
const retryInMs = 1000 * (retryDelaySec * (1 + 0.1 * Math.random()))
|
|
935
1204
|
await delay(retryInMs)
|
|
936
1205
|
// Retry
|
|
937
1206
|
} else {
|