0
\$\begingroup\$

I was suggested by @Bergi on StackOverflow that Async Promise constructors are any pattern.

The argument behind this is that async Promise swallows the errors. In my case, I have try..catch so I'm not sure if the argument is valid. But here is my code - the core of the Wayne JavaScript library.

export class HTTPResponse {
    constructor(resolve, reject) {
        this._resolve = resolve;
        this._reject = reject;
    }
    html(data, init) {
        this.send(data, { type: 'text/html', ...init });
    }
    text(data, init) {
        this.send(data, init);
    }
    json(data, init) {
        this.send(JSON.stringify(data), { type: 'application/json', ...init });
    }
    blob(blob, init = {}) {
        this._resolve(new Response(blob, init));
    }
    send(data, { type = 'text/plain', ...init } = {}) {
        if (![undefined, null].includes(data)) {
            data = new Blob([data], {
                type
            });
        }
        this.blob(data, init);
    }
    // .. there are more code that are irrelevant
}

function chain_handlers(handlers, callback) {
    if (handlers.length) {
        return new Promise((resolve, reject) => {
            let i = 0;
            (async function recur() {
                const handler = handlers[i];
                if (!handler) {
                    return resolve();
                }
                try {
                    await callback(handler, function next() {
                        i++
                        recur();
                    });
                } catch(error) {
                    reject(error);
                }
            })();
        });
    }
}

export class Wayne {
    constructor() {
        this._er_handlers = [];
        this._middlewares = [];
        this._routes = {};
        this._timeout = 5 * 60 * 1000; // 5 minutes
        this._parser = new RouteParser();
        self.addEventListener('fetch', (event) => {
            const promise = new Promise(async (resolve, reject) => {
                const req = event.request;
                try {
                    const res = new HTTPResponse(resolve, reject);
                    await chain_handlers(this._middlewares, function(fn, next) {
                        return fn(req, res, next);
                    });
                    const method = req.method;
                    const url = new URL(req.url);
                    const path = normalize_url(url.pathname);
                    const routes = this._routes[method];
                    if (routes) {
                        const match = this._parser.pick(routes, path);
                        if (match.length) {
                            const [first_match] = match;
                            const fns = [...this._middlewares, ...routes[first_match.pattern]];
                            req.params = first_match.data;
                            setTimeout(function() {
                                reject('Timeout Error');
                            }, this._timeout);
                            await chain_handlers(fns, (fn, next) => {
                                return fn(req, res, next);
                            });
                            return;
                        }
                    }
                    if (event.request.cache === 'only-if-cached' && event.request.mode !== 'same-origin') {
                        return;
                    }
                    //request = credentials: 'include'
                    fetch(event.request).then(resolve).catch(reject);
                } catch(error) {
                    this._handle_error(resolve, req, error);
                }
            });
            event.respondWith(promise.catch(() => {}));
        });
        ['GET', 'POST', 'DELETE', 'PATCH', 'PUT'].forEach(method => {
            this[method.toLowerCase()] = this.method(method);
        });
    }
    _handle_error(resolve, req, error) {
        const res = new HTTPResponse(resolve);
        if (this._er_handlers.length) {
            chain_handlers(this._er_handlers, function(handler, next) {
                handler(error, req, res, next);
            }, function(error) {
                res.html(...error500(error));
            });
        } else {
            res.html(...error500(error));
        }
    }
    use(...fns) {
        fns.forEach(fn => {
            if (typeof fn === 'function') {
                if (fn.length === 4) {
                    this._er_handlers.push(fn);
                } else if (fn.length === 3) {
                    this._middlewares.push(fn);
                }
            }
        });
    }
    method(method) {
        return function(url, fn) {
            if (!this._routes[method]) {
                this._routes[method] = {};
            }
            const routes = this._routes[method];
            if (!routes[url]) {
                routes[url] = [];
            }
            routes[url].push(fn);
            return this;
        };
    }
}

The library works like Express.js in NodeJS but for Service Worker in browser. You can have a middleware or get/post handlers:

const app = new Wayne();

app.use((req, res, next) => {

});

app.get((req, res) => {
   res.json({foo: 'bar'});
});

and the user can call res.json() (and other methods) to resolve the promise from the code above. The code needed a Promise constructor to resolve it in different places and times (by the user). And async was added to simplify calling the chain_handlers function and waiting for it to finish.

The whole code can be seen on GitHub: index.js.

I need help with refactoring this code to get rid of the async promise (if possible) and not make the code a lot of complicated than it is.

\$\endgroup\$
5
  • \$\begingroup\$ The current question title, which states your concerns about the code, is too general to be useful here. Please edit to the site standard, which is for the title to simply state the task accomplished by the code. Please see How to get the best value out of Code Review: Asking Questions for guidance on writing good question titles. \$\endgroup\$ Aug 30, 2023 at 18:52
  • \$\begingroup\$ Would you be open refactor all the middleware handlers to promise-returning functions instead of taking next() callbacks? \$\endgroup\$
    – Bergi
    Aug 30, 2023 at 18:55
  • \$\begingroup\$ @Bergi No, because this is how express.js works it has a callback that is usually called next. Unless you can change the code to make the API stay the same. \$\endgroup\$
    – jcubic
    Aug 30, 2023 at 18:59
  • \$\begingroup\$ @jcubic By "make the API stay the same", do you mean the signature of the middleware functions, or the functionality of the http server? Express.js was written in callback style, before promises were en vogue, and you're having a really hard time converting/wrapping it to deal nicely with promises and async functions. More recent ("modern") libraries take a better approach. See blog.logrocket.com/node-js-alternative-frameworks-express-js for a good comparison. \$\endgroup\$
    – Bergi
    Aug 30, 2023 at 19:11
  • \$\begingroup\$ @Bergi this is a fundamental feature of the library to have almost the same API as express.js. Thanks for the link but I don't care about alternatives. \$\endgroup\$
    – jcubic
    Aug 30, 2023 at 19:28

1 Answer 1

1
\$\begingroup\$

Interesting;

  • JavaScript is lowerCamelCase, so chain_handlers -> chainHandlers

  • The tracking of i in chain_handlers seems clumsy, why not a For .. Of on `handlers'?

  • er_handlers is not an intuitive name, no idea what it is

  • I would rewrite this

      method(method) {
          return function(url, fn) {
              if (!this._routes[method]) {
                  this._routes[method] = {};
              }
              const routes = this._routes[method];
              if (!routes[url]) {
                  routes[url] = [];
              }
              routes[url].push(fn);
              return this;
          };
      }
    

as

    method(method) {
        return function(url, fn) {
            if (!this._routes[method]) {
                this._routes[method] = {url:[fn]};
            } else {
                const routes = this._routes[method] || [];
                routes[url].push(fn);
            }
            return this;
        };
    }    
  • I see no comments at all, but I do see some tricky parts, I would comment the parts that were hardest to get right (I am thinking specifically the use function`)
\$\endgroup\$
2
  • \$\begingroup\$ Thanks, but JavaScript is not camelCase this is just a pattern that some people follow, I don't. Also, the main issue with the code is the async promise constructor your suggestions make no difference. \$\endgroup\$
    – jcubic
    Oct 24, 2023 at 9:58
  • \$\begingroup\$ @jcubic re: camelCase, that is only technically true. All standard JS objects (Array, String, Date, Object, etc. etc.) follow lowerCamelCase except for object constructors. Fair point that I did not read the whole question, I missed async promise part, I will look at that. \$\endgroup\$
    – konijn
    Oct 24, 2023 at 11:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.