0

There is a function:

export function getImage(requestParameters: TRequestParameters): TRequest<TResponse<ImageBitmap | HTMLImageElement>> {
    const request = helper.getArrayBuffer(requestParameters);

    return {
        response: (async () => {
            const response = await request.response;
            const image = await arrayBufferToCanvasImageSource(response.data);

            return {
                data: image,
                cacheControl: response.cacheControl,
                expires: response.expires
            };
        })(),

        cancel: request.cancel
    };
}

It is synchronous, but returns an object consisting of two fields: response - a Promise, which is resolved with an object (3 fields: data, cacheControl, expires, but that's not interesing for us) and cancel - a method that cancels the request.

The function works as expected and everything about it is just fine. However, I need to implement an additional constraint. It is necessary to make sure that the number of parallel (simultaneous) requests to the network at any given point in time does not exceed n.

Thus, if n === 0, no request should be made. If n === 1, then only one image can be loaded at a time (that is, all images are loaded sequentially). For n > 1 < m, no more than m images can be loaded simultaneously.


My solution

Based on the fact that the getImage function is synchronous, the line

const request = helper.getArrayBuffer(requestParameters);

is executed immediately when getImage is called. That's not what we want though, we need to postpone the execution of the request itself. Therefore, we will replace the request variable with the requestMaker function, which we will call only when we need it:

export function getImage(requestParameters: TRequestParameters): TRequest<TResponse<ImageBitmap | HTMLImageElement>> {
    if (webpSupported.supported) {
        if (!requestParameters.headers) requestParameters.headers = {};
        requestParameters.headers['Accept'] = 'image/webp,*/*';
    }

    function requestMaker() {
        const request = helper.getArrayBuffer(requestParameters);
        return request;
    }

    return {
        response: (async () => {
            const response = await requestMaker().response;
            const image = await arrayBufferToCanvasImageSource(response.data);

            return {
                data: image,
                cacheControl: response.cacheControl,
                expires: response.expires
            };
        })(),

        cancel() {
            //
        }
    };
}

(Let's omit the cancel for now for the sakes of simplicity).

Now the execution of this requestMaker function, which makes the request itself, needs to be postponed until some point.

Suppose now we are trying to solve the problem only for n === 1.

Let's create an array in which we will store all requests that are currently running:

const ongoingImageRequests = [];

Now, inside requestMaker, we will save requests to this variable as soon as they occur, and delete them as soon as we receive a response:

const ongoingImageRequests = [];

export function getImage(requestParameters: TRequestParameters): TRequest<TResponse<ImageBitmap | HTMLImageElement>> {
    if (webpSupported.supported) {
        if (!requestParameters.headers) requestParameters.headers = {};
        requestParameters.headers['Accept'] = 'image/webp,*/*';
    }

    function requestMaker() {
        const request = helper.getArrayBuffer(requestParameters);

        ongoingImageRequests.push(request);
        request.response.finally(() => ongoingImageRequests.splice(ongoingImageRequests.indexOf(request), 1));

        return request;
    }

    return {
        response: (async () => {
            const response = await requestMaker().response;
            const image = await arrayBufferToCanvasImageSource(response.data);

            return {
                data: image,
                cacheControl: response.cacheControl,
                expires: response.expires
            };
        })(),

        cancel() {
            //
        }
    };
}

It's only left now to add a restriction regarding the launch of requestMaker: before starting it, we need to wait until all the requests from the array are finished:

const ongoingImageRequests = [];

export function getImage(requestParameters: TRequestParameters): TRequest<TResponse<ImageBitmap | HTMLImageElement>> {
    if (webpSupported.supported) {
        if (!requestParameters.headers) requestParameters.headers = {};
        requestParameters.headers['Accept'] = 'image/webp,*/*';
    }

    function requestMaker() {
        const request = helper.getArrayBuffer(requestParameters);

        ongoingImageRequests.push(request);
        request.response.finally(() => ongoingImageRequests.splice(ongoingImageRequests.indexOf(request), 1));

        return request;
    }

    return {
        response: (async () => {
            await Promise.allSettled(ongoingImageRequests.map(ongoingImageRequest => ongoingImageRequest.response));

            const response = await requestMaker().response;
            const image = await arrayBufferToCanvasImageSource(response.data);

            return {
                data: image,
                cacheControl: response.cacheControl,
                expires: response.expires
            };
        })(),

        cancel() {
            //
        }
    };
}

I understand it this way: when getImage starts executing (it is called from somewhere outside), it immediately returns an object in which response is a Promise, which will resolve at least not before the moment when all the other requests from the queue are completed.

But, as it turns out, this solution for some reason does not work. The question is why? And how to make it work? At least for n === 1.

smellyshovel
  • 176
  • 10
  • *"It is synchronous"*: code that follows below `await` is certainly *not* synchronous in the most common definition of the word in the JavaScript world. – trincot Jan 18 '23 at 17:40
  • @trincot the `getImage` function itself is 100% synchronous. It's not declared using `async` nor it returns a `Promise`. Don't see any terminology misuse here... – smellyshovel Jan 18 '23 at 17:41
  • That will be a debate over words, but the execution of `getImage` includes the execution of an `async` function, so it is a bit arbitrary to say that it is synchronous because it is not `async`. When you wrote "it is synchronous", I thought you referred to the presented code, that clearly initiates asynchronous behaviour. You write it doesn't return a promise, but it does create one, and returns an object that includes a promise. I don't see much of a difference when it comes to what is synchronous or not. – trincot Jan 18 '23 at 17:44
  • 1
    There are several Q&A here on limiting the number of pending promises: [1](https://stackoverflow.com/q/40639432/5459839), [2](https://stackoverflow.com/q/40375551/5459839), [3](https://stackoverflow.com/q/54901078/5459839)... – trincot Jan 18 '23 at 17:54
  • @trincot well, the p-limit package mentioned in one of the answers looks quite promising to give it a try. Though it still doesn’t answer the question about what is wrong about my solution. – smellyshovel Jan 18 '23 at 17:59
  • "*this solution for some reason does not work*" - which undesirable behaviour do you observe? (I have at least one guess, but please elaborate on what is not working) – Bergi Jan 18 '23 at 19:32
  • @Bergi well, by "does not work" I mean that all the requests are made simultaneously, wihout any queueing. There's that `getArrayBuffer` call, and if I add a `console.log` to it, I can see that all the requests are done at the same time. – smellyshovel Jan 19 '23 at 08:39

0 Answers0