char/avril

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

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

Charlotte Somport fetch extension to typescriptf2c35fd

main
7.6 KiB256 linesraw
1import {
2  ObjectCreate,
3  TypedArrayPrototypeGetTag as tag,
4  TypeError,
5  Uint8Array,
6  uncurryThis,
7  WeakMapPrototypeSet as set,
8} from "ext:core/primordials";
9import {
10  markPromiseHandled,
11  promiseThen as then,
12  safePromise,
13} from "ext:core/promises";
14import { state } from "ext:web/idl.js";
15import { userAgent } from "ext:web/navigator.js";
16import {
17  op_fetch_cancel,
18  op_fetch_create,
19  op_fetch_read,
20  op_fetch_read_all,
21  op_fetch_send,
22  op_fetch_write,
23} from "ext:core/ops";
24import {
25  enqueue,
26  errorStream,
27  finish,
28  read,
29  ReadableStream,
30  readerCancel,
31  release,
32  streamCancel,
33  streamReader,
34} from "ext:webstreams/streams.js";
35import { addListener, removeListener, signals } from "ext:web/events.js";
36import { Request } from "ext:http_types/request.js";
37import { Response } from "ext:http_types/response.js";
38import {
39  fromPairs,
40  getHeader,
41  headerEntries,
42  headers,
43  setHeader,
44} from "ext:http_types/headers.js";
45import { bodies, bodyCollectors, usable } from "ext:http_types/body.js";
46
47import type { RequestState } from "ext:http_types/body.js";
48import type {
49  ReadableByteStreamController,
50  ReadableStreamDefaultReader,
51} from "ext:webstreams/streams.js";
52import type { FetchMetadata } from "ext:core/ops";
53
54interface FetchResource {
55  id: number;
56  cleanup(): void;
57}
58
59const WeakRef = globalThis.WeakRef,
60  FinalizationRegistry = globalThis.FinalizationRegistry;
61const deref = uncurryThis(
62    (WeakRef.prototype as WeakRef<ReadableByteStreamController>).deref,
63  ),
64  registerFinalizer = uncurryThis(
65    (FinalizationRegistry.prototype as FinalizationRegistry<FetchResource>)
66      .register,
67  );
68function failure(error: unknown): TypeError {
69  // Guest message getters and stringification can abort the request.
70  return new TypeError((error as { message?: string }).message);
71}
72const finalizer = new FinalizationRegistry<FetchResource>(({ id, cleanup }) => {
73  cleanup();
74  op_fetch_cancel(id);
75});
76async function fetch(
77  input: unknown,
78  init: unknown = undefined,
79): Promise<Response> {
80  if (arguments.length === 0) throw new TypeError("fetch requires input");
81  const request = new Request(input, init),
82    s = state(bodies, request) as RequestState,
83    b = s.body;
84  if (state(signals, s.signal).aborted) throw state(signals, s.signal).reason;
85  usable(b);
86  const id = op_fetch_create(null);
87  let controller: WeakRef<ReadableByteStreamController> | undefined,
88    uploadError: TypeError | undefined,
89    uploadReader: ReadableStreamDefaultReader<unknown> | undefined;
90  const aborted = () => {
91    if (uploadReader) {
92      markPromiseHandled(
93        readerCancel(uploadReader, state(signals, s.signal).reason),
94      );
95    }
96    op_fetch_cancel(id);
97    const c = controller && deref(controller);
98    if (c) errorStream(c, state(signals, s.signal).reason);
99  };
100  addListener(s.signal, "abort", aborted, { once: true });
101  const cleanup = () => removeListener(s.signal, "abort", aborted);
102  let pending: Promise<FetchMetadata>;
103  try {
104    if (getHeader(s.headers, "user-agent") === null) {
105      setHeader(s.headers, "user-agent", userAgent);
106    }
107    const entries = headerEntries(s.headers);
108    b.used = b.stream !== null;
109    pending = op_fetch_send([
110      id,
111      s.url,
112      s.method,
113      entries,
114      b.bytes,
115      b.stream !== null && b.bytes === null,
116      s.redirect,
117    ]);
118    if (b.bytes !== null) markPromiseHandled(streamCancel(b.stream));
119    else if (b.stream) {
120      const reader = streamReader(b.stream);
121      uploadReader = reader;
122      then(
123        (async () => {
124          try {
125            while (true) {
126              const result = await safePromise(read(reader));
127              if (result.done) break;
128              if (tag(result.value) !== "Uint8Array") {
129                throw new TypeError("Upload chunks must be Uint8Array");
130              }
131              // The native write still checks shared and resizable backing stores.
132              if (
133                !await safePromise(
134                  op_fetch_write([id, result.value as Uint8Array<ArrayBuffer>]),
135                )
136              ) {
137                try {
138                  await safePromise(readerCancel(reader));
139                } catch {
140                  // The body is no longer wanted; failing cleanup must not fail the response.
141                }
142                return;
143              }
144            }
145            await safePromise(op_fetch_write([id, null]));
146          } finally {
147            release(reader);
148            uploadReader = undefined;
149          }
150        })(),
151        () => {},
152        (error) => {
153          uploadError = failure(error);
154          op_fetch_cancel(id);
155          const c = controller && deref(controller);
156          if (c) errorStream(c, uploadError);
157        },
158      );
159    }
160    const reply = await safePromise(pending);
161    const status = reply[0],
162      statusText = reply[1],
163      list = reply[2],
164      url = reply[3],
165      redirected = reply[4],
166      abandoned = reply[5];
167    // The upload loop only notices on its next write, which may never come in time.
168    if (abandoned && uploadReader) {
169      markPromiseHandled(readerCancel(uploadReader));
170    }
171    if (state(signals, s.signal).aborted) {
172      throw state(signals, s.signal).reason;
173    }
174    const h = fromPairs(list);
175    state(headers, h).immutable = true;
176    let stream: ReadableStream<Uint8Array<ArrayBuffer>> | null = null;
177    if (
178      s.method === "HEAD" || status === 204 || status === 205 ||
179      status === 304
180    ) {
181      cleanup();
182      op_fetch_cancel(id);
183    } else {
184      let collecting = false;
185      stream = new ReadableStream<Uint8Array<ArrayBuffer>>({
186        type: "bytes",
187        start(c) {
188          controller = new WeakRef(c);
189        },
190        async pull(c) {
191          try {
192            const bytes = await safePromise(
193              collecting ? op_fetch_read_all(id) : op_fetch_read(id),
194            );
195            if (bytes !== null && bytes.length) enqueue(c, bytes);
196            if (bytes === null || collecting) {
197              cleanup();
198              finish(c);
199            }
200          } catch (error) {
201            cleanup();
202            op_fetch_cancel(id);
203            errorStream(
204              c,
205              state(signals, s.signal).aborted
206                ? state(signals, s.signal).reason
207                : failure(error),
208            );
209          }
210        },
211        cancel() {
212          cleanup();
213          op_fetch_cancel(id);
214        },
215      }, { highWaterMark: 0 });
216      // Read through the stream so it still owns disturbance, locking, and errors.
217      set(bodyCollectors, stream, async (reader, convert) => {
218        collecting = true;
219        const result = await safePromise(read(reader));
220        if (!result.done && tag(result.value) !== "Uint8Array") {
221          throw new TypeError("Body stream must contain Uint8Array chunks");
222        }
223        // Guest promise hooks can retain stream chunks; Blob storage must not alias them.
224        return {
225          __proto__: null,
226          value: convert(
227            result.done
228              ? new Uint8Array()
229              : new Uint8Array(result.value as Uint8Array),
230          ),
231        };
232      });
233      registerFinalizer(finalizer, stream, { id, cleanup });
234    }
235    const response = ObjectCreate(Response.prototype) as Response;
236    set(bodies, response, {
237      kind: "response",
238      body: { stream, bytes: null, used: false, type: null },
239      headers: h,
240      status,
241      statusText,
242      url,
243      redirected,
244      type: "basic",
245    });
246    return response;
247  } catch (error) {
248    cleanup();
249    op_fetch_cancel(id);
250    throw state(signals, s.signal).aborted
251      ? state(signals, s.signal).reason
252      : uploadError ?? failure(error);
253  }
254}
255
256export { fetch };