import { safePromise } from "ext:core/promises"; import { ArrayBuffer, ArrayBufferPrototypeGetResizable as resizable, getter, ObjectGetOwnPropertyDescriptor as descriptor, ReflectApply as apply, Set, SetPrototypeHas as has, TypedArrayPrototypeGetBuffer as typedArrayBuffer, TypedArrayPrototypeGetByteLength as typedArrayLength, TypedArrayPrototypeGetByteOffset as typedArrayOffset, TypeError, Uint8Array, uncurryThis, WeakMap, WeakMapPrototypeSet as set, } from "ext:core/primordials"; import { op_compression_dispose as dispose, op_compression_finish as finishCodec, op_compression_new as newCodec, op_compression_transform as transformCodec, } from "ext:core/ops"; import { installInterface } from "ext:web/interface.js"; import { required, state, string } from "ext:web/idl.js"; import { registerUncloneable } from "ext:web/serialization.js"; import { ReadableStream, TransformStream, TransformStreamDefaultController, WritableStream, } from "ext:webstreams/streams.js"; import type { ReadableStreamDefaultReader as Reader, ReadableStreamDefaultController as ReadableController, } from "ext:webstreams/streams.js"; type Bytes = Uint8Array; interface CodecState { id: number | null; owner: object; stream: TransformStream | null; readable?: ReadableStream; writable?: WritableStream; } const isView = ArrayBuffer.isView; const arrayBufferLength = getter(ArrayBuffer.prototype, "byteLength"); const dataViewBuffer = getter(DataView.prototype, "buffer"); const dataViewLength = getter(DataView.prototype, "byteLength"); const dataViewOffset = getter(DataView.prototype, "byteOffset"); const enqueue: (controller: TransformStreamDefaultController, chunk: Bytes) => void = uncurryThis(TransformStreamDefaultController.prototype.enqueue); const readable = descriptor(TransformStream.prototype, "readable")!.get!; const writable = descriptor(TransformStream.prototype, "writable")!.get!; const getReader = uncurryThis((ReadableStream.prototype as ReadableStream).getReader); const read = uncurryThis((ReadableStreamDefaultReader.prototype as Reader).read); const readerCancel = uncurryThis(ReadableStreamDefaultReader.prototype.cancel); const enqueueReadable: (controller: ReadableController, chunk: Bytes) => void = uncurryThis( ReadableStreamDefaultController.prototype.enqueue, ); const closeReadable = uncurryThis( ReadableStreamDefaultController.prototype.close, ); const getWriter = uncurryThis(WritableStream.prototype.getWriter); const write = uncurryThis(WritableStreamDefaultWriter.prototype.write); const closeWriter = uncurryThis(WritableStreamDefaultWriter.prototype.close); const abortWriter = uncurryThis(WritableStreamDefaultWriter.prototype.abort); const register: (registry: FinalizationRegistry, target: WeakKey, id: number) => void = uncurryThis(FinalizationRegistry.prototype.register); const compressionStates = new WeakMap>(), decompressionStates = new WeakMap>(); registerUncloneable(compressionStates); registerUncloneable(decompressionStates); const finalizer = new FinalizationRegistry((id) => dispose(id)); const formats = new Set(["deflate", "deflate-raw", "gzip", "brotli"]); function bufferSource(value: unknown): Bytes { let buffer, offset = 0, length; if (isView(value)) { try { buffer = typedArrayBuffer(value); offset = typedArrayOffset(value); length = typedArrayLength(value); } catch { try { buffer = dataViewBuffer(value); offset = dataViewOffset(value); length = dataViewLength(value); } catch { throw new TypeError("Expected an attached BufferSource"); } } } else { buffer = value; try { length = arrayBufferLength(buffer); } catch { throw new TypeError("Expected a non-shared BufferSource"); } } try { arrayBufferLength(buffer); } catch { throw new TypeError("Expected a non-shared BufferSource"); } try { const bytes = new Uint8Array(buffer as ArrayBuffer, offset, length); return resizable(buffer) ? new Uint8Array(bytes) : bytes; } catch { throw new TypeError("Expected an attached BufferSource"); } } function disposeState(internal: CodecState): void { const id = internal.id; if (id !== null) { internal.id = null; dispose(id); } } function create(receiver: object, rawFormat: unknown, decompress: boolean, states: WeakMap>): void { const format = string(rawFormat); if (!has(formats, format)) { throw new TypeError("Unsupported compression format"); } const id = newCodec([format, decompress]); const internal: CodecState = { id, owner: receiver, stream: null }; const stream = new TransformStream({ async transform(chunk, controller) { try { const input = bufferSource(chunk); // Cancellation can clear the handle; preserve the native rejection/catch path. const result = await safePromise(transformCodec([internal.id!, input])); const output = result[0]; if (output.byteLength !== 0) enqueue(controller, output); if (result[1]) { throw new TypeError("Invalid or unsupported compressed data"); } } catch { disposeState(internal); throw new TypeError("Invalid or unsupported compressed data"); } }, async flush(controller) { if (internal.id === null) return; try { const result = await safePromise(finishCodec(internal.id)); const output = result[0]; if (output.byteLength !== 0) enqueue(controller, output); } catch { disposeState(internal); throw new TypeError("Invalid or truncated compressed data"); } disposeState(internal); }, cancel() { disposeState(internal); }, }); const outputReader = getReader(apply(readable, stream, []) as ReadableStream); const inputWriter = getWriter(apply(writable, stream, []) as WritableStream); internal.readable = new ReadableStream({ async pull(controller) { try { const result = await safePromise(read(outputReader)); if (result.done) closeReadable(controller); else enqueueReadable(controller, result.value); } catch (error) { disposeState(internal); throw error; } }, cancel(reason) { disposeState(internal); return readerCancel(outputReader, reason); }, }); internal.writable = new WritableStream({ write(chunk) { return write(inputWriter, chunk); }, close() { return closeWriter(inputWriter); }, abort(reason) { disposeState(internal); return abortWriter(inputWriter, reason); }, }); internal.stream = stream; // Publish the brand only after both stream endpoints have been installed. set(states, receiver, internal as Required); register(finalizer, receiver, id); } class CompressionStream { constructor(format: unknown) { required(arguments.length, 1); create(this, format, false, compressionStates); } get readable(): ReadableStream { return state(compressionStates, this).readable; } get writable(): WritableStream { return state(compressionStates, this).writable; } } class DecompressionStream { constructor(format: unknown) { required(arguments.length, 1); create(this, format, true, decompressionStates); } get readable(): ReadableStream { return state(decompressionStates, this).readable; } get writable(): WritableStream { return state(decompressionStates, this).writable; } } export type { CompressionStream, DecompressionStream }; for (const ctor of [CompressionStream, DecompressionStream]) { installInterface(ctor.name, ctor); }