@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.
Files changed (105) hide show
  1. package/README.md +81 -1
  2. package/dist/Transloadit.d.ts +36 -5
  3. package/dist/Transloadit.d.ts.map +1 -1
  4. package/dist/Transloadit.js +228 -39
  5. package/dist/Transloadit.js.map +1 -1
  6. package/dist/alphalib/assembly-linter.d.ts +123 -0
  7. package/dist/alphalib/assembly-linter.d.ts.map +1 -0
  8. package/dist/alphalib/assembly-linter.js +1142 -0
  9. package/dist/alphalib/assembly-linter.js.map +1 -0
  10. package/dist/alphalib/assembly-linter.lang.en.d.ts +87 -0
  11. package/dist/alphalib/assembly-linter.lang.en.d.ts.map +1 -0
  12. package/dist/alphalib/assembly-linter.lang.en.js +326 -0
  13. package/dist/alphalib/assembly-linter.lang.en.js.map +1 -0
  14. package/dist/alphalib/mcache.d.ts.map +1 -1
  15. package/dist/alphalib/mcache.js +22 -7
  16. package/dist/alphalib/mcache.js.map +1 -1
  17. package/dist/alphalib/object.d.ts +20 -0
  18. package/dist/alphalib/object.d.ts.map +1 -0
  19. package/dist/alphalib/object.js +23 -0
  20. package/dist/alphalib/object.js.map +1 -0
  21. package/dist/alphalib/stepParsing.d.ts +93 -0
  22. package/dist/alphalib/stepParsing.d.ts.map +1 -0
  23. package/dist/alphalib/stepParsing.js +1154 -0
  24. package/dist/alphalib/stepParsing.js.map +1 -0
  25. package/dist/alphalib/templateMerge.d.ts +4 -0
  26. package/dist/alphalib/templateMerge.d.ts.map +1 -0
  27. package/dist/alphalib/templateMerge.js +22 -0
  28. package/dist/alphalib/templateMerge.js.map +1 -0
  29. package/dist/alphalib/types/assemblyReplay.d.ts +56 -0
  30. package/dist/alphalib/types/assemblyReplay.d.ts.map +1 -1
  31. package/dist/alphalib/types/assemblyReplayNotification.d.ts +56 -0
  32. package/dist/alphalib/types/assemblyReplayNotification.d.ts.map +1 -1
  33. package/dist/alphalib/types/assemblyStatus.d.ts +63 -57
  34. package/dist/alphalib/types/assemblyStatus.d.ts.map +1 -1
  35. package/dist/alphalib/types/assemblyStatus.js +9 -1
  36. package/dist/alphalib/types/assemblyStatus.js.map +1 -1
  37. package/dist/alphalib/types/assemblyUrls.d.ts +1 -1
  38. package/dist/alphalib/types/assemblyUrls.d.ts.map +1 -1
  39. package/dist/alphalib/types/assemblyUrls.js.map +1 -1
  40. package/dist/alphalib/types/robots/_index.d.ts +608 -81
  41. package/dist/alphalib/types/robots/_index.d.ts.map +1 -1
  42. package/dist/alphalib/types/robots/_index.js +4 -0
  43. package/dist/alphalib/types/robots/_index.js.map +1 -1
  44. package/dist/alphalib/types/robots/_instructions-primitives.d.ts +4 -4
  45. package/dist/alphalib/types/robots/_instructions-primitives.d.ts.map +1 -1
  46. package/dist/alphalib/types/robots/_instructions-primitives.js +1 -0
  47. package/dist/alphalib/types/robots/_instructions-primitives.js.map +1 -1
  48. package/dist/alphalib/types/robots/document-optimize.d.ts +489 -0
  49. package/dist/alphalib/types/robots/document-optimize.d.ts.map +1 -0
  50. package/dist/alphalib/types/robots/document-optimize.js +151 -0
  51. package/dist/alphalib/types/robots/document-optimize.js.map +1 -0
  52. package/dist/alphalib/types/template.d.ts +1050 -174
  53. package/dist/alphalib/types/template.d.ts.map +1 -1
  54. package/dist/cli/commands/assemblies.d.ts +20 -1
  55. package/dist/cli/commands/assemblies.d.ts.map +1 -1
  56. package/dist/cli/commands/assemblies.js +137 -2
  57. package/dist/cli/commands/assemblies.js.map +1 -1
  58. package/dist/cli/commands/auth.d.ts.map +1 -1
  59. package/dist/cli/commands/auth.js +19 -19
  60. package/dist/cli/commands/auth.js.map +1 -1
  61. package/dist/cli/commands/index.d.ts.map +1 -1
  62. package/dist/cli/commands/index.js +2 -1
  63. package/dist/cli/commands/index.js.map +1 -1
  64. package/dist/cli/docs/assemblyLintingExamples.d.ts +2 -0
  65. package/dist/cli/docs/assemblyLintingExamples.d.ts.map +1 -0
  66. package/dist/cli/docs/assemblyLintingExamples.js +10 -0
  67. package/dist/cli/docs/assemblyLintingExamples.js.map +1 -0
  68. package/dist/cli/helpers.d.ts +11 -0
  69. package/dist/cli/helpers.d.ts.map +1 -1
  70. package/dist/cli/helpers.js +29 -0
  71. package/dist/cli/helpers.js.map +1 -1
  72. package/dist/lintAssemblyInput.d.ts +10 -0
  73. package/dist/lintAssemblyInput.d.ts.map +1 -0
  74. package/dist/lintAssemblyInput.js +73 -0
  75. package/dist/lintAssemblyInput.js.map +1 -0
  76. package/dist/lintAssemblyInstructions.d.ts +29 -0
  77. package/dist/lintAssemblyInstructions.d.ts.map +1 -0
  78. package/dist/lintAssemblyInstructions.js +33 -0
  79. package/dist/lintAssemblyInstructions.js.map +1 -0
  80. package/dist/tus.d.ts +2 -1
  81. package/dist/tus.d.ts.map +1 -1
  82. package/dist/tus.js +2 -1
  83. package/dist/tus.js.map +1 -1
  84. package/package.json +5 -2
  85. package/src/Transloadit.ts +318 -49
  86. package/src/alphalib/assembly-linter.lang.en.ts +393 -0
  87. package/src/alphalib/assembly-linter.ts +1475 -0
  88. package/src/alphalib/mcache.ts +26 -7
  89. package/src/alphalib/object.ts +27 -0
  90. package/src/alphalib/stepParsing.ts +1465 -0
  91. package/src/alphalib/templateMerge.ts +32 -0
  92. package/src/alphalib/types/assemblyStatus.ts +9 -1
  93. package/src/alphalib/types/assemblyUrls.ts +2 -5
  94. package/src/alphalib/types/robots/_index.ts +14 -0
  95. package/src/alphalib/types/robots/_instructions-primitives.ts +1 -0
  96. package/src/alphalib/types/robots/document-optimize.ts +180 -0
  97. package/src/alphalib/typings/json-to-ast.d.ts +34 -0
  98. package/src/cli/commands/assemblies.ts +161 -2
  99. package/src/cli/commands/auth.ts +19 -22
  100. package/src/cli/commands/index.ts +2 -0
  101. package/src/cli/docs/assemblyLintingExamples.ts +9 -0
  102. package/src/cli/helpers.ts +50 -0
  103. package/src/lintAssemblyInput.ts +89 -0
  104. package/src/lintAssemblyInstructions.ts +72 -0
  105. package/src/tus.ts +3 -0
@@ -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
- export interface CreateAssemblyOptions {
81
- params?: CreateAssemblyParams
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 assembly creation and upload.
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
- // Convert uploads to streams
280
- const streamsMap = Object.fromEntries(
281
- Object.entries(uploads).map(([label, value]) => {
282
- const isReadable = isReadableStream(value)
283
- if (!isReadable && isStream(value)) {
284
- // https://github.com/transloadit/node-sdk/issues/92
285
- throw new Error(`Upload named "${label}" is not a Readable stream`)
286
- }
287
-
288
- return [label, isReadableStream(value) ? value : intoStream(value)]
289
- }),
290
- )
291
-
292
- // Wrap in object structure (so we can store whether it's a pathless stream or not)
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
- for (const { stream } of allStreams) {
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 = new Promise<AssemblyStatus>((_resolve, reject) => {
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 this.getAssembly(assemblyId, { signal })
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
- const rawResult = await this._remoteJson<Record<string, unknown>, OptionalAuthParams>({
580
- urlSuffix: `/assemblies/${assemblyId}`,
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: ${assemblyId}.\n${parsedResult.humanReadable}`,
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
- if (
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
- Boolean(body.info.retryIn) &&
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 && body.error === 'RATE_LIMIT_REACHED') || statusCode === 429)
931
- ) {
932
- const { retryIn: retryInSec } = body.info
933
- logWarn(`Rate limit reached, retrying request in approximately ${retryInSec} seconds.`)
934
- const retryInMs = 1000 * (retryInSec * (1 + 0.1 * Math.random()))
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 {