1/**
2 * Deno implementation of the Effect HTTP platform service.
3 *
4 * File responses use `Deno.FsFile.readable` directly so full-file responses
5 * retain Deno's resource-backed fast path. Deno closes the file after the body
6 * is sent, but server adapters must cancel a raw body when they do not send it,
7 * including when handling `HEAD` requests.
8 *
9 * The provided layer uses strong ETags, unlike the portable `HttpPlatform`
10 * layer, which uses weak ETags.
11 *
12 * @since 4.0.0
13 */
14import * as NodeHttpCompression from "@effect/platform-node-shared/NodeHttpCompression"
15import { contentType } from "@std/media-types"
16import { extname } from "@std/path"
17import { ByteSliceStream } from "@std/streams"
18import * as Etag from "effect/http/Etag"
19import * as Platform from "effect/http/HttpPlatform"
20import * as Response from "effect/http/HttpServerResponse"
21import * as Layer from "effect/Layer"
22import * as DenoFileSystem from "./DenoFileSystem.ts"
23
24// gzip and deflate use the native CompressionStream, which does not expose a
25// per-chunk flush control. br and zstd go through node:zlib compatibility
26// streams and flush each input chunk.
27const compression = NodeHttpCompression.make(Platform.makeCompressionWeb({
28 algorithms: NodeHttpCompression.algorithms,
29 transform: (algorithm, options) =>
30 algorithm === "gzip" || algorithm === "deflate"
31 ? Platform.compressionTransformWeb(algorithm)
32 : NodeHttpCompression.compressTransformWeb(algorithm, options)
33}))
34
35/**
36 * Creates the Deno `HttpPlatform`, serving file responses from resource-backed
37 * readable streams and adding content type and content length headers.
38 *
39 * @category constructors
40 * @since 4.0.0
41 */
42export const make = Platform.make({
43 platform: "deno",
44 compression,
45 fileResponse(path, status, statusText, headers, start, end, contentLength) {
46 let body: ReadableStream<Uint8Array>
47 if (contentLength === BigInt(0)) {
48 body = new ReadableStream<Uint8Array>({
49 start(controller) {
50 controller.close()
51 }
52 })
53 } else {
54 const file = Deno.openSync(path)
55 file.seekSync(start, Deno.SeekMode.Start)
56 body = end === undefined
57 ? file.readable
58 : file.readable.pipeThrough(new ByteSliceStream(0, end - start - 1))
59 }
60 return Response.raw(body, {
61 headers: {
62 ...headers,
63 "content-type": headers["content-type"] ?? contentType(extname(path)) ?? "application/octet-stream",
64 "content-length": contentLength.toString()
65 },
66 status,
67 statusText
68 })
69 },
70 fileWebResponse(file, status, statusText, headers, options) {
71 const offset = options?.offset ?? 0
72 const available = Math.max(0, file.size - offset)
73 const contentLength = options?.bytesToRead === undefined
74 ? available
75 : Math.min(available, Math.max(0, options.bytesToRead))
76 let body: typeof file | ReadableStream<Uint8Array> = file
77 if (contentLength === 0) {
78 body = new ReadableStream<Uint8Array>({
79 start(controller) {
80 controller.close()
81 }
82 })
83 } else if (offset > 0 || options?.bytesToRead !== undefined) {
84 body = (file.stream() as ReadableStream<Uint8Array>).pipeThrough(
85 new ByteSliceStream(offset, offset + contentLength - 1)
86 )
87 }
88 return Response.raw(body, {
89 headers: {
90 ...headers,
91 "content-type": headers["content-type"] ??
92 (file.type === "" ? contentType(extname(file.name)) ?? "application/octet-stream" : file.type),
93 "content-length": contentLength.toString()
94 },
95 status,
96 statusText
97 })
98 }
99})
100
101/**
102 * Provides the Deno `HttpPlatform` together with its filesystem and strong ETag
103 * services.
104 *
105 * @category layers
106 * @since 4.0.0
107 */
108export const layer: Layer.Layer<Platform.HttpPlatform> = Layer.effect(Platform.HttpPlatform)(make).pipe(
109 Layer.provide(DenoFileSystem.layer),
110 Layer.provide(Etag.layer)
111)