SkillAgentSearch skills...

Wayne

Wayne: Open Source Service Worker Routing library for in browser HTTP requests

Install / Use

/learn @jcubic/Wayne

README

<h1 align="center"> <img src="https://github.com/jcubic/wayne/blob/master/assets/wayne-logo.svg?raw=true" alt="Logo of Wayne library - it represents construction worker helmet and text with the name of the library" /> </h1>

npm PRs Welcome jSDelivr

Wayne: Open Source Service Worker Routing library for in-browser HTTP requests

It's like an Express inside Service Worker.

Most of the time Service Worker is used for caching HTTP requests and making the app work when there is no internet (mostly for PWA), but in fact, you can create completely new responses to requests that never leave the browser. This library makes that easier by adding a simple API similar to Express.

Usage

Installation from npm:

npm install @jcubic/wayne
yarn add @jcubic/wayne

The standard way of installing the service worker

if ('serviceWorker' in navigator) {
    const scope = location.pathname.replace(/\/[^\/]+$/, '/');
    navigator.serviceWorker.register('sw.js', { scope, type: 'module' })
             .then(function(reg) {
                 reg.addEventListener('updatefound', function() {
                     const installingWorker = reg.installing;
                     console.log('A new service worker is being installed:',
                                 installingWorker);
                 });
                 // registration worked
                 console.log('Registration succeeded. Scope is ' + reg.scope);
             }).catch(function(error) {
                 // registration failed
                 console.log('Registration failed with ' + error);
             });
}

If you want to support browsers that don't support ES Modules in Service Worker use this instead:

if ('serviceWorker' in navigator) {
    const scope = location.pathname.replace(/\/[^\/]+$/, '/');
    navigator.serviceWorker.register('sw.js', { scope })
             .then(function(reg) {
                 reg.addEventListener('updatefound', function() {
                     const installingWorker = reg.installing;
                     console.log('A new service worker is being installed:',
                                 installingWorker);
                 });
                 // registration worked
                 console.log('Registration succeeded. Scope is ' + reg.scope);
             }).catch(function(error) {
                 // registration failed
                 console.log('Registration failed with ' + error);
             });
}

Inside the same file you can send AJAX requests with standard fetch API.

function get(url) {
    fetch(url)
      .then(res => res.text())
      .then(text => output.innerHTML = text);
}

input.addEventListener('click', () => {
    get(`./user/${user_id.value}`);
});

error.addEventListener('click', () => {
    get(`./error`);
});

Service worker - sw.js file

Importing Wayne module:

  • when worker created as ES Module
import { Wayne } from 'https://cdn.jsdelivr.net/npm/@jcubic/wayne';

const app = new Wayne();
  • When the Service Worker created as normal script
importScripts('https://cdn.jsdelivr.net/npm/@jcubic/wayne/index.umd.min.js');

const app = new wayne.Wayne();
  • When using bundlers like Vite:
import { Wayne } from '@jcubic/wayne';

Using the library

const users = {
  1: 'Jakub T. Jankiewicz',
  2: 'John Doe',
  3: 'Jane Doe'
};

app.get('/user/{id}', function(req, res) {
  const user = users[req.params.id];
  if (user) {
    res.json({result: user});
  } else {
    res.json({error: 'User Not Found'});
  }
});

app.get('/error', function(req, res) {
  nonExisting();
});

app.get('/redirect', function(req, res) {
  res.redirect(301, '/message');
});

app.get('/message', function(req, res) {
  res.text('Lorem Ipsum');
});

app.get('/404', function(req, res) {
    res.text('Not Found', { status: 404, statusText: 'Not Found' });
});

app.get('/external', function(req, res) {
  // lorem ipsum API
  res.redirect('https://api.buildable.dev/@62d55492951509001abc363e/live/lorem-ipsum');
});

Handle the same extension for all requests

importScripts(
  'https://cdn.jsdelivr.net/npm/@jcubic/wayne/index.umd.min.js',
  'https://cdn.jsdelivr.net/gh/jcubic/static@master/js/path.js'
);

const app = new Wayne();

app.get('*', function(req, res) {
  const url = new URL(req.url);
  const extension = path.extname(url.pathname);
  const accept = req.headers.get('Accept');
  if (extension === '.js' && accept.match(/text\/html/)) {
    res.text('// Sorry no source code for you');
  } else {
    res.fetch(req);
  }
});

This code will show the comment // Sorry no source code for you for every request to JavaScript files from the browser (if open in a new tab). When you want to view the file the browser sends Accept: text/html HTTP header.

File system middleware

import { Wayne, FileSystem } from 'https://cdn.jsdelivr.net/npm/@jcubic/wayne';
import FS from "https://cdn.skypack.dev/@isomorphic-git/lightning-fs";
import mime from "https://cdn.skypack.dev/mime";
import path from "https://cdn.skypack.dev/path-browserify";

const { promises: fs } = new FS("__wayne__");

const app = new Wayne();

app.use(FileSystem({ path, fs, mime, prefix: '__fs__' }));

When not using a module the code will be similar. When you access URLs with the prefix __fs__ like ./__fs__/foo it will read files from the indexedDB file system named __wayne__. See Lightning-FS repo for details about the library.

From version 0.12 you can use test callback option to check if the file should serve from the filesystem. Note that it will receive URLs from all domains.

From version 0.13.0 you can use dir callback function that allow to dynamically change directory of served files.

const test = url => {
    const path = url.pathname;
    // return true if pathname should go to filesystem
    return path.match(/__fs__/);
};

const dir = () => '/';

app.use(wayne.FileSystem({ path, fs, mime, test, dir }));

From version 0.14.0 both functions dir and test can be async. So you can use data from IndexedDB e.g. using idb-keyval by Jake Archibald.

A patch in 0.14.3 allow putting interceptors to inject something into output HTML from FileSystem middleware. You do this by adding middleware before FileSystem and patch res.send method:

function fs_interecept(callback) {
    return function(req, res, next) {
        const send = res.send.bind(res);
        res.send = function(data, ...rest) {
            const url = new URL(req.url);
            if (test(url)) {
                data = callback(data);
            }
            return send(data, ...rest);
        };
        next();
    };
}

app.use(fs_interecept(function(html) {
    return html.replace(/<\/body>/, `<script>console.log('intercepted')</script></body>`);
}));

You should use the same test function to make sure that you patch only those requests that came from FS.

Serving files from Cache

Since version 0.19.0 you can use Cache instead of indexedDB to serve the file from Service Worker. You still need file system on main thread to save the files, but then you can use:

wayne.make_cache({ fs, path, mime, dir: '/', prefix: '__fs__', cache: '__wayne__' });

This function will cache all requests from filesystem, so you can use in service worker:

const app = new Wayne();

app.use(FileSystem({ path, prefix: '__fs__', cache: '__wayne__' }));

Only path is required in service worker. Parameters cache and prefix needs to be the same on both calls.

RPC mechanism

In Service Worker, you create a generic route that sends data to the BroadcastChannel:

import { send } from 'https://cdn.jsdelivr.net/npm/@jcubic/wayne';

const channel = new BroadcastChannel('__rpc__');

app.get('/rpc/{name}/*', async (req, res) => {
    const args = req.params[0].split('/');
    const method = req.params.name;
    try {
        const data = await send(channel, method, args);
        res.json(data);
    } catch(e) {
        res.text(e.message);
    }
});

and in the main thread, you create the other side of the channel and the remote methods:

import { rpc } from 'https://cdn.jsdelivr.net/npm/@jcubic/wayne';

const channel = new BroadcastChannel('__rpc__');

rpc(channel, {
    ping: function() {
        return 'pong';
    },
    sin: function(x) {
        return Math.sin(x);
    },
    random: function() {
        return Math.random();
    },
    json: function() {
        return fetch('https://api.npoint.io/8c7cc24b3fd405b775ce').then(res => res.json());
    }
});

When you send a request /rpc/ping you will get a response from methods.ping function.

fetch('./rpc/ping')
  .then(res => res.text())
  .then(text => {
     console.log({ text });
  });

With this setup, you can create new functions/methods that will map to HTTP requests.

The demo below uses random requests:

let index = 0;
const requests = [
    './rpc/ping/',
    './rpc/json/',
    './rpc/random/',
    './rpc/sin/10'
];

rpc.addEventListener('click', () => {
    get(random_request() );
});

function random_request() {
    const next_

Related Skills

View on GitHub
GitHub Stars584
CategoryDevelopment
Updated1d ago
Forks29

Languages

JavaScript

Security Score

100/100

Audited on Apr 3, 2026

No findings