Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/curly-files-range.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": patch
---

Apply byte range and chunk size options to default Web file responses.
40 changes: 34 additions & 6 deletions packages/effect/src/unstable/http/HttpPlatform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,14 +156,42 @@ export const layer = Layer.effect(HttpPlatform)(
{ contentLength, headers, status, statusText }
)
},
fileWebResponse(file, status, statusText, headers, _options) {
return Response.stream(
Stream.fromReadableStream({
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 end = offset + (bytesToRead ?? Infinity)
const stream = end <= offset
? Stream.empty
: Stream.fromReadableStream({
evaluate: () => file.stream() as ReadableStream<Uint8Array>,
onError: identity
}),
{ headers, status, statusText }
)
}).pipe(
Stream.mapAccum(
() => 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 < stop; index += chunkSize) {
chunks.push({
bytes: bytes.subarray(index, Math.min(index + chunkSize, stop)),
done: next >= end && index + chunkSize >= stop
})
}
return [next, chunks]
}
),
Stream.takeUntil((chunk) => chunk.done),
Stream.map((chunk) => chunk.bytes)
)
return Response.stream(stream, {
contentLength: bytesToRead ?? file.size - offset,
headers,
status,
statusText
})
}
}))
).pipe(Layer.provide(Etag.layerWeak))
43 changes: 43 additions & 0 deletions packages/effect/test/unstable/http/HttpPlatform.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { assert, describe, it } from "@effect/vitest"
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(file, { 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])
}
}).pipe(
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(file, { 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)
))
})
Loading