From cfc4f6afe82219032343c3168b6fe08278eeaff7 Mon Sep 17 00:00:00 2001 From: Sebastian Lorenz Date: Tue, 4 Aug 2026 13:51:15 +0000 Subject: [PATCH 1/3] Apply byte-range options to default Web-file responses --- .../test/unstable/http/HttpPlatform.test.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 packages/effect/test/unstable/http/HttpPlatform.test.ts diff --git a/packages/effect/test/unstable/http/HttpPlatform.test.ts b/packages/effect/test/unstable/http/HttpPlatform.test.ts new file mode 100644 index 00000000000..2e806498bea --- /dev/null +++ b/packages/effect/test/unstable/http/HttpPlatform.test.ts @@ -0,0 +1,25 @@ +import { assert, describe, it } from "@effect/vitest" +import { Effect, FileSystem, Stream } from "effect" +import { HttpPlatform } from "effect/unstable/http" + +describe("HttpPlatform", () => { + it.effect("honors Web file offset and byte count", () => + Effect.gen(function*() { + const platform = yield* HttpPlatform.HttpPlatform + const response = yield* platform.fileWebResponse({ + name: "file.bin", + lastModified: 0, + size: 4, + type: "application/octet-stream", + stream: () => new Blob([new Uint8Array([1, 2, 3, 4])]).stream() + }, { offset: 1, bytesToRead: 2 }) + assert.strictEqual(response.body._tag, "Stream") + if (response.body._tag === "Stream") { + const bytes = yield* Stream.mkUint8Array(response.body.stream) + assert.deepStrictEqual(Array.from(bytes), [2, 3]) + } + }).pipe( + Effect.provide(HttpPlatform.layer), + Effect.provideService(FileSystem.FileSystem, {} as any) + )) +}) From e6d5f6463668edb144a3dce8a6a3f3450a48f364 Mon Sep 17 00:00:00 2001 From: Tim Smart Date: Tue, 4 Aug 2026 22:08:24 +0000 Subject: [PATCH 2/3] Fix Web file response ranges --- .changeset/curly-files-range.md | 5 +++ .../effect/src/unstable/http/HttpPlatform.ts | 43 ++++++++++++++++--- .../test/unstable/http/HttpPlatform.test.ts | 22 ++++++++++ 3 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 .changeset/curly-files-range.md diff --git a/.changeset/curly-files-range.md b/.changeset/curly-files-range.md new file mode 100644 index 00000000000..aa2a20ade46 --- /dev/null +++ b/.changeset/curly-files-range.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Apply byte range and chunk size options to default Web file responses. diff --git a/packages/effect/src/unstable/http/HttpPlatform.ts b/packages/effect/src/unstable/http/HttpPlatform.ts index ad4d07b9e60..94b002c1b01 100644 --- a/packages/effect/src/unstable/http/HttpPlatform.ts +++ b/packages/effect/src/unstable/http/HttpPlatform.ts @@ -156,13 +156,44 @@ export const layer = Layer.effect(HttpPlatform)( { contentLength, headers, status, statusText } ) }, - fileWebResponse(file, status, statusText, headers, _options) { + fileWebResponse(file, status, statusText, headers, options) { + const offset = Number(options?.offset ?? 0) + const bytesToRead = options?.bytesToRead !== undefined ? Number(options.bytesToRead) : undefined + const chunkSize = options?.chunkSize !== undefined ? Math.max(1, Number(options.chunkSize)) : Infinity + const contentLength = bytesToRead ?? file.size - offset + const source = Stream.fromReadableStream({ + evaluate: () => file.stream() as ReadableStream, + onError: identity + }) + const stream = bytesToRead !== undefined && bytesToRead <= 0 + ? Stream.empty + : source.pipe( + Stream.mapAccum( + () => ({ offset, remaining: bytesToRead }), + (state, bytes) => { + const start = Math.min(state.offset, bytes.length) + const offset = state.offset - start + const length = state.remaining === undefined + ? bytes.length - start + : Math.min(state.remaining, bytes.length - start) + const end = start + length + const remaining = state.remaining === undefined ? undefined : state.remaining - length + const chunks: Array<{ readonly bytes: Uint8Array; readonly done: boolean }> = [] + for (let index = start; index < end; index += chunkSize) { + chunks.push({ + bytes: bytes.subarray(index, Math.min(index + chunkSize, end)), + done: remaining === 0 && index + chunkSize >= end + }) + } + return [{ offset, remaining }, chunks] as const + } + ), + Stream.takeUntil((chunk) => chunk.done), + Stream.map((chunk) => chunk.bytes) + ) return Response.stream( - Stream.fromReadableStream({ - evaluate: () => file.stream() as ReadableStream, - onError: identity - }), - { headers, status, statusText } + stream, + { contentLength, headers, status, statusText } ) } })) diff --git a/packages/effect/test/unstable/http/HttpPlatform.test.ts b/packages/effect/test/unstable/http/HttpPlatform.test.ts index 2e806498bea..63c5e87e55f 100644 --- a/packages/effect/test/unstable/http/HttpPlatform.test.ts +++ b/packages/effect/test/unstable/http/HttpPlatform.test.ts @@ -15,6 +15,7 @@ describe("HttpPlatform", () => { }, { offset: 1, bytesToRead: 2 }) assert.strictEqual(response.body._tag, "Stream") if (response.body._tag === "Stream") { + assert.strictEqual(response.body.contentLength, 2) const bytes = yield* Stream.mkUint8Array(response.body.stream) assert.deepStrictEqual(Array.from(bytes), [2, 3]) } @@ -22,4 +23,25 @@ describe("HttpPlatform", () => { Effect.provide(HttpPlatform.layer), Effect.provideService(FileSystem.FileSystem, {} as any) )) + + it.effect("honors Web file chunk size", () => + Effect.gen(function*() { + const platform = yield* HttpPlatform.HttpPlatform + const response = yield* platform.fileWebResponse({ + name: "file.bin", + lastModified: 0, + size: 4, + type: "application/octet-stream", + stream: () => new Blob([new Uint8Array([1, 2, 3, 4])]).stream() + }, { offset: 0, bytesToRead: 4, chunkSize: 2 }) + assert.strictEqual(response.body._tag, "Stream") + if (response.body._tag === "Stream") { + assert.strictEqual(response.body.contentLength, 4) + const chunks = yield* Stream.runCollect(response.body.stream) + assert.deepStrictEqual(chunks.map((chunk) => Array.from(chunk)), [[1, 2], [3, 4]]) + } + }).pipe( + Effect.provide(HttpPlatform.layer), + Effect.provideService(FileSystem.FileSystem, {} as any) + )) }) From a02ada8fc69c87ffc50497aef62ed9f205231cad Mon Sep 17 00:00:00 2001 From: Tim Smart Date: Tue, 4 Aug 2026 23:01:01 +0000 Subject: [PATCH 3/3] Simplify Web file range slicing Co-Authored-By: Claude Fable 5 --- .../effect/src/unstable/http/HttpPlatform.ts | 45 +++++++++---------- .../test/unstable/http/HttpPlatform.test.ts | 24 +++++----- 2 files changed, 31 insertions(+), 38 deletions(-) diff --git a/packages/effect/src/unstable/http/HttpPlatform.ts b/packages/effect/src/unstable/http/HttpPlatform.ts index 94b002c1b01..c6d056696e6 100644 --- a/packages/effect/src/unstable/http/HttpPlatform.ts +++ b/packages/effect/src/unstable/http/HttpPlatform.ts @@ -160,41 +160,38 @@ export const layer = Layer.effect(HttpPlatform)( const offset = Number(options?.offset ?? 0) const bytesToRead = options?.bytesToRead !== undefined ? Number(options.bytesToRead) : undefined const chunkSize = options?.chunkSize !== undefined ? Math.max(1, Number(options.chunkSize)) : Infinity - const contentLength = bytesToRead ?? file.size - offset - const source = Stream.fromReadableStream({ - evaluate: () => file.stream() as ReadableStream, - onError: identity - }) - const stream = bytesToRead !== undefined && bytesToRead <= 0 + const end = offset + (bytesToRead ?? Infinity) + const stream = end <= offset ? Stream.empty - : source.pipe( + : Stream.fromReadableStream({ + evaluate: () => file.stream() as ReadableStream, + onError: identity + }).pipe( Stream.mapAccum( - () => ({ offset, remaining: bytesToRead }), - (state, bytes) => { - const start = Math.min(state.offset, bytes.length) - const offset = state.offset - start - const length = state.remaining === undefined - ? bytes.length - start - : Math.min(state.remaining, bytes.length - start) - const end = start + length - const remaining = state.remaining === undefined ? undefined : state.remaining - length + () => 0, + (position, bytes) => { + const next = position + bytes.length + const start = Math.min(Math.max(offset - position, 0), bytes.length) + const stop = Math.min(Math.max(end - position, 0), bytes.length) const chunks: Array<{ readonly bytes: Uint8Array; readonly done: boolean }> = [] - for (let index = start; index < end; index += chunkSize) { + for (let index = start; index < stop; index += chunkSize) { chunks.push({ - bytes: bytes.subarray(index, Math.min(index + chunkSize, end)), - done: remaining === 0 && index + chunkSize >= end + bytes: bytes.subarray(index, Math.min(index + chunkSize, stop)), + done: next >= end && index + chunkSize >= stop }) } - return [{ offset, remaining }, chunks] as const + return [next, chunks] } ), Stream.takeUntil((chunk) => chunk.done), Stream.map((chunk) => chunk.bytes) ) - return Response.stream( - stream, - { contentLength, headers, status, statusText } - ) + return Response.stream(stream, { + contentLength: bytesToRead ?? file.size - offset, + headers, + status, + statusText + }) } })) ).pipe(Layer.provide(Etag.layerWeak)) diff --git a/packages/effect/test/unstable/http/HttpPlatform.test.ts b/packages/effect/test/unstable/http/HttpPlatform.test.ts index 63c5e87e55f..f5ae0cab4ac 100644 --- a/packages/effect/test/unstable/http/HttpPlatform.test.ts +++ b/packages/effect/test/unstable/http/HttpPlatform.test.ts @@ -3,16 +3,18 @@ import { Effect, FileSystem, Stream } from "effect" import { HttpPlatform } from "effect/unstable/http" describe("HttpPlatform", () => { + const file = { + name: "file.bin", + lastModified: 0, + size: 4, + type: "application/octet-stream", + stream: () => new Blob([new Uint8Array([1, 2, 3, 4])]).stream() + } + it.effect("honors Web file offset and byte count", () => Effect.gen(function*() { const platform = yield* HttpPlatform.HttpPlatform - const response = yield* platform.fileWebResponse({ - name: "file.bin", - lastModified: 0, - size: 4, - type: "application/octet-stream", - stream: () => new Blob([new Uint8Array([1, 2, 3, 4])]).stream() - }, { offset: 1, bytesToRead: 2 }) + const response = yield* platform.fileWebResponse(file, { offset: 1, bytesToRead: 2 }) assert.strictEqual(response.body._tag, "Stream") if (response.body._tag === "Stream") { assert.strictEqual(response.body.contentLength, 2) @@ -27,13 +29,7 @@ describe("HttpPlatform", () => { it.effect("honors Web file chunk size", () => Effect.gen(function*() { const platform = yield* HttpPlatform.HttpPlatform - const response = yield* platform.fileWebResponse({ - name: "file.bin", - lastModified: 0, - size: 4, - type: "application/octet-stream", - stream: () => new Blob([new Uint8Array([1, 2, 3, 4])]).stream() - }, { offset: 0, bytesToRead: 4, chunkSize: 2 }) + const response = yield* platform.fileWebResponse(file, { offset: 0, bytesToRead: 4, chunkSize: 2 }) assert.strictEqual(response.body._tag, "Stream") if (response.body._tag === "Stream") { assert.strictEqual(response.body.contentLength, 4)