@inkandswitch/patchwork-bootloader 0.0.4 → 0.0.6

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.
@@ -1,10 +1,75 @@
1
1
  /// <reference types="service-worker-types" />
2
2
 
3
- import type { HandoffResponse, HandoffResponseMessage } from "./types.js";
3
+ import { SwLogger } from "./sw-logger.js";
4
4
 
5
+ // Heavy imports — marked external by the service-worker vite plugin,
6
+ // resolved to /packages/... URLs at build time. The SW is registered with
7
+ // type:"module" so the browser fetches these as regular network requests.
8
+ // Uses /slim to avoid top-level await (disallowed in service workers).
9
+ // Wasm is fetched from /automerge.wasm (emitted by the vite plugin) instead
10
+ // of bundling the ~3MB base64 string.
11
+ import { initializeWasm } from "@automerge/automerge/slim";
12
+ // eslint-disable-next-line
13
+ // @ts-ignore — initSync is a wasm-bindgen runtime helper not in the .d.ts
14
+ import { initSync as initSubductionSync } from "@automerge/automerge-subduction/slim";
15
+ import { WebCryptoSigner } from "@automerge/automerge-subduction/slim";
16
+
17
+ import {
18
+ Repo,
19
+ isValidAutomergeUrl,
20
+ parseAutomergeUrl,
21
+ stringifyAutomergeUrl,
22
+ type PeerId,
23
+ } from "@automerge/automerge-repo/slim";
24
+ import {
25
+ findHandleInFolderHandle,
26
+ resolvePackageExport,
27
+ type FolderDoc,
28
+ } from "@inkandswitch/patchwork-filesystem";
29
+
30
+ // Small adapters — bundled directly into the SW
31
+ import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb";
32
+ import { MessageChannelNetworkAdapter } from "@automerge/automerge-repo-network-messagechannel";
33
+ import { WebSocketClientAdapter } from "@automerge/automerge-repo-network-websocket";
34
+
35
+ // TEMPORARY: enable debug npm module in SW context (no localStorage available)
5
36
  let cachename = "default";
6
37
  let debugging = false;
7
38
 
39
+ const SUBDUCTION_ENDPOINTS = ["wss://subduction.sync.inkandswitch.com"];
40
+
41
+ // ── Persistent logger ───────────────────────────────────────────────────
42
+ // Initialized eagerly so it's available for the entire SW lifetime.
43
+ // Access from the SW inspector console via self.printLogs(), self.tailLogs(),
44
+ // self.exportLogs(), self.clearLogs().
45
+ const slog = SwLogger.open().then((logger) => {
46
+ (self as any).slog = logger;
47
+
48
+ (self as any).printLogs = async (n = 200) => {
49
+ const entries = await logger.tail(n);
50
+ for (const e of entries) {
51
+ const prefix = `[${e.ts}] [${e.level}]`;
52
+ if (e.data !== undefined) {
53
+ console.log(prefix, e.msg, e.data);
54
+ } else {
55
+ console.log(prefix, e.msg);
56
+ }
57
+ }
58
+ console.log(`--- ${entries.length} entries ---`);
59
+ };
60
+
61
+ (self as any).tailLogs = (n = 200) => logger.tail(n);
62
+ (self as any).exportLogs = () => logger.exportAll();
63
+ (self as any).clearLogs = () => logger.clear();
64
+
65
+ logger.info("sw-logger initialized");
66
+ return logger;
67
+ });
68
+
69
+ const cacheableStatuses = [
70
+ 200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501,
71
+ ];
72
+
8
73
  function log(...args: any[]) {
9
74
  if (!debugging) return;
10
75
  console.log.call(
@@ -34,38 +99,81 @@ self.addEventListener("activate", async () => {
34
99
  clients.claim();
35
100
  });
36
101
 
37
- // a map of response promises by their id
38
- const responseResolvers = new Map<
39
- number,
40
- PromiseWithResolvers<HandoffResponse>
41
- >();
102
+ let repoPromise: Promise<Repo> | null = null;
103
+
104
+ function getRepo() {
105
+ if (!repoPromise) {
106
+ repoPromise = (async () => {
107
+ const logger = await slog;
108
+
109
+ logger.info("fetching wasm modules");
110
+ const [amWasmBuf, sdnWasmBuf] = await Promise.all([
111
+ fetch("/automerge.wasm").then((r) => r.arrayBuffer()),
112
+ fetch("/subduction.wasm").then((r) => r.arrayBuffer()),
113
+ ]);
114
+ initSubductionSync(new Uint8Array(sdnWasmBuf));
115
+ await initializeWasm(new Uint8Array(amWasmBuf));
116
+ logger.info("wasm initialized");
117
+
118
+ const signer = await WebCryptoSigner.setup();
119
+
120
+ const repo = new Repo({
121
+ storage: new IndexedDBStorageAdapter(),
122
+ signer,
123
+ peerId: ("service-worker-" +
124
+ (Math.random() * 10000).toString(36).slice(2)) as PeerId,
125
+ async sharePolicy(peerId) {
126
+ return peerId.includes("storage-server");
127
+ },
128
+ enableRemoteHeadsGossiping: true,
129
+ subductionWebsocketEndpoints: SUBDUCTION_ENDPOINTS,
130
+ network: [new WebSocketClientAdapter("wss://sync3.automerge.org")],
131
+ });
132
+
133
+ (self as any).repo = repo;
134
+ logger.info("repo constructed, waiting for network subsystem");
135
+
136
+ // Don't block getRepo() on whenReady() — the network subsystem starts
137
+ // with only the subduction adapter, and the MessageChannel adapter is
138
+ // added later via connectPort (which awaits getRepo). Blocking here
139
+ // would deadlock that path and starve the fetch handler.
140
+ repo.networkSubsystem.whenReady().then(() => {
141
+ logger.info("repo network subsystem ready");
142
+ });
42
143
 
43
- function accept(message: HandoffResponseMessage) {
44
- const responseItem = responseResolvers.get(message.id);
45
- if (!responseItem) {
46
- return console.warn(`No read response found for id ${message.id}`);
144
+ return repo;
145
+ })();
47
146
  }
48
- return responseItem.resolve(message.response);
147
+ return repoPromise;
49
148
  }
50
149
 
51
- const bc = new BroadcastChannel("@patchwork/handoff");
52
-
53
- bc.addEventListener("message", (event) => {
54
- if (event.data.type == "response") accept(event.data);
55
- });
150
+ // Connect client MessagePorts to the repo for sync
151
+ async function connectPort(port: MessagePort) {
152
+ const repo = await getRepo();
153
+ repo.networkSubsystem.addNetworkAdapter(
154
+ new MessageChannelNetworkAdapter(port, { useWeakRef: true })
155
+ );
156
+ }
56
157
 
57
- // when we receive a `response` req, we resolve the promise with that id
58
158
  self.addEventListener("message", async (event) => {
59
- if (event.data.type == "response") {
60
- accept(event.data);
159
+ if (event.data.type == "ping") {
160
+ // Keepalive — Chromium idles out service workers after ~30s of inactivity.
161
+ // Reply via the provided port if any; the message event itself also resets
162
+ // the idle timer.
163
+ const [pongPort] = event.ports;
164
+ log("ping");
165
+ if (pongPort) {
166
+ pongPort.postMessage({ type: "pong" });
167
+ log("pong");
168
+ pongPort.close();
169
+ } else if (event.source) {
170
+ (event.source as unknown as Client).postMessage({ type: "pong" });
171
+ log("pong");
172
+ }
61
173
  } else if (event.data.type == "port") {
62
- log("recieved messagechannel");
174
+ log("received messagechannel");
63
175
  const [port] = event.ports;
64
- port.addEventListener("message", (event) => {
65
- if (event.data.type == "response") {
66
- accept(event.data);
67
- }
68
- });
176
+ connectPort(port);
69
177
  } else if (event.data.type == "cachename") {
70
178
  const nextCachename = event.data.cachename;
71
179
  if (cachename == nextCachename) {
@@ -82,14 +190,139 @@ self.addEventListener("message", async (event) => {
82
190
  }
83
191
  });
84
192
 
85
- // request ids are kept in a counter
86
- let reqcount = 0;
87
- self.addEventListener("fetch", async (fetchEvent: FetchEvent) => {
193
+ interface FileDoc {
194
+ content: string | Uint8Array;
195
+ mimeType?: string;
196
+ }
197
+
198
+ // ── Automerge URL resolution ───────────────────────────────────────────
199
+
200
+ async function resolveAutomergeUrl(automergeURL: URL): Promise<Response> {
201
+ const repo = await getRepo();
202
+ const href = automergeURL.href;
203
+ const [maybeAutomergeUrl, ...path] = href.split("/");
204
+
205
+ if (!isValidAutomergeUrl(maybeAutomergeUrl)) {
206
+ return new Response("invalid automerge url", { status: 400 });
207
+ }
208
+
209
+ // Trim trailing empty path segment
210
+ if (path.length && !path[path.length - 1]) path.pop();
211
+
212
+ const { heads, documentId } = parseAutomergeUrl(maybeAutomergeUrl);
213
+
214
+ if (!heads) {
215
+ // Redirect to pinned-heads URL
216
+ const folder = await repo.find(maybeAutomergeUrl);
217
+ const latestHeads = folder.heads();
218
+ const url = stringifyAutomergeUrl({ documentId, heads: latestHeads });
219
+ let location = `/${encodeURIComponent(url)}`;
220
+ if (path.length) location += `/${path.join("/")}`;
221
+ return Response.redirect(location, 307);
222
+ }
223
+
224
+ // If no path, check if this is a package with exports to resolve
225
+ // e.g. /automerge%3Adocid/abc → resolve "abc" via package.json exports
226
+ const folderHandle = await repo.find<FolderDoc>(maybeAutomergeUrl);
227
+
228
+ let fileHandle;
229
+ if (path.length) {
230
+ // Try direct file navigation first
231
+ fileHandle = await findHandleInFolderHandle<FileDoc>(
232
+ repo,
233
+ folderHandle,
234
+ path.map(decodeURIComponent)
235
+ );
236
+
237
+ // If not found as a direct path, try resolving as a package subpath export
238
+ // e.g. /automerge%3Adocid/abc → exports["./abc"] → "./dist/abc.js"
239
+ if (!fileHandle) {
240
+ const subpath = "./" + path.map(decodeURIComponent).join("/");
241
+ const pkgFileHandle = await findHandleInFolderHandle<FileDoc>(
242
+ repo,
243
+ folderHandle,
244
+ ["package.json"]
245
+ );
246
+ if (pkgFileHandle) {
247
+ const pkgDoc = pkgFileHandle.doc() as FileDoc | undefined;
248
+ if (pkgDoc?.content) {
249
+ const pkgJson = JSON.parse(String(pkgDoc.content));
250
+ try {
251
+ const resolved = resolvePackageExport(pkgJson, subpath);
252
+ if (resolved) {
253
+ const resolvedPath = resolved.replace(/^\.\//, "").split("/");
254
+ fileHandle = await findHandleInFolderHandle<FileDoc>(
255
+ repo,
256
+ folderHandle,
257
+ resolvedPath
258
+ );
259
+ }
260
+ } catch {
261
+ // not a valid export subpath, fall through to error
262
+ }
263
+ }
264
+ }
265
+ }
266
+ } else {
267
+ // No path — resolve the root export (like "." in package.json)
268
+ const pkgFileHandle = await findHandleInFolderHandle<FileDoc>(
269
+ repo,
270
+ folderHandle,
271
+ ["package.json"]
272
+ );
273
+ if (pkgFileHandle) {
274
+ const pkgDoc = pkgFileHandle.doc() as FileDoc | undefined;
275
+ if (pkgDoc?.content) {
276
+ const pkgJson = JSON.parse(String(pkgDoc.content));
277
+ try {
278
+ const resolved = resolvePackageExport(pkgJson);
279
+ if (resolved) {
280
+ const resolvedPath = resolved.replace(/^\.\//, "").split("/");
281
+ fileHandle = await findHandleInFolderHandle<FileDoc>(
282
+ repo,
283
+ folderHandle,
284
+ resolvedPath
285
+ );
286
+ }
287
+ } catch {}
288
+ }
289
+ }
290
+ }
291
+
292
+ if (!fileHandle) {
293
+ throw new Error(
294
+ `couldn't resolve ${path.join("/")} in folder at ${maybeAutomergeUrl}`
295
+ );
296
+ }
297
+
298
+ const fileDoc = fileHandle.doc() as unknown as FileDoc;
299
+ const content = fileDoc?.content;
300
+ if (!content) {
301
+ throw new Error(`file at ${href} has no content`);
302
+ }
303
+
304
+ let body: BodyInit =
305
+ content instanceof Uint8Array
306
+ ? (new Uint8Array(content) as BlobPart)
307
+ : String(content);
308
+ const mimeType = fileDoc.mimeType ?? "text/plain";
309
+
310
+ const headers = new Headers({ "content-type": mimeType });
311
+ headers.set("cross-origin-embedder-policy", "credentialless");
312
+ headers.set("cross-origin-resource-policy", "cross-origin");
313
+
314
+ return new Response(body, { status: 200, headers });
315
+ }
316
+
317
+ // ── Fetch handler ──────────────────────────────────────────────────────
318
+
319
+ self.addEventListener("fetch", (fetchEvent: FetchEvent) => {
320
+ log("fetch event", fetchEvent.request.url);
88
321
  const request = fetchEvent.request;
89
322
  if (request.method !== "GET") return fetchEvent.respondWith(fetch(request));
90
323
  const url = new URL(fetchEvent.request.url);
91
324
 
92
- let handoffURL: URL | undefined;
325
+ let specialURL: URL | undefined;
93
326
 
94
327
  if (
95
328
  url.hostname == self.location.hostname &&
@@ -97,10 +330,8 @@ self.addEventListener("fetch", async (fetchEvent: FetchEvent) => {
97
330
  url.protocol == self.location.protocol
98
331
  ) {
99
332
  try {
100
- // trap any request like /url e.g. /automerge%3Awhatever or /http%3A%2F%2Fsomething.com
101
- // URI encoded so we can include hashes etc
102
- handoffURL = new URL(decodeURIComponent(url.pathname.slice(1)));
103
- log(`received handoff request ${handoffURL}`);
333
+ specialURL = new URL(decodeURIComponent(url.pathname.slice(1)));
334
+ log(`received special request ${specialURL}`);
104
335
  } catch {}
105
336
  }
106
337
 
@@ -110,66 +341,42 @@ self.addEventListener("fetch", async (fetchEvent: FetchEvent) => {
110
341
  const match = await cache.match(request);
111
342
 
112
343
  try {
113
- if (handoffURL) {
114
- // cache-first strategy for handoff requests
115
- if (match) return match;
116
- const client = await self.clients.get(fetchEvent.clientId);
117
-
118
- // set up a request id
119
- const reqid = reqcount++;
120
- // create a place for the response event handler to put the response
121
- const resolvers = Promise.withResolvers<HandoffResponse>();
122
- responseResolvers.set(reqid, resolvers);
123
-
124
- // i don't think this can happen
125
- if (!client) {
126
- throw new Error(
127
- `the client has gone missing!!! ${fetchEvent.clientId}. i have NO IDEA what to do`
128
- );
129
- }
130
- const message = {
131
- id: reqid,
132
- type: "request",
133
- cache: cachename,
134
- request: {
135
- url: handoffURL.href,
136
- headers: Object.fromEntries(request.headers.entries()),
137
- method: request.method,
138
- destination: request.destination,
139
- referrer: request.referrer,
140
- },
141
- };
142
- log("sending handoff request", message);
143
- // send request event to main thread to ask them how to handle it
144
- client.postMessage(message);
145
- // this'll finish when the main thread gets back to us
146
- fetchEvent.waitUntil(resolvers.promise);
147
- const handoffResponse = await resolvers.promise;
148
- log("received handoff response", handoffResponse);
149
- if (handoffResponse) {
150
- const response = new Response(handoffResponse.body, {
151
- status: handoffResponse.status,
152
- headers: handoffResponse.headers,
344
+ if (specialURL) {
345
+ if (match) {
346
+ log(`serving ${specialURL} from cache ${cachename}`);
347
+ const headers = new Headers(match.headers);
348
+ headers.set("cross-origin-embedder-policy", "credentialless");
349
+ headers.set("cross-origin-resource-policy", "cross-origin");
350
+ return new Response(match.body, {
351
+ status: match.status,
352
+ headers,
153
353
  });
154
- if (handoffResponse.cache !== false) {
155
- log(`caching ${handoffURL}`);
156
- await cache.put(request, response.clone());
157
- } else {
158
- log(`caching disabled on ${handoffURL}`);
159
- }
354
+ }
355
+
356
+ const response = await resolveAutomergeUrl(specialURL);
357
+
358
+ if (response.status === 307) {
160
359
  return response;
161
360
  }
162
361
 
163
- // no idea what's going on now i'm a teapot i'm a teapot
164
- return new Response("handler returned nothing", {
165
- status: 418,
166
- });
362
+ if (cacheableStatuses.includes(response.status)) {
363
+ log(`caching ${specialURL}`);
364
+ await cache.put(request, response.clone());
365
+ }
366
+
367
+ return response;
167
368
  } else {
168
- // network first strategy for external requests
169
- const response = await fetch(request);
369
+ const response = await fetch(request).catch(() => null);
170
370
  if (response) {
171
- if (response.ok && response.url.match(/^https?\:/)) {
371
+ if (
372
+ cacheableStatuses.includes(response.status) &&
373
+ response.url.match(/^https?\:/)
374
+ ) {
172
375
  await cache.put(request, response.clone());
376
+ } else {
377
+ log(
378
+ `skipping uncacheable response code from cache: ${response.status} for ${response.url}`
379
+ );
173
380
  }
174
381
  return response;
175
382
  }
@@ -177,10 +384,19 @@ self.addEventListener("fetch", async (fetchEvent: FetchEvent) => {
177
384
  return new Response("couldnt fetch and no stale", { status: 503 });
178
385
  }
179
386
  } catch (error) {
387
+ const message =
388
+ error instanceof Error
389
+ ? `${error.message}\n\n${error.stack}`
390
+ : String(error);
391
+ console.error(
392
+ `service worker error resolving ${request.url}${specialURL ? ` (for: ${specialURL})` : ""}.\n${message}`
393
+ );
180
394
  if (match) return match;
181
- // if something fucked up happens, serve a stale thing if there is one
182
- // probably can do better error messaging here based on what was caught
183
- return new Response(`yikes: ${error}`, { status: 555 });
395
+
396
+ return new Response(message, {
397
+ status: 500,
398
+ headers: { "content-type": "text/plain" },
399
+ });
184
400
  }
185
401
  })()
186
402
  );
package/src/setup.ts CHANGED
@@ -1,9 +1,4 @@
1
- import type {
2
- HandoffHandler,
3
- HandoffRequestMessage,
4
- HandoffResponse,
5
- SetupServiceWorkerOptions,
6
- } from "./types.js";
1
+ import type { SetupServiceWorkerOptions } from "./types.js";
7
2
  import debug from "debug";
8
3
 
9
4
  const debugging = debug.enabled("patchwork:serviceworker");
@@ -43,89 +38,87 @@ export function bumpServiceWorkerCache(
43
38
  setServiceWorkerCacheName(sw);
44
39
  }
45
40
 
41
+ (window as any).bumpServiceWorkerCache = bumpServiceWorkerCache;
42
+
43
+ /** Wait for a registration to have an active worker */
44
+ function waitForActive(reg: ServiceWorkerRegistration): Promise<ServiceWorker> {
45
+ if (reg.active) return Promise.resolve(reg.active);
46
+ const worker = reg.installing || reg.waiting;
47
+ if (!worker)
48
+ return Promise.reject(new Error("no service worker in registration"));
49
+ return new Promise((resolve) => {
50
+ worker.addEventListener("statechange", () => {
51
+ if (worker.state === "activated") resolve(worker);
52
+ });
53
+ });
54
+ }
55
+
46
56
  export default async function setupServiceWorker(
47
- handler: HandoffHandler,
48
57
  options?: SetupServiceWorkerOptions
49
58
  ) {
50
- navigator.serviceWorker.addEventListener("controllerchange", function () {
51
- console.log(
52
- "%cnew service worker, reloading",
53
- "color: pink; font-weight: bold"
54
- );
55
- bumpServiceWorkerCache(navigator.serviceWorker.controller);
56
- location.reload();
59
+ // Backwards compat: if an old service worker sends handoff "request" messages,
60
+ // immediately reject them so its fetch handler doesn't hang forever.
61
+ navigator.serviceWorker.addEventListener("message", (event) => {
62
+ if (event.data?.type === "request" && event.data.id != null) {
63
+ navigator.serviceWorker.controller?.postMessage({
64
+ type: "response",
65
+ id: event.data.id,
66
+ response: {
67
+ body: "service worker upgraded, please refresh",
68
+ status: 503,
69
+ headers: { "content-type": "text/plain" },
70
+ },
71
+ });
72
+ }
57
73
  });
58
74
 
59
- navigator.serviceWorker.addEventListener("message", async (event) => {
60
- if (event.data.type == "request") {
61
- const requestMessage: HandoffRequestMessage = event.data;
62
- const source = event.source;
63
-
64
- if (!source) {
65
- throw new TypeError("can't operate without a source");
66
- }
67
-
68
- async function send(
69
- response: HandoffResponse,
70
- transfer?: Transferable[]
71
- ) {
72
- source!.postMessage(
73
- {
74
- id: requestMessage.id,
75
- type: "response",
76
- response,
77
- },
78
- { transfer }
79
- );
80
- }
81
-
82
- const handoffResponse = await handler(
83
- requestMessage.request.url,
84
- requestMessage.request
85
- );
75
+ const path = options?.path ?? "/service-worker.js";
76
+ const reg = await navigator.serviceWorker.register(path, { type: "module" });
86
77
 
87
- if (!handoffResponse) {
88
- return source?.postMessage({ id: requestMessage.id, type: "response" });
89
- }
78
+ // If there's an update waiting or installing, wait for it to activate
79
+ if (reg.installing || reg.waiting) {
80
+ await waitForActive(reg);
81
+ }
90
82
 
91
- if (typeof handoffResponse == "string") {
92
- return send({ body: handoffResponse }, [handoffResponse]);
93
- }
83
+ const active = reg.active!;
84
+ active.postMessage({ type: "debug", debug: debugging });
94
85
 
95
- if (handoffResponse instanceof Uint8Array) {
96
- return send({ body: handoffResponse }, [handoffResponse.buffer]);
97
- }
86
+ // Wait for the controller to be available
87
+ if (!navigator.serviceWorker.controller) {
88
+ await new Promise<void>((resolve) => {
89
+ navigator.serviceWorker.addEventListener(
90
+ "controllerchange",
91
+ () => resolve(),
92
+ { once: true }
93
+ );
94
+ });
95
+ }
98
96
 
99
- const { body: handoffBody, headers, status, cache } = handoffResponse;
97
+ // Send a MessagePort so the SW's repo can sync with clients
98
+ const { port1, port2 } = new MessageChannel();
99
+ navigator.serviceWorker.controller!.postMessage({ type: "port" }, [port2]);
100
100
 
101
- const body = handoffBody;
101
+ // Keepalive Chromium idles out service workers after ~30s of inactivity,
102
+ // which tears down the in-memory Repo and forces a cold restart on the next
103
+ // fetch. Send a ping every 20s while the page is visible to keep it warm.
104
+ setInterval(() => {
105
+ navigator.serviceWorker.controller?.postMessage({ type: "ping" });
106
+ }, 20_000);
102
107
 
103
- send(
104
- { body, headers, status, cache },
105
- body instanceof Uint8Array ? [body.buffer] : undefined
106
- );
107
- }
108
+ // Reload on future SW updates (added after setup so the initial
109
+ // activation doesn't trigger a reload loop).
110
+ navigator.serviceWorker.addEventListener("controllerchange", function () {
111
+ console.info(
112
+ "%cnew service worker took control, reloading...",
113
+ "color: pink; font-weight: bold"
114
+ );
115
+ location.reload();
108
116
  });
109
117
 
110
- const existingSw = await navigator.serviceWorker.getRegistration();
111
-
112
- return navigator.serviceWorker
113
- .register(options?.path ?? "/service-worker.js")
114
- .then(async (sw) => {
115
- sw.active?.postMessage({
116
- type: "debug",
117
- debug: debugging,
118
- });
119
-
120
- if (!existingSw?.active) {
121
- bumpServiceWorkerCache(sw.installing);
122
- queueMicrotask(() => location.reload());
123
- return sw.active!;
124
- }
118
+ console.log(
119
+ "service worker alive, loading %c patchwork system ",
120
+ "background: #fcf2f0; color: #333; border: 2px solid; border-radius: 4px"
121
+ );
125
122
 
126
- console.log(
127
- "service worker alive, loading %c patchwork system ",
128
- "background: #fff8f0; border: 1px solid; border-radius: 4px"
129
- );
130
- });
123
+ return { port: port1 };
131
124
  }