RPC for Service Workers -- move that heavy computation off of your UI thread!
npm add swarpc arktype
If you want to use the latest commit instead of a published version, you can, either by using the Git URL:
npm add git+https://github.com/gwennlbh/swarpc.git
Or by straight up cloning the repository and pointing to the local directory (very useful to hack on sw&rpc while testing out your changes on a more substantial project):
mkdir -p vendored
git clone https://github.com/gwennlbh/swarpc.git vendored/swarpc
npm add file:vendored/swarpc
This works thanks to the fact that dist/
is published on the repository (and kept up to date with a CI workflow).
import type { ProceduresMap } from "swarpc"
import { type } from "arktype"
export const procedures = {
searchIMDb: {
// Input for the procedure
input: type({ query: "string", "pageSize?": "number" }),
// Function to be called whenever you can update progress while the procedure is running -- long computations are a first-class concern here. Examples include using the fetch-progress NPM package.
progress: type({ transferred: "number", total: "number" }),
// Output of a successful procedure call
success: type({
id: "string",
primary_title: "string",
genres: "string[]",
}).array(),
},
} as const satisfies ProceduresMap
In your service worker file:
import fetchProgress from "fetch-progress"
import { Server } from "swarpc"
import { procedures } from "./procedures.js"
// 1. Give yourself a server instance
const swarpc = Server(procedures)
// 2. Implement your procedures
swarpc.searchIMDb(async ({ query, pageSize = 10 }, onProgress) => {
const queryParams = new URLSearchParams({
page_size: pageSize.toString(),
query,
})
return fetch(`https://rest.imdbapi.dev/v2/search/titles?${queryParams}`)
.then(fetchProgress({ onProgress }))
.then((response) => response.json())
.then(({ titles } => titles)
})
// ...
// 3. Start the event listener
swarpc.start(self)
Here's a Svelte example!
<script>
import { Client } from "swarpc"
import { procedures } from "./procedures.js"
const swarpc = Client(procedures)
let query = $state("")
let results = $state([])
let progress = $state(0)
</script>
<search>
<input type="text" bind:value={query} placeholder="Search IMDb" />
<button onclick={async () => {
results = await swarpc.searchIMDb({ query }, (p) => {
progress = p.transferred / p.total
})
}}>
Search
</button>
</search>
{#if progress > 0 && progress < 1}
<progress value={progress} max="1" />
{/if}
<ul>
{#each results as { id, primary_title, genres } (id)}
<li>{primary_title} - {genres.join(", ")}</li>
{/each}
</ul>
To make your procedures meaningfully cancelable, you have to make use of the AbortSignal
API. This is passed as a third argument when implementing your procedures:
server.searchIMDb(async ({ query }, onProgress, abort) => {
// If you're doing heavy computation without fetch:
let aborted = false
abort?.addEventListener("abort", () => {
aborted = true
})
// Use `aborted` to check if the request was canceled within your hot loop
for (...) {
/* here */ if (aborted) return
...
}
// When using fetch:
await fetch(..., { signal: abort })
})
Instead of calling await client.myProcedure()
directly, call client.myProcedure.cancelable()
. You'll get back an object with
async cancel(reason)
: a function to cancel the requestrequest
: a Promise that resolves to the result of the procedure call. await
it to wait for the request to finish.Example:
// Normal call:
const result = await swarpc.searchIMDb({ query })
// Cancelable call:
const { request, cancel } = swarpc.searchIMDb.cancelable({ query })
setTimeout(() => cancel().then(() => console.warn("Took too long!!")), 5_000)
await request