char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somport http extension to typescript495c9a5

main
5.0 KiB175 linesraw
1import {
2  FinalizationRegistry,
3  TypeError,
4  uncurryThis,
5  WeakMap,
6  WeakMapPrototypeGet as get,
7  WeakMapPrototypeSet as set,
8} from "ext:core/primordials";
9import { markPromiseHandled, safePromise } from "ext:core/promises";
10import {
11  op_http_file_close,
12  op_http_file_handle,
13  op_http_file_read,
14  op_http_serve_file,
15} from "ext:core/ops";
16import { dictionary, state } from "ext:web/idl.js";
17import { addListener, removeListener, signals } from "ext:web/events.js";
18import type { AbortSignal } from "ext:web/events.js";
19import {
20  args,
21  detachResponseHandle,
22  path,
23  responseHandle,
24} from "ext:file/main.js";
25import { result } from "ext:file/errors.js";
26import { messageState } from "ext:http_types/body.js";
27import { fromPairs, headerEntries, Headers } from "ext:http_types/headers.js";
28import { Response } from "ext:http_types/response.js";
29import {
30  enqueue,
31  errorStream,
32  finish,
33  ReadableStream,
34  streamCancel,
35} from "ext:webstreams/streams.js";
36import type { ReadableByteStreamController } from "ext:webstreams/streams.js";
37
38interface FileData {
39  id: number | null;
40  signal: AbortSignal;
41  owner: ReadableStream<Uint8Array<ArrayBuffer>> | null;
42  onAbort: (() => void) | null;
43  reading: boolean;
44}
45
46const files = new WeakMap<ReadableStream<unknown>, FileData>(),
47  registry = new FinalizationRegistry<number>((id) => op_http_file_close(id)),
48  register = uncurryThis(
49    (FinalizationRegistry.prototype as FinalizationRegistry<number>).register,
50  ),
51  unregister = uncurryThis(FinalizationRegistry.prototype.unregister);
52
53function close(data: FileData) {
54  if (data.id === null) return;
55  const id = data.id;
56  data.id = null;
57  unregister(registry, data.owner!);
58  removeListener(data.signal, "abort", data.onAbort);
59  op_http_file_close(id);
60}
61
62function takeFile(
63  stream: ReadableStream<unknown>,
64  signal: AbortSignal,
65): number | null {
66  const data = get(files, stream);
67  if (!data || data.id === null || data.reading || data.signal !== signal) {
68    return null;
69  }
70  const id = data.id;
71  data.id = null;
72  unregister(registry, data.owner!);
73  removeListener(signal, "abort", data.onAbort);
74  // Disturb the JS body without canceling the native body being transferred.
75  markPromiseHandled(streamCancel(stream));
76  return id;
77}
78
79async function serveFile(
80  request: unknown,
81  source: unknown,
82  options: unknown = undefined,
83): Promise<Response> {
84  const req = messageState(request, "request"),
85    handle = responseHandle(source),
86    filename = handle === null ? path(source) : null,
87    settings = dictionary(options),
88    headers = new Headers(settings.headers),
89    consume = settings.consume,
90    signal = req.signal,
91    aborted = state(signals, signal);
92  if (consume !== undefined && typeof consume !== "boolean") {
93    throw new TypeError("consume must be boolean");
94  }
95  if (aborted.aborted) throw aborted.reason;
96  const requestHeaders = headerEntries(req.headers),
97    extraHeaders = headerEntries(headers);
98  let nativeSource: string | number | null = filename;
99  if (handle !== null) {
100    nativeSource = result(op_http_file_handle(args(handle, consume !== false)));
101    if (consume !== false) detachResponseHandle(source);
102  }
103  const prepared = result(
104    await safePromise(op_http_serve_file(args(
105      req.method,
106      requestHeaders,
107      nativeSource!,
108      extraHeaders,
109    ))),
110  );
111  const id = prepared[2];
112  if (aborted.aborted) {
113    if (id !== null) op_http_file_close(id);
114    throw aborted.reason;
115  }
116  const data: FileData = {
117    id,
118    signal,
119    owner: null,
120    onAbort: null,
121    reading: false,
122  };
123  try {
124    const init = {
125      __proto__: null,
126      status: prepared[0],
127      headers: fromPairs(prepared[1]),
128    };
129    if (id === null) return new Response(null, init);
130    let controller: ReadableByteStreamController;
131    const stream = new ReadableStream<Uint8Array<ArrayBuffer>>({
132      __proto__: null,
133      type: "bytes",
134      start(value) {
135        controller = value;
136      },
137      async pull(controller) {
138        data.reading = true;
139        try {
140          const chunk = result(await safePromise(op_http_file_read(data.id!)));
141          if (data.id === null) return;
142          enqueue(controller, chunk[0]);
143          if (chunk[1]) {
144            finish(controller);
145            close(data);
146          }
147        } catch (error) {
148          if (data.id !== null) errorStream(controller, error);
149          close(data);
150        }
151      },
152      cancel() {
153        close(data);
154      },
155    }, { __proto__: null, highWaterMark: 0 });
156    data.owner = stream;
157    data.onAbort = () => {
158      errorStream(controller, aborted.reason);
159      close(data);
160    };
161    set(files, stream, data);
162    register(registry, stream, id, stream);
163    addListener(signal, "abort", data.onAbort, { once: true });
164    const response = new Response(stream, init);
165    if (aborted.aborted) throw aborted.reason;
166    return response;
167  } catch (error) {
168    if (data.owner === null) {
169      if (id !== null) op_http_file_close(id);
170    } else close(data);
171    throw error;
172  }
173}
174
175export { serveFile, takeFile };