char/avril

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

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

Charlotte Somport compression extension to typescriptec742cf

main
7.8 KiB230 linesraw
1import { safePromise } from "ext:core/promises";
2import {
3  ArrayBuffer,
4  ArrayBufferPrototypeGetResizable as resizable,
5  getter,
6  ObjectGetOwnPropertyDescriptor as descriptor,
7  ReflectApply as apply,
8  Set,
9  SetPrototypeHas as has,
10  TypedArrayPrototypeGetBuffer as typedArrayBuffer,
11  TypedArrayPrototypeGetByteLength as typedArrayLength,
12  TypedArrayPrototypeGetByteOffset as typedArrayOffset,
13  TypeError,
14  Uint8Array,
15  uncurryThis,
16  WeakMap,
17  WeakMapPrototypeSet as set,
18} from "ext:core/primordials";
19import {
20  op_compression_dispose as dispose,
21  op_compression_finish as finishCodec,
22  op_compression_new as newCodec,
23  op_compression_transform as transformCodec,
24} from "ext:core/ops";
25import { installInterface } from "ext:web/interface.js";
26import { required, state, string } from "ext:web/idl.js";
27import { registerUncloneable } from "ext:web/serialization.js";
28import {
29  ReadableStream,
30  TransformStream,
31  TransformStreamDefaultController,
32  WritableStream,
33} from "ext:webstreams/streams.js";
34
35import type {
36  ReadableStreamDefaultReader as Reader,
37  ReadableStreamDefaultController as ReadableController,
38} from "ext:webstreams/streams.js";
39
40type Bytes = Uint8Array<ArrayBuffer>;
41interface CodecState {
42  id: number | null;
43  owner: object;
44  stream: TransformStream<unknown, Bytes> | null;
45  readable?: ReadableStream<Bytes>;
46  writable?: WritableStream<unknown>;
47}
48
49const isView = ArrayBuffer.isView;
50const arrayBufferLength = getter(ArrayBuffer.prototype, "byteLength");
51const dataViewBuffer = getter(DataView.prototype, "buffer");
52const dataViewLength = getter(DataView.prototype, "byteLength");
53const dataViewOffset = getter(DataView.prototype, "byteOffset");
54const enqueue: (controller: TransformStreamDefaultController<Bytes>, chunk: Bytes) => void =
55  uncurryThis(TransformStreamDefaultController.prototype.enqueue);
56const readable = descriptor(TransformStream.prototype, "readable")!.get!;
57const writable = descriptor(TransformStream.prototype, "writable")!.get!;
58const getReader = uncurryThis((ReadableStream.prototype as ReadableStream<Bytes>).getReader);
59const read = uncurryThis((ReadableStreamDefaultReader.prototype as Reader<Bytes>).read);
60const readerCancel = uncurryThis(ReadableStreamDefaultReader.prototype.cancel);
61const enqueueReadable: (controller: ReadableController<Bytes>, chunk: Bytes) => void = uncurryThis(
62  ReadableStreamDefaultController.prototype.enqueue,
63);
64const closeReadable = uncurryThis(
65  ReadableStreamDefaultController.prototype.close,
66);
67const getWriter = uncurryThis(WritableStream.prototype.getWriter);
68const write = uncurryThis(WritableStreamDefaultWriter.prototype.write);
69const closeWriter = uncurryThis(WritableStreamDefaultWriter.prototype.close);
70const abortWriter = uncurryThis(WritableStreamDefaultWriter.prototype.abort);
71const register: (registry: FinalizationRegistry<number>, target: WeakKey, id: number) => void =
72  uncurryThis(FinalizationRegistry.prototype.register);
73const compressionStates = new WeakMap<object, Required<CodecState>>(),
74  decompressionStates = new WeakMap<object, Required<CodecState>>();
75registerUncloneable(compressionStates);
76registerUncloneable(decompressionStates);
77const finalizer = new FinalizationRegistry<number>((id) => dispose(id));
78const formats = new Set(["deflate", "deflate-raw", "gzip", "brotli"]);
79
80function bufferSource(value: unknown): Bytes {
81  let buffer, offset = 0, length;
82  if (isView(value)) {
83    try {
84      buffer = typedArrayBuffer(value);
85      offset = typedArrayOffset(value);
86      length = typedArrayLength(value);
87    } catch {
88      try {
89        buffer = dataViewBuffer(value);
90        offset = dataViewOffset(value);
91        length = dataViewLength(value);
92      } catch {
93        throw new TypeError("Expected an attached BufferSource");
94      }
95    }
96  } else {
97    buffer = value;
98    try {
99      length = arrayBufferLength(buffer);
100    } catch {
101      throw new TypeError("Expected a non-shared BufferSource");
102    }
103  }
104  try {
105    arrayBufferLength(buffer);
106  } catch {
107    throw new TypeError("Expected a non-shared BufferSource");
108  }
109  try {
110    const bytes = new Uint8Array(buffer as ArrayBuffer, offset, length);
111    return resizable(buffer) ? new Uint8Array(bytes) : bytes;
112  } catch {
113    throw new TypeError("Expected an attached BufferSource");
114  }
115}
116
117function disposeState(internal: CodecState): void {
118  const id = internal.id;
119  if (id !== null) {
120    internal.id = null;
121    dispose(id);
122  }
123}
124
125function create(receiver: object, rawFormat: unknown, decompress: boolean, states: WeakMap<object, Required<CodecState>>): void {
126  const format = string(rawFormat);
127  if (!has(formats, format)) {
128    throw new TypeError("Unsupported compression format");
129  }
130  const id = newCodec([format, decompress]);
131  const internal: CodecState = { id, owner: receiver, stream: null };
132  const stream = new TransformStream<unknown, Bytes>({
133    async transform(chunk, controller) {
134      try {
135        const input = bufferSource(chunk);
136        // Cancellation can clear the handle; preserve the native rejection/catch path.
137        const result = await safePromise(transformCodec([internal.id!, input]));
138        const output = result[0];
139        if (output.byteLength !== 0) enqueue(controller, output);
140        if (result[1]) {
141          throw new TypeError("Invalid or unsupported compressed data");
142        }
143      } catch {
144        disposeState(internal);
145        throw new TypeError("Invalid or unsupported compressed data");
146      }
147    },
148    async flush(controller) {
149      if (internal.id === null) return;
150      try {
151        const result = await safePromise(finishCodec(internal.id));
152        const output = result[0];
153        if (output.byteLength !== 0) enqueue(controller, output);
154      } catch {
155        disposeState(internal);
156        throw new TypeError("Invalid or truncated compressed data");
157      }
158      disposeState(internal);
159    },
160    cancel() {
161      disposeState(internal);
162    },
163  });
164  const outputReader = getReader(apply(readable, stream, []) as ReadableStream<Bytes>);
165  const inputWriter = getWriter(apply(writable, stream, []) as WritableStream<unknown>);
166  internal.readable = new ReadableStream<Bytes>({
167    async pull(controller) {
168      try {
169        const result = await safePromise(read(outputReader));
170        if (result.done) closeReadable(controller);
171        else enqueueReadable(controller, result.value);
172      } catch (error) {
173        disposeState(internal);
174        throw error;
175      }
176    },
177    cancel(reason) {
178      disposeState(internal);
179      return readerCancel(outputReader, reason);
180    },
181  });
182  internal.writable = new WritableStream<unknown>({
183    write(chunk) {
184      return write(inputWriter, chunk);
185    },
186    close() {
187      return closeWriter(inputWriter);
188    },
189    abort(reason) {
190      disposeState(internal);
191      return abortWriter(inputWriter, reason);
192    },
193  });
194  internal.stream = stream;
195  // Publish the brand only after both stream endpoints have been installed.
196  set(states, receiver, internal as Required<CodecState>);
197  register(finalizer, receiver, id);
198}
199
200class CompressionStream {
201  constructor(format: unknown) {
202    required(arguments.length, 1);
203    create(this, format, false, compressionStates);
204  }
205  get readable(): ReadableStream<Bytes> {
206    return state(compressionStates, this).readable;
207  }
208  get writable(): WritableStream<unknown> {
209    return state(compressionStates, this).writable;
210  }
211}
212
213class DecompressionStream {
214  constructor(format: unknown) {
215    required(arguments.length, 1);
216    create(this, format, true, decompressionStates);
217  }
218  get readable(): ReadableStream<Bytes> {
219    return state(decompressionStates, this).readable;
220  }
221  get writable(): WritableStream<unknown> {
222    return state(decompressionStates, this).writable;
223  }
224}
225
226export type { CompressionStream, DecompressionStream };
227
228for (const ctor of [CompressionStream, DecompressionStream]) {
229  installInterface(ctor.name, ctor);
230}