| | @@ -306,44 +306,39 @@ |
| 306 | 306 | return out; |
| 307 | 307 | } |
| 308 | 308 | |
| 309 | | -/** The concatenation of `slices`, read one after another. */ |
| 310 | | -function concat(bucket: BlobBucket, slices: Slice[]): ReadableStream { |
| 311 | | - let i = 0; |
| 312 | | - let reader: ReadableStreamDefaultReader<Uint8Array> | null = null; |
| 313 | | - return new ReadableStream<Uint8Array>({ |
| 314 | | - async pull(ctl) { |
| 315 | | - for (;;) { |
| 316 | | - if (!reader) { |
| 317 | | - const s = slices[i++]; |
| 318 | | - if (!s) return ctl.close(); |
| 319 | | - const o = await bucket.get(s.key, s.whole ? undefined : { range: { offset: s.offset, length: s.length } }); |
| 320 | | - if (!o) throw new Error(`staged block ${s.key} is missing`); |
| 321 | | - reader = o.body.getReader() as ReadableStreamDefaultReader<Uint8Array>; |
| 322 | | - } |
| 323 | | - const { done, value } = await reader.read(); |
| 324 | | - if (done) { |
| 325 | | - reader = null; |
| 326 | | - continue; |
| 327 | | - } |
| 328 | | - ctl.enqueue(value); |
| 329 | | - return; |
| 309 | +/** |
| 310 | + * The concatenation of `slices` (`length` bytes), read one after another. Each slice's R2 body is |
| 311 | + * piped into one fixed-length stream, which the runtime does natively: a JavaScript loop over the |
| 312 | + * chunks kept a Durable Object's CPU busy, so eight steps at once ran little faster than one, and |
| 313 | + * the Durable Object answered some of them with errors when it was overloaded. |
| 314 | + */ |
| 315 | +function concat(bucket: BlobBucket, slices: Slice[], length: number): ReadableStream { |
| 316 | + const F = (globalThis as { FixedLengthStream?: new (n: number) => TransformStream }).FixedLengthStream; |
| 317 | + const t: TransformStream = F ? new F(length) : new TransformStream(); |
| 318 | + void (async () => { |
| 319 | + try { |
| 320 | + for (const s of slices) { |
| 321 | + const o = await bucket.get(s.key, s.whole ? undefined : { range: { offset: s.offset, length: s.length } }); |
| 322 | + if (!o) throw new Error(`staged block ${s.key} is missing`); |
| 323 | + await o.body.pipeTo(t.writable, { preventClose: true }); |
| 330 | 324 | } |
| 331 | | - }, |
| 332 | | - async cancel() { |
| 333 | | - await reader?.cancel().catch(() => {}); |
| 334 | | - }, |
| 335 | | - }); |
| 325 | + await t.writable.close(); |
| 326 | + } catch (e) { |
| 327 | + await t.writable.abort(e).catch(() => {}); |
| 328 | + } |
| 329 | + })(); |
| 330 | + return t.readable; |
| 336 | 331 | } |
| 337 | 332 | |
| 338 | 333 | /** Runs one step against the bucket. Returns the parts it uploaded. */ |
| 339 | 334 | export async function runComposeStep(bucket: BlobBucket, step: ComposeStep, contentType?: string): Promise<Array<{ partNumber: number; etag: string }>> { |
| 340 | 335 | if (step.kind === "merge") { |
| 341 | | - await bucket.put(step.target, sized(concat(bucket, step.slices), step.length), contentType ? { httpMetadata: { contentType } } : undefined); |
| 336 | + await bucket.put(step.target, concat(bucket, step.slices, step.length), contentType ? { httpMetadata: { contentType } } : undefined); |
| 342 | 337 | return []; |
| 343 | 338 | } |
| 344 | 339 | const upload = bucket.resumeMultipartUpload(step.key, step.uploadId); |
| 345 | 340 | const out: Array<{ partNumber: number; etag: string }> = []; |
| 346 | | - for (const p of step.parts) out.push(await upload.uploadPart(p.partNumber, sized(concat(bucket, p.slices), p.length))); |
| 341 | + for (const p of step.parts) out.push(await upload.uploadPart(p.partNumber, concat(bucket, p.slices, p.length))); |
| 347 | 342 | return out; |
| 348 | 343 | } |
| 349 | 344 | |