willfarrell / workbee

A tiny ServiceWorker for secure web applications.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

The WorkBee ServiceWorker 🐝

A tiny ServiceWorker for secure web applications.

Features

  • Free to use under MIT license
  • Small and modular (up to 1KB minify + brotli)
  • Tree-shaking supported
  • Zero (0) dependencies
  • GDPR Compliant

TODO

  • improve test coverage
  • more e2e tests
  • documentation
  • session management
  • Push events
  • Range Requests strategy
  • precache extract zip
  • workbox comparison (~1KB vs ~70KB)

Getting Started

Install

npm install @work-bee/core

Basic Setup

import { compileConfig, installEvent, activateEvent, fetchEvent, strategyCacheFirst } from '@work-bee/core'

const config = compileConfig({
  cachePrefix, '1-',
  //precache: ['/path/to/file.ext'],
  routes: [
    {
      methods: ['GET'],
      pathPattern: new RegExp('/img/(.+)$'),
      cacheName: 'img',
      strategy: strategyCacheFirst
    },
    ...
  ]
})

addEventListener('install', (event) => {
  installEvent(event, config)
})

addEventListener('activate', (event) => {
  activateEvent(event, config)
})

addEventListener('fetch', (event) => {
  fetchEvent(event, config)
})

Events

Install

Activate

Fetch

Push (future)

BackgroundFetch (future)

import { backgroundFetchSuccessEvent, backgroundFetchFailEvent } from '@work-bee/core'

...

self.addEventListener('backgroundfetchsuccess', (event) => {
  backgroundFetchSuccessEvent(event, config)
})

self.addEventListener('backgroundfetchfail', (event) => {
  backgroundFetchFailEvent(event, config)
})

Strategies

Network Only

sequenceDiagram
    participant Page
    participant ServiceWorker
    participant Cache
    participant Network
    autonumber
    Page->>ServiceWorker: request
    ServiceWorker->>Network: fetch
	Network->>ServiceWorker: 200: OK
	ServiceWorker->>Page: response

Cache Only

sequenceDiagram
    participant Page
    participant ServiceWorker
    participant Cache
    participant Network
    autonumber
    Page->>ServiceWorker: request
    ServiceWorker->>Cache: match
    Cache->>ServiceWorker: resolve
	ServiceWorker->>Page: response

Network First

sequenceDiagram
    participant Page
    participant ServiceWorker
    participant Cache
    participant Network
    autonumber
    Page->>ServiceWorker: request
    ServiceWorker->>Network: fetch
    Network-->>ServiceWorker: 404: Not Found
    ServiceWorker->>Cache: match
    Cache->>ServiceWorker: resolve
    ServiceWorker->>Page: response

Cache First

sequenceDiagram
    participant Page
    participant ServiceWorker
    participant Cache
    participant Network
    autonumber
    Page->>ServiceWorker: request
    ServiceWorker->>Cache: Cache: Not Found
    ServiceWorker->>Network: fetch
	Network->>ServiceWorker: 200: OK
	ServiceWorker->>Page: response

StaleWhileRevalidate

sequenceDiagram
    participant Page
    participant ServiceWorker
    participant Cache
    participant Network
    autonumber
    Page->>ServiceWorker: request
    par
        ServiceWorker->>strategyCacheFirst: fetch
        strategyCacheFirst->>ServiceWorker: 200: OK
	      ServiceWorker->>Page: response
    and Revalidate
        ServiceWorker->>strategy: fetch
        strategy->>ServiceWorker: 200: OK
        ServiceWorker->>Cache: put
    end

Partition

sequenceDiagram
    participant Page
    participant ServiceWorker
    participant Cache
    participant Network
    autonumber
    Page->>ServiceWorker: request
    par routes[0]
        ServiceWorker->>strategy: fetch
        strategy->>ServiceWorker: 200: OK
    and routes[...]
        ServiceWorker->>strategy: fetch
        strategy->>ServiceWorker: 200: OK
    end
    ServiceWorker->>Page: response

HTML Partitioning

Breaks a page into parts that can each have their own strategy. ie <head>, <header>, <main>, and <footer> where only the <main> may need to be requested when multiple pages are being viewed (<main> in this case should bootstrap the <head> using js).

sequenceDiagram
    participant Page
    participant ServiceWorker
    participant Cache
    participant Network
    autonumber
    Page->>ServiceWorker: request
    par header.html
        ServiceWorker->>Cache: match
        Cache->>ServiceWorker: resolve
    and main.html
        ServiceWorker->>Network: fetch
        Network->>ServiceWorker: 200: OK
    and footer.html
        ServiceWorker->>Cache: match
        Cache->>ServiceWorker: resolve
    end
	ServiceWorker->>Page: response

Request Partitioning

import { compileConfig } @work-bee/core
import { installEvent, activateEvent, fetchEvent } from '@work-bee/events'
import { strategyCacheFirst, strategyPartition } from '@work-bee/strategies'

const config = compileConfig({
  cachePrefix, 'sw-VERSION-',
  routes: [
    {
      methods: ['GET'],
      pathPattern: new RegExp('/api/data$'),
      cacheName: 'data',
      strategy: strategyPartition(compileConfig({
        strategy: strategyCacheFirst,
        cacheName: 'strategyPartition',
        makeRequests: () => []
      }),
      cacheMaxAgeInSeconds: -1
    },
    ...
  ]
})

addEventListener('install', (event) => {
  installEvent(event, config)
})

addEventListener('activate', async (event) => {
  await activateEvent(event, config)
})

addEventListener('fetch', (event) => {
  fetchEvent(event, config)
})
sequenceDiagram
    participant Page
    participant ServiceWorker
    participant Cache
    participant Network
    autonumber
    Page->>ServiceWorker: request
    par ?year=2000
        ServiceWorker->>Cache: match
        Cache->>ServiceWorker: resolve
    and ?year=2001
        ServiceWorker->>Cache: no-match
        ServiceWorker->>Network: fetch
        Network->>ServiceWorker: 200: OK
    end
	ServiceWorker->>Page: response

Middleware

SaveData

Choose strategy based on if Save-Data is enabled.

Session Management (future)

Offline Request Enqueue

Examples

https://serviceworke.rs https://github.com/mdn/serviceworker-cookbook

Caching strategies

/* eslint-env: serviceworker */
import { strategyNetworkFirst } from '@work-bee/core'

const config = {
  strategy: strategyNetworkFirst
}

addEventListener('install', (event) => {
  eventInstall(event, config)
})

addEventListener('activate', (event) => {
  eventActivate(event, config)
})

addEventListener('fetch', (event) => {
  eventFetch(event, config)
})
/* eslint-env: serviceworker */
import { strategyCacheOnly } from '@work-bee/core'

const config = {
  strategy: strategyCacheOnly
}

addEventListener('install', (event) => {
  eventInstall(event, config)
})

addEventListener('activate', (event) => {
  eventActivate(event, config)
})

addEventListener('fetch', (event) => {
  eventFetch(event, config)
})
/* eslint-env: serviceworker */
import { strategyStaleWhileRevalidate } from '@work-bee/core'

const config = {
  strategy: strategyStaleWhileRevalidate
}

addEventListener('install', (event) => {
  eventInstall(event, config)
})

addEventListener('activate', (event) => {
  eventActivate(event, config)
})

addEventListener('fetch', (event) => {
  eventFetch(event, config)
})

TODO refresh middleware, add to strategyStaleWhileRevalidate?

/* eslint-env: serviceworker */
import { strategyNetworkOnly, strategyCacheOnly } from '@work-bee/core'
import fallbackMiddleware from '@work-bee/fallback'

const fallback = fallbackMiddleware({ path: '/path/to/fallback' })
const config = {
  precache: {
    routes: [
      {
        path: '/path/to/fallback'
      }
    ],
    strategy: strategyCacheOnly
  },
  strategy: strategyNetworkOnly,
  after: [fallback.after]
}

addEventListener('install', (event) => {
  eventInstall(event, config)
})

addEventListener('activate', (event) => {
  eventActivate(event, config)
})

addEventListener('fetch', (event) => {
  eventFetch(event, config)
})

Web Push

General Usage

Offline

/* eslint-env: serviceworker */
import { strategyNetworkOnly, strategyCacheOnly } from '@work-bee/core'
import fallbackMiddleware from '@work-bee/fallback'

const fallback = fallbackMiddleware({
  path: '/path/to/offline',
  statusCodes: [503, 504] // or Error
})
const config = {
  precache: {
    routes: [
      {
        path: '/path/to/offline'
      }
    ],
    strategy: strategyCacheOnly
  },
  strategy: strategyNetworkOnly,
  after: [fallback.after]
}

addEventListener('install', (event) => {
  eventInstall(event, config)
})

addEventListener('activate', (event) => {
  eventActivate(event, config)
})

addEventListener('fetch', (event) => {
  eventFetch(event, config)
})
/* eslint-env: serviceworker */
import { strategyCacheFirst, strategyCacheOnly } from '@work-bee/core'

const config = {
  precache: {
    routes: [
      {
        path: '/path/to/required'
      },
      ...
    ],
    eventType: 'precache'
  },
  strategy: strategyCacheFirst
}

addEventListener('install', (event) => {
  eventInstall(event, config)
})

addEventListener('activate', (event) => {
  eventActivate(event, config)
})

addEventListener('fetch', (event) => {
  eventFetch(event, config)
})
/* eslint-env: serviceworker */
import { strategyCacheFirst, strategyCacheOnly } from '@work-bee/core'

const config = {
  precache: {
    routes: '/path/to/precache.json',
    extract: // TODO
  },
  strategy: strategyCacheFirst
}

addEventListener('install', (event) => {
  eventInstall(event, config)
})

addEventListener('activate', (event) => {
  eventActivate(event, config)
})

addEventListener('fetch', (event) => {
  eventFetch(event, config)
})

Beyond Offline

/* eslint-env: serviceworker */
/*import { strategyLocalDownload } from '@work-bee/core'

const config = {
  routes: [
    {
      pathPattern: new Regexp('/path/to/download/.*$'),
      strategy: strategyLocalDownload
    }
  ]
}

addEventListener('install', (event) => {
  eventInstall(event, config)
})

addEventListener('activate', (event) => {
  eventActivate(event, config)
})

addEventListener('fetch', (event) => {
  eventFetch(event, config)
})*/

TODO 3rd party middleware - ie plausible

TODO new middleware.before - group external domains to allow request path replace & ping all to pick best

TODO new plugin - return empty response, iterate over contents and cachePut

TODO need to understand more

/* eslint-env: serviceworker */
import { strategyCacheFirst } from '@work-bee/core'
import { offlineMiddleware } from '@work-bee/offline'

const offline = offlineMiddleware({ pollDelay: 0 })
const config = {
  strategy: strategyCacheFirst,
  middlewares: [offline]
}

addEventListener('install', (event) => {
  eventInstall(event, config)
})

addEventListener('activate', (event) => {
  eventActivate(event, config)
})

addEventListener('fetch', (event) => {
  eventFetch(event, config)
})

addEventListener('message', (event) => {
  const { data } = event
  event.waitUntil(messageEvents[data.type](data))
})
const messageEvents = {
  online: offline.onlineEvent
}

Performance

/* eslint-env: serviceworker */
import { strategyCacheFirst } from '@work-bee/core'

const config = {
  strategy: strategyCacheFirst
}

addEventListener('install', (event) => {
  eventInstall(event, config)
})

addEventListener('activate', (event) => {
  eventActivate(event, config)
})

addEventListener('fetch', (event) => {
  eventFetch(event, config)
})
/* eslint-env: serviceworker */
import {
  compileConfig,
  eventInstall,
  eventActivate,
  eventFetch,
  cacheOverrideEvent
} from '@work-bee/core'

const config = compileConfig({
  strategy: strategyNetworkFirst
})

addEventListener('install', (event) => {
  eventInstall(event, config)
})

addEventListener('activate', (event) => {
  eventActivate(event, config)
})

addEventListener('fetch', (event) => {
  eventFetch(event, config)
})

addEventListener('message', (event) => {
  const { data } = event
  /* data = {
    type: 'cache',
    request: new Request('/path/to/template', {method:'GET'}),
    response: new Response('')
  }*/
  event.waitUntil(messageEvents[data.type](data))
})
const messageEvents = {
  cache: cacheOverrideEvent(config)
}

About

A tiny ServiceWorker for secure web applications.

License:MIT License


Languages

Language:JavaScript 95.6%Language:HTML 4.0%Language:Shell 0.4%