@inkandswitch/patchwork-bootloader 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/externals.d.ts +5 -0
- package/dist/externals.js +20 -0
- package/dist/service-worker.d.ts +1 -0
- package/dist/service-worker.js +164 -0
- package/dist/setup.d.ts +3 -0
- package/dist/setup.js +81 -0
- package/dist/types.d.ts +34 -0
- package/dist/types.js +1 -0
- package/dist/vite/importmap-plugin.d.ts +4 -0
- package/dist/vite/importmap-plugin.js +61 -0
- package/dist/vite/patchwork-plugin.d.ts +14 -0
- package/dist/vite/patchwork-plugin.js +8 -0
- package/dist/vite/service-worker-plugin.d.ts +2 -0
- package/dist/vite/service-worker-plugin.js +39 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* these dependencies will be built into the outdir, and injected into the importmap
|
|
3
|
+
*/
|
|
4
|
+
const externals = [
|
|
5
|
+
"@automerge/automerge",
|
|
6
|
+
"@automerge/automerge/slim",
|
|
7
|
+
"@automerge/automerge-repo",
|
|
8
|
+
"@automerge/automerge-repo/slim",
|
|
9
|
+
"@automerge/automerge-repo-keyhive",
|
|
10
|
+
"@keyhive/keyhive",
|
|
11
|
+
"@keyhive/keyhive/slim",
|
|
12
|
+
"@inkandswitch/patchwork-bootloader",
|
|
13
|
+
"@inkandswitch/patchwork-elements",
|
|
14
|
+
"@inkandswitch/patchwork-filesystem",
|
|
15
|
+
"@inkandswitch/patchwork-plugins",
|
|
16
|
+
// sad
|
|
17
|
+
"@codemirror/state",
|
|
18
|
+
"@codemirror/view",
|
|
19
|
+
];
|
|
20
|
+
export default externals;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/// <reference types="service-worker-types" />
|
|
2
|
+
let cachename = "default";
|
|
3
|
+
let debugging = false;
|
|
4
|
+
function log(...args) {
|
|
5
|
+
if (!debugging)
|
|
6
|
+
return;
|
|
7
|
+
console.log.call(console, `%cpatchwork:serviceworker%c\n`, `color: #00ffcc; font-weight: bold`, "color: inherit", ...args);
|
|
8
|
+
}
|
|
9
|
+
self.addEventListener("install", () => self.skipWaiting());
|
|
10
|
+
async function clearOldCaches() {
|
|
11
|
+
const cacheWhitelist = [cachename];
|
|
12
|
+
const cacheNames = await caches.keys();
|
|
13
|
+
const deletePromises = cacheNames.map((cacheName) => {
|
|
14
|
+
if (!cacheWhitelist.includes(cacheName)) {
|
|
15
|
+
return caches.delete(cacheName);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
await Promise.all(deletePromises);
|
|
19
|
+
}
|
|
20
|
+
self.addEventListener("activate", async () => {
|
|
21
|
+
await clearOldCaches();
|
|
22
|
+
clients.claim();
|
|
23
|
+
});
|
|
24
|
+
// a map of response promises by their id
|
|
25
|
+
const responseResolvers = new Map();
|
|
26
|
+
function accept(message) {
|
|
27
|
+
const responseItem = responseResolvers.get(message.id);
|
|
28
|
+
if (!responseItem) {
|
|
29
|
+
return console.warn(`No read response found for id ${message.id}`);
|
|
30
|
+
}
|
|
31
|
+
return responseItem.resolve(message.response);
|
|
32
|
+
}
|
|
33
|
+
const bc = new BroadcastChannel("@patchwork/handoff");
|
|
34
|
+
bc.addEventListener("message", (event) => {
|
|
35
|
+
if (event.data.type == "response")
|
|
36
|
+
accept(event.data);
|
|
37
|
+
});
|
|
38
|
+
// when we receive a `response` req, we resolve the promise with that id
|
|
39
|
+
self.addEventListener("message", async (event) => {
|
|
40
|
+
if (event.data.type == "response") {
|
|
41
|
+
accept(event.data);
|
|
42
|
+
}
|
|
43
|
+
else if (event.data.type == "port") {
|
|
44
|
+
log("recieved messagechannel");
|
|
45
|
+
const [port] = event.ports;
|
|
46
|
+
port.addEventListener("message", (event) => {
|
|
47
|
+
if (event.data.type == "response") {
|
|
48
|
+
accept(event.data);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
else if (event.data.type == "cachename") {
|
|
53
|
+
const nextCachename = event.data.cachename;
|
|
54
|
+
if (cachename == nextCachename) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
console.info(`deleting ${cachename} and setting cache name to ${nextCachename}`);
|
|
58
|
+
caches.delete(cachename);
|
|
59
|
+
cachename = nextCachename;
|
|
60
|
+
}
|
|
61
|
+
else if (event.data.type == "debug") {
|
|
62
|
+
debugging = event.data.debug;
|
|
63
|
+
log("serviceworker debugging enabled");
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
// request ids are kept in a counter
|
|
67
|
+
let reqcount = 0;
|
|
68
|
+
self.addEventListener("fetch", async (fetchEvent) => {
|
|
69
|
+
const request = fetchEvent.request;
|
|
70
|
+
if (request.method !== "GET")
|
|
71
|
+
return fetchEvent.respondWith(fetch(request));
|
|
72
|
+
const url = new URL(fetchEvent.request.url);
|
|
73
|
+
let handoffURL;
|
|
74
|
+
if (url.hostname == self.location.hostname &&
|
|
75
|
+
url.port == self.location.port &&
|
|
76
|
+
url.protocol == self.location.protocol) {
|
|
77
|
+
try {
|
|
78
|
+
// trap any request like /url e.g. /automerge%3Awhatever or /http%3A%2F%2Fsomething.com
|
|
79
|
+
// URI encoded so we can include hashes etc
|
|
80
|
+
handoffURL = new URL(decodeURIComponent(url.pathname.slice(1)));
|
|
81
|
+
log(`received handoff request ${handoffURL}`);
|
|
82
|
+
}
|
|
83
|
+
catch { }
|
|
84
|
+
}
|
|
85
|
+
fetchEvent.respondWith((async () => {
|
|
86
|
+
const cache = await caches.open(cachename);
|
|
87
|
+
const match = await cache.match(request);
|
|
88
|
+
try {
|
|
89
|
+
if (handoffURL) {
|
|
90
|
+
// cache-first strategy for handoff requests
|
|
91
|
+
if (match)
|
|
92
|
+
return match;
|
|
93
|
+
const client = await self.clients.get(fetchEvent.clientId);
|
|
94
|
+
// set up a request id
|
|
95
|
+
const reqid = reqcount++;
|
|
96
|
+
// create a place for the response event handler to put the response
|
|
97
|
+
const resolvers = Promise.withResolvers();
|
|
98
|
+
responseResolvers.set(reqid, resolvers);
|
|
99
|
+
// i don't think this can happen
|
|
100
|
+
if (!client) {
|
|
101
|
+
throw new Error(`the client has gone missing!!! ${fetchEvent.clientId}. i have NO IDEA what to do`);
|
|
102
|
+
}
|
|
103
|
+
const message = {
|
|
104
|
+
id: reqid,
|
|
105
|
+
type: "request",
|
|
106
|
+
cache: cachename,
|
|
107
|
+
request: {
|
|
108
|
+
url: handoffURL.href,
|
|
109
|
+
headers: Object.fromEntries(request.headers.entries()),
|
|
110
|
+
method: request.method,
|
|
111
|
+
destination: request.destination,
|
|
112
|
+
referrer: request.referrer,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
log("sending handoff request", message);
|
|
116
|
+
// send request event to main thread to ask them how to handle it
|
|
117
|
+
client.postMessage(message);
|
|
118
|
+
// this'll finish when the main thread gets back to us
|
|
119
|
+
fetchEvent.waitUntil(resolvers.promise);
|
|
120
|
+
const handoffResponse = await resolvers.promise;
|
|
121
|
+
log("received handoff response", handoffResponse);
|
|
122
|
+
if (handoffResponse) {
|
|
123
|
+
const response = new Response(handoffResponse.body, {
|
|
124
|
+
status: handoffResponse.status,
|
|
125
|
+
headers: handoffResponse.headers,
|
|
126
|
+
});
|
|
127
|
+
if (handoffResponse.cache !== false) {
|
|
128
|
+
log(`caching ${handoffURL}`);
|
|
129
|
+
await cache.put(request, response.clone());
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
log(`caching disabled on ${handoffURL}`);
|
|
133
|
+
}
|
|
134
|
+
return response;
|
|
135
|
+
}
|
|
136
|
+
// no idea what's going on now i'm a teapot i'm a teapot
|
|
137
|
+
return new Response("handler returned nothing", {
|
|
138
|
+
status: 418,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
// network first strategy for external requests
|
|
143
|
+
const response = await fetch(request);
|
|
144
|
+
if (response) {
|
|
145
|
+
if (response.ok && response.url.match(/^https?\:/)) {
|
|
146
|
+
await cache.put(request, response.clone());
|
|
147
|
+
}
|
|
148
|
+
return response;
|
|
149
|
+
}
|
|
150
|
+
if (match)
|
|
151
|
+
return match;
|
|
152
|
+
return new Response("couldnt fetch and no stale", { status: 503 });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (match)
|
|
157
|
+
return match;
|
|
158
|
+
// if something fucked up happens, serve a stale thing if there is one
|
|
159
|
+
// probably can do better error messaging here based on what was caught
|
|
160
|
+
return new Response(`yikes: ${error}`, { status: 555 });
|
|
161
|
+
}
|
|
162
|
+
})());
|
|
163
|
+
});
|
|
164
|
+
export {};
|
package/dist/setup.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { HandoffHandler, SetupServiceWorkerOptions } from "./types.js";
|
|
2
|
+
export declare function bumpServiceWorkerCache(sw?: ServiceWorker | null): void;
|
|
3
|
+
export default function setupServiceWorker(handler: HandoffHandler, options?: SetupServiceWorkerOptions): Promise<ServiceWorker | undefined>;
|
package/dist/setup.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import debug from "debug";
|
|
2
|
+
const debugging = debug.enabled("patchwork:serviceworker");
|
|
3
|
+
const key = "patchworkServiceWorkerCacheVersion";
|
|
4
|
+
function bumpServiceWorkerCacheVersion() {
|
|
5
|
+
const version = new Date().valueOf().toString(36);
|
|
6
|
+
localStorage.setItem(key, version);
|
|
7
|
+
return getServiceWorkerCacheVersion();
|
|
8
|
+
}
|
|
9
|
+
function getServiceWorkerCacheVersion() {
|
|
10
|
+
return localStorage.getItem(key);
|
|
11
|
+
}
|
|
12
|
+
function getOrCreateServiceWorkerCacheVersion() {
|
|
13
|
+
const existing = getServiceWorkerCacheVersion();
|
|
14
|
+
if (existing)
|
|
15
|
+
return existing;
|
|
16
|
+
return bumpServiceWorkerCacheVersion();
|
|
17
|
+
}
|
|
18
|
+
function setServiceWorkerCacheName(sw) {
|
|
19
|
+
if (!sw) {
|
|
20
|
+
throw new Error("no service worker!");
|
|
21
|
+
}
|
|
22
|
+
sw.postMessage({
|
|
23
|
+
type: "cachename",
|
|
24
|
+
cachename: getOrCreateServiceWorkerCacheVersion(),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export function bumpServiceWorkerCache(sw = navigator.serviceWorker.controller) {
|
|
28
|
+
bumpServiceWorkerCacheVersion();
|
|
29
|
+
setServiceWorkerCacheName(sw);
|
|
30
|
+
}
|
|
31
|
+
export default async function setupServiceWorker(handler, options) {
|
|
32
|
+
navigator.serviceWorker.addEventListener("controllerchange", function () {
|
|
33
|
+
console.log("%cnew service worker, reloading", "color: pink; font-weight: bold");
|
|
34
|
+
bumpServiceWorkerCache(navigator.serviceWorker.controller);
|
|
35
|
+
location.reload();
|
|
36
|
+
});
|
|
37
|
+
navigator.serviceWorker.addEventListener("message", async (event) => {
|
|
38
|
+
if (event.data.type == "request") {
|
|
39
|
+
const requestMessage = event.data;
|
|
40
|
+
const source = event.source;
|
|
41
|
+
if (!source) {
|
|
42
|
+
throw new TypeError("can't operate without a source");
|
|
43
|
+
}
|
|
44
|
+
async function send(response, transfer) {
|
|
45
|
+
source.postMessage({
|
|
46
|
+
id: requestMessage.id,
|
|
47
|
+
type: "response",
|
|
48
|
+
response,
|
|
49
|
+
}, { transfer });
|
|
50
|
+
}
|
|
51
|
+
const handoffResponse = await handler(requestMessage.request.url, requestMessage.request);
|
|
52
|
+
if (!handoffResponse) {
|
|
53
|
+
return source?.postMessage({ id: requestMessage.id, type: "response" });
|
|
54
|
+
}
|
|
55
|
+
if (typeof handoffResponse == "string") {
|
|
56
|
+
return send({ body: handoffResponse }, [handoffResponse]);
|
|
57
|
+
}
|
|
58
|
+
if (handoffResponse instanceof Uint8Array) {
|
|
59
|
+
return send({ body: handoffResponse }, [handoffResponse.buffer]);
|
|
60
|
+
}
|
|
61
|
+
const { body: handoffBody, headers, status, cache } = handoffResponse;
|
|
62
|
+
const body = handoffBody;
|
|
63
|
+
send({ body, headers, status, cache }, body instanceof Uint8Array ? [body.buffer] : undefined);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
const existingSw = await navigator.serviceWorker.getRegistration();
|
|
67
|
+
return navigator.serviceWorker
|
|
68
|
+
.register(options?.path ?? "/service-worker.js")
|
|
69
|
+
.then(async (sw) => {
|
|
70
|
+
sw.active?.postMessage({
|
|
71
|
+
type: "debug",
|
|
72
|
+
debug: debugging,
|
|
73
|
+
});
|
|
74
|
+
if (!existingSw?.active) {
|
|
75
|
+
bumpServiceWorkerCache(sw.installing);
|
|
76
|
+
queueMicrotask(() => location.reload());
|
|
77
|
+
return sw.active;
|
|
78
|
+
}
|
|
79
|
+
console.log("service worker alive, loading %c patchwork system ", "background: #fff8f0; border: 1px solid; border-radius: 4px");
|
|
80
|
+
});
|
|
81
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface HandoffRequest {
|
|
2
|
+
url: string;
|
|
3
|
+
headers: Record<string, string>;
|
|
4
|
+
method: string;
|
|
5
|
+
destination: RequestDestination;
|
|
6
|
+
referrer: string;
|
|
7
|
+
}
|
|
8
|
+
export interface HandoffResponse {
|
|
9
|
+
body?: string | Uint8Array<ArrayBuffer> | ReadableStream;
|
|
10
|
+
/** defaults to 200 */
|
|
11
|
+
status?: number;
|
|
12
|
+
headers?: [string, string][] | Record<string, string>;
|
|
13
|
+
cache?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface HandoffRequestMessage {
|
|
16
|
+
id: number;
|
|
17
|
+
type: "request";
|
|
18
|
+
/** the current name of the service worker cache */
|
|
19
|
+
cachename: string;
|
|
20
|
+
request: HandoffRequest;
|
|
21
|
+
}
|
|
22
|
+
export interface HandoffResponseMessage {
|
|
23
|
+
id: number;
|
|
24
|
+
type: "response";
|
|
25
|
+
response: HandoffResponse;
|
|
26
|
+
}
|
|
27
|
+
export type HandoffHandler = (href: string, request: HandoffRequest) => Promise<HandoffResponse | void | string | Uint8Array<ArrayBuffer>>;
|
|
28
|
+
export type SetupServiceWorkerOptions = {
|
|
29
|
+
/**
|
|
30
|
+
* The public path to the service worker file.
|
|
31
|
+
* Defaults to `/service-worker.js`
|
|
32
|
+
*/
|
|
33
|
+
path?: string;
|
|
34
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* these dependencies will be built into the outdir,
|
|
3
|
+
* and injected into the importmap
|
|
4
|
+
*/
|
|
5
|
+
import externals from "../externals.js";
|
|
6
|
+
export const builtins = externals.reduce((builtins, name) => ((builtins[name] = `/packages/${name}.js`), builtins), {});
|
|
7
|
+
/**
|
|
8
|
+
* merge the importmap option with our builtins
|
|
9
|
+
*/
|
|
10
|
+
function createImportMap(options) {
|
|
11
|
+
const importmap = structuredClone(options?.importmap ?? { imports: {}, scopes: {} });
|
|
12
|
+
importmap.imports ??= {};
|
|
13
|
+
importmap.scopes ??= {};
|
|
14
|
+
Object.assign(importmap.imports, builtins);
|
|
15
|
+
return { importmap, builtins };
|
|
16
|
+
}
|
|
17
|
+
export function importmap(options) {
|
|
18
|
+
const { importmap, builtins } = createImportMap(options);
|
|
19
|
+
return {
|
|
20
|
+
name: "@patchwork/vite",
|
|
21
|
+
async buildStart() {
|
|
22
|
+
if (this.environment.mode == "build") {
|
|
23
|
+
for (const [id, fileName] of Object.entries(builtins)) {
|
|
24
|
+
this.emitFile({
|
|
25
|
+
type: "chunk",
|
|
26
|
+
fileName: fileName.slice(1),
|
|
27
|
+
id,
|
|
28
|
+
preserveSignature: "strict",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
resolveId(id) {
|
|
34
|
+
if (id in importmap.imports && !(id in builtins)) {
|
|
35
|
+
return { id: importmap.imports[id], external: true };
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
transformIndexHtml: {
|
|
39
|
+
order: "pre",
|
|
40
|
+
handler(html, ctx) {
|
|
41
|
+
const map = structuredClone(importmap);
|
|
42
|
+
if (ctx.server) {
|
|
43
|
+
// serve builtins from dev server in dev mode
|
|
44
|
+
for (const id of Object.keys(builtins)) {
|
|
45
|
+
map.imports[id] = `/@id/${id}`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
html,
|
|
50
|
+
tags: [
|
|
51
|
+
{
|
|
52
|
+
tag: "script",
|
|
53
|
+
attrs: { type: "importmap" },
|
|
54
|
+
children: JSON.stringify(map, null, 2),
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export default function patchwork(options?: PatchworkVitePluginOptions): import("vite").Plugin<any>[];
|
|
2
|
+
type Imports = {
|
|
3
|
+
[name: string]: string;
|
|
4
|
+
};
|
|
5
|
+
export type ImportMap = {
|
|
6
|
+
imports: Imports;
|
|
7
|
+
scopes?: {
|
|
8
|
+
[scope: string]: Imports;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
export interface PatchworkVitePluginOptions {
|
|
12
|
+
importmap?: ImportMap;
|
|
13
|
+
}
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// todo this is now not patchwork-specific, it's just a fun little importmap thing
|
|
2
|
+
// and a separate thing that returns the service worker which doesn't really
|
|
3
|
+
// need to be a plugin at all
|
|
4
|
+
import { importmap } from "./importmap-plugin.js";
|
|
5
|
+
import { serviceworker } from "./service-worker-plugin.js";
|
|
6
|
+
export default function patchwork(options) {
|
|
7
|
+
return [importmap(options), serviceworker()];
|
|
8
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { transformWithEsbuild } from "vite";
|
|
2
|
+
export function serviceworker() {
|
|
3
|
+
const moduleId = "service-worker.js";
|
|
4
|
+
const path = `/${moduleId}`;
|
|
5
|
+
const ids = [moduleId, path];
|
|
6
|
+
const serviceWorkerExport = "@inkandswitch/patchwork-bootloader/service-worker";
|
|
7
|
+
async function transform(resolve, fs) {
|
|
8
|
+
const exportPath = await resolve(serviceWorkerExport);
|
|
9
|
+
const file = await fs.readFile(exportPath.id, {
|
|
10
|
+
encoding: "utf8",
|
|
11
|
+
});
|
|
12
|
+
const transformation = await transformWithEsbuild(file, serviceWorkerExport, { format: "iife" });
|
|
13
|
+
return transformation;
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
name: "@patchwork/vite",
|
|
17
|
+
async buildStart() {
|
|
18
|
+
if (this.environment.mode == "build") {
|
|
19
|
+
const trans = await transform(this.resolve.bind(this), this.fs);
|
|
20
|
+
this.emitFile({
|
|
21
|
+
type: "prebuilt-chunk",
|
|
22
|
+
fileName: path.slice(1),
|
|
23
|
+
code: trans.code,
|
|
24
|
+
map: trans.map,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
resolveId(id) {
|
|
29
|
+
if (ids.includes(id)) {
|
|
30
|
+
return moduleId;
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
async load(id) {
|
|
34
|
+
if (ids.includes(id)) {
|
|
35
|
+
return transform(this.resolve.bind(this), this.fs);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|