Implement `TransformStream` Global

Hello! I’m hacking on an extension that makes use of the Streams API quite a bit. I was very surprised to learn that TransformStream is not a global that is provided by the Nova extension environment! Is there a reason that that API is not available? It seems very odd to me that ReadableStream and WritableStream exist, but not their sibling TransformStream.

TransformStream has been a really useful API for me when writing my extension. I really like the ability to design a transformation pipeline for the data I’m reading that can be broken up so nicely into individual parts for testing. It would be really great to see the base class defined to aid in using the ReadableStream’s pipeThrough method!

So far, I’ve tried to make use of the web-streams-polyfill package without success.

esbuild, which I’m using to bundle my dependencies for the extension, is happy to give me a TransformStream implementation through web-streams-polyfill/ponyfill

import { TransformStream } from 'web-streams-polyfill/ponyfill';

However, it seems that TransformStream then validates that the WritableStream it is piped to also comes from the polyfill’s implementation, meaning that it doesn’t actually work for filling in the missing functionality in the Nova extension environment; it isn’t compatible with the ReadableStream and WritableStream that are native to the environment, and I don’t really want to attempt to monkey-patch the global ReadableStream and WritableStream classes in order to make use of the TransformStream from the polyfill.

In case anyone else runs into this issue as well, I built a really simplified version of a TransformStream base class for simple transformations (which is all I needed)

interface NovaTransformStreamController<Out> {
    enqueue(chunk: Out): void;
    error(error: any): void;
}

interface NovaTransformStreamConfig<In, Out> {
    transform(
        chunk: In,
        controller: NovaTransformStreamController<Out>
    ): Promise<void> | void;
}

export class NovaTransformStream<In, Out>
    implements ReadableWritablePair<Out, In>
{
    readable: ReadableStream<Out>;
    writable: WritableStream<In>;

    constructor(config: NovaTransformStreamConfig<In, Out>) {
        let readableController: ReadableStreamController<Out>;
        const readable = new ReadableStream<Out>({
            start(controller) {
                readableController = controller;
            },
        });

        const writable = new WritableStream<In>({
            write(chunk) {
                config.transform(chunk, readableController);
            },

            close() {
                readableController.close();
            },

            abort(reason) {
                readableController.error(reason);
            },
        });

        this.readable = readable;
        this.writable = writable;
    }
}

This can be used like a TransformStream (though it isn’t nearly as robust)

class Doubler extends NovaTransformStream<number, number> {
    constructor() {
        super({
            transform(chunk, controller) {
                controller.enqueue(chunk * 2);
            },
        });
    }
}

This… might not actually work. Running the tests for this class in Node seem to work correctly, but with the ReadableStream and WritableStream implementation that Node provides.

I can confirm that the readable stream is enqueuing data correctly, but the transformation that I created using this base class never receives the data.