Cloudflare Workers “Singleton” class

519 views Asked by At

I've started using workers for my sites. The purpose of workers on my sites: mostly inject some code(html/js/css) in different locations of HTML page. It can be some config data, or some legal text and etc.

So what I do now, is create a config in KV for each website and based on user country/language injecting above html/js and etc.

Below is a Store Class (Singleton pattern), that holds all the info from config, but doesn't work in workers, by doesn't work I mean, after first request, the data is persistent, and after some time it gets updated:

For example 1st request URL: /about On Second request URL: /es/about/

By output console.log(event.request) will show /es/about , but Store.request outputs: /about/

any workaround for this, to force refresh of data, I thought becuase i don't do it in constructor, but by calling custom method should do the trick but, it doesn't.?

Below is some code example.


import { Store } from "@helpers/store";

addEventListener('fetch', event => {
    event.respondWith(handleRequest(event.request))
});

//HTML Rewriter Class
class Head {

    element(el) {
        el.append(`
                <script id="config">
                    var config = ${Store.export()};
                </script>`, {
            html: true
        });
    }
}

async function handleRequest(request) {
    let store = await Store.make(request);

    const response = await fetch(request);

    let html = new HTMLRewriter();
    html.on("head", new Head());

    return html.transform(response);
}

//src/helpers/store.js

class Store {

    constructor(){

        this._request = null
        this._config = {}
        this._url = null

    }

    async make(request){

        let config = {}

        this._request = request;

        const domain = this.url.hostname.replace(/www\./g, "");

        const country = request.headers.get('cf-ipcountry')

        const website = await WEBSITES.get(domain, "json"); //WEBSITES is KV namespace

        const { license, lang } = website;

        this._config = {
            country,
            domain,
            license,
            lang
        };

        return this;
    }

    export(){

        return JSON.stringify(this._config)
    }

    get request(){
        
        return this._request;

    }

    get url(){
        
        if(!this._url){

            this._url = new URL(this.request.url)

        }

        return this._url;

    }
}

export default new Store()
2

There are 2 answers

3
Kenton Varda On BEST ANSWER

A single instance of your Worker may handle multiple requests, including concurrent requests. In your code, you are using a singleton instance of Store to store metadata about the current request. But if multiple requests are handled concurrently, then the second request will overwrite the content of Store before the first request completes. This may cause the first request to render its HTML using metadata from the second request.

It seems like the use of a singleton pattern isn't what you want here. Based on the code, it looks like you really want to create a separate instance of Store for every request.

1
user2575858 On

2 issues come to mind:

  • You are creating a new HTMLRewriter for each call to the worker. This will make for some concurrency issues. The instantiation of the rewriter should be done outside the handleRequst method. For example right after the import statement.
  • You are importing the Store class and never instantiating it but using its methods like they are static(which they aren't). This will also give you concurrency issues.