Typescript Multi Async Interface

03 August 2020

I recently had to write a Javascript library which was going to be used in multiple different contexts by lots of different people. The purpose of the library was to supply a stream of updating values. Internally to the library, RxJS is used as it excellently models streams of updating values. However, the interface to the library should not expose the inner workings.

So, I wanted to allow the consumer of the library to access the values in a number of ways:

Usage

Promises

The standard way of supplying asynchronous values. It is commonly used and with async/await the code is very readable:

const currentValue = await lib.getValue();

The problem with this is, you can only get one value, you won’t receive any further updates to the value. So, we can also support the next access method:

Event Emitter

This will allow the consumer to subscribe to updates in the value and the syntax would be as follows:

let currentValue;
lib.getValue()
   .onNew((value) => currentValue = value);

Synchronous

I also wanted to allow the users of the library to synchronously obtain the latest as well as all previous values when they were unable to use async or event emitters:

const currentValue = lib.getValue().latest();
const previousValues = lib.getValue().all();

Implementation

We can see above that the getValue() function has some interesting properties. It returns something which can be listened to like a promise, but it also returns an object which has other methods on it for accessing the values. So what does this object look like? Well here is a Typescript interface describing the object that getValue() returns:

interface AsyncAccessor<T> extends PromiseLike<T> {
    onNew(listener: (value: T) => void): void;
    latest(): T;
    all(): T[];
}

And using RxJS, the implementation of this class is fairly simple:

class ValueAccessor<T> implements AsyncAccessor<T> {
    private _previousValues: T[] = [];

    constructor(private _valueStream: Observable<T>) {
        _valueSteam.subscribe(v => this._previousValues.push(v));
    }

    public getLatest(): T {
        return this._previousValues[this._previousValues.length-1];
    }

    public getAll(): T[] {
        return this._previousValues;
    }

    public onNew(listener: (value: T) => void) {
        this._valueSteam.subscribe(value => listener(value));
    }

    public then<TResult1 = T, TResult2 = never>(onfulfilled?: (value: T) => TResult1 | PromiseLike<TResult1>, onrejected?: (reason: any) => TResult2 | PromiseLike<TResult2>): PromiseLike<TResult1 | TResult2> {
        const previousValue = this.getLatest();
        if (previousValue) {
            return Promise.resolve(previousValue)
                        .then(onfulfilled, onrejected);
        }
        return this._valueSteam
                    .pipe(first())
                    .toPromise()
                    .then(onfulfilled, onrejected);
    }
}

And that’s it. We now have a value accessor that can be used in a number of different values by a consumer to the library.