Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit dd5ec5c

Browse files
authoredMay 19, 2024
[Feature] Replace BroadcastChannel API with Client API in Service Worker (#401)
## Overview There are many different APIs available for the two-way communication between service worker and web pages. According to the suggestion of web.dev, previously we used the simple BroadcastChannel API. https://web.dev/articles/two-way-communication-guide However, if the service worker has been killed by the browser, messages sent via the BroadcastChannel API cannot revive it. This causes unstable service worker life while users are using the webapp. This PR replaces BroadcastChannel API with the more primitive Client API as we found that messages sent via Client API will revive a stopped service worker. This ensures the normal functioning of our keep-alive mechanism. ## Primary Change - service_worker.ts: - Replace `BroadcastChannel` with Client API (`navigator.serviceWorker.controller.postMessage()` and `client.postMessage()`) - Add `clientRegistry` to remember mapping between incoming messages and client ids - Add - Rename files (the export names are kept the same): - `web_service_worker.ts` -> `service_worker.ts` - `service_worker.ts` -> `extension_service_worker.ts` ## Testing - https://chat.webllm.ai/ - `examples/service-worker` The chat webapp is able to correctly keeping service worker alive after this change.
1 parent ad04a7d commit dd5ec5c

File tree

10 files changed

+494
-307
lines changed

10 files changed

+494
-307
lines changed
 

‎examples/service-worker/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "0.1.0",
44
"private": true,
55
"scripts": {
6-
"start": "parcel src/index.html --port 3000",
6+
"start": "parcel src/index.html --port 3000",
77
"build": "parcel build src/index.html --dist-dir lib"
88
},
99
"devDependencies": {
@@ -15,6 +15,6 @@
1515
"url": "^0.11.3"
1616
},
1717
"dependencies": {
18-
"@mlc-ai/web-llm": "file:../../lib"
18+
"@mlc-ai/web-llm": "file:../../lib/"
1919
}
2020
}

‎examples/service-worker/src/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ async function mainStreaming() {
7777
};
7878
const selectedModel = "Llama-3-8B-Instruct-q4f32_1";
7979

80-
const engine: webllm.EngineInterface =
80+
const engine: webllm.WebServiceWorkerEngine =
8181
await webllm.CreateWebServiceWorkerEngine(selectedModel, {
8282
initProgressCallback: initProgressCallback,
8383
});

‎package-lock.json

Lines changed: 11 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"@rollup/plugin-node-resolve": "^13.0.4",
3131
"@types/chrome": "^0.0.266",
3232
"@types/jest": "^29.5.11",
33+
"@types/serviceworker": "^0.0.86",
3334
"@typescript-eslint/eslint-plugin": "^5.59.6",
3435
"@typescript-eslint/parser": "^5.59.6",
3536
"@webgpu/types": "^0.1.24",
@@ -45,4 +46,4 @@
4546
"tvmjs": "file:./tvm_home/web",
4647
"typescript": "^4.9.5"
4748
}
48-
}
49+
}

‎src/web_service_worker.ts renamed to ‎src/extension_service_worker.ts

Lines changed: 81 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,37 @@
11
import * as tvmjs from "tvmjs";
22
import { AppConfig, ChatOptions, EngineConfig } from "./config";
3-
import { ReloadParams, WorkerMessage } from "./message";
3+
import { ReloadParams, WorkerRequest } from "./message";
44
import { EngineInterface } from "./types";
55
import {
6+
ChatWorker,
67
EngineWorkerHandler,
78
WebWorkerEngine,
89
PostMessageHandler,
9-
ChatWorker,
1010
} from "./web_worker";
1111
import { areAppConfigsEqual, areChatOptionsEqual } from "./utils";
1212

13-
const BROADCAST_CHANNEL_SERVICE_WORKER_ID = "@mlc-ai/web-llm-sw";
14-
const BROADCAST_CHANNEL_CLIENT_ID = "@mlc-ai/web-llm-client";
15-
export const serviceWorkerBroadcastChannel = new BroadcastChannel(
16-
BROADCAST_CHANNEL_SERVICE_WORKER_ID
17-
);
18-
export const clientBroadcastChannel = new BroadcastChannel(
19-
BROADCAST_CHANNEL_CLIENT_ID
20-
);
21-
2213
/**
23-
* PostMessageHandler wrapper for sending message from service worker back to client
14+
* A post message handler that sends messages to a chrome.runtime.Port.
2415
*/
25-
class ClientPostMessageHandler implements PostMessageHandler {
26-
postMessage(message: any) {
27-
clientBroadcastChannel.postMessage(message);
28-
}
29-
}
16+
export class PortPostMessageHandler implements PostMessageHandler {
17+
port: chrome.runtime.Port;
18+
enabled: boolean = true;
3019

31-
/**
32-
* PostMessageHandler wrapper for sending message from client to service worker
33-
*/
34-
class ServiceWorker implements ChatWorker {
35-
constructor() {
36-
serviceWorkerBroadcastChannel.onmessage = this.onmessage;
20+
constructor(port: chrome.runtime.Port) {
21+
this.port = port;
3722
}
3823

39-
// ServiceWorkerEngine will later overwrite this
40-
onmessage() {}
24+
/**
25+
* Close the PortPostMessageHandler. This will prevent any further messages
26+
*/
27+
close() {
28+
this.enabled = false;
29+
}
4130

42-
postMessage(message: any) {
43-
serviceWorkerBroadcastChannel.postMessage(message);
31+
postMessage(event: any) {
32+
if (this.enabled) {
33+
this.port.postMessage(event);
34+
}
4435
}
4536
}
4637

@@ -65,25 +56,29 @@ export class ServiceWorkerEngineHandler extends EngineWorkerHandler {
6556
chatOpts?: ChatOptions;
6657
appConfig?: AppConfig;
6758

68-
constructor(engine: EngineInterface) {
69-
super(engine, new ClientPostMessageHandler());
70-
serviceWorkerBroadcastChannel.onmessage = this.onmessage.bind(this);
59+
constructor(engine: EngineInterface, port: chrome.runtime.Port) {
60+
let portHandler = new PortPostMessageHandler(port);
61+
super(engine, portHandler);
62+
63+
port.onDisconnect.addListener(() => {
64+
portHandler.close();
65+
});
66+
}
67+
68+
setPort(port: chrome.runtime.Port) {
69+
let portHandler = new PortPostMessageHandler(port);
70+
this.setPostMessageHandler(portHandler);
71+
port.onDisconnect.addListener(() => {
72+
portHandler.close();
73+
});
7174
}
7275

7376
onmessage(event: any): void {
74-
const msgEvent = event as MessageEvent;
75-
const msg = msgEvent.data as WorkerMessage;
76-
77-
if (msg.kind === "keepAlive") {
78-
const msg: WorkerMessage = {
79-
kind: "heartbeat",
80-
uuid: msgEvent.data.uuid,
81-
content: "",
82-
};
83-
this.postMessageInternal(msg);
77+
if (event.type === "keepAlive") {
8478
return;
8579
}
8680

81+
const msg = event as WorkerRequest;
8782
if (msg.kind === "init") {
8883
this.handleTask(msg.uuid, async () => {
8984
const params = msg.content as ReloadParams;
@@ -124,7 +119,7 @@ export class ServiceWorkerEngineHandler extends EngineWorkerHandler {
124119
});
125120
return;
126121
}
127-
super.onmessage(msg);
122+
super.onmessage(event);
128123
}
129124
}
130125

@@ -134,14 +129,17 @@ export class ServiceWorkerEngineHandler extends EngineWorkerHandler {
134129
* @param modelId The model to load, needs to either be in `webllm.prebuiltAppConfig`, or in
135130
* `engineConfig.appConfig`.
136131
* @param engineConfig Optionally configures the engine, see `webllm.EngineConfig` for more.
132+
* @param keepAliveMs The interval to send keep alive messages to the service worker.
133+
* See [Service worker lifecycle](https://developer.chrome.com/docs/extensions/develop/concepts/service-workers/lifecycle#idle-shutdown)
134+
* The default is 10s.
137135
* @returns An initialized `WebLLM.ServiceWorkerEngine` with `modelId` loaded.
138136
*/
139137
export async function CreateServiceWorkerEngine(
140138
modelId: string,
141-
engineConfig?: EngineConfig
139+
engineConfig?: EngineConfig,
140+
keepAliveMs: number = 10000
142141
): Promise<ServiceWorkerEngine> {
143-
await navigator.serviceWorker.ready;
144-
const serviceWorkerEngine = new ServiceWorkerEngine(new ServiceWorker());
142+
const serviceWorkerEngine = new ServiceWorkerEngine(keepAliveMs);
145143
serviceWorkerEngine.setInitProgressCallback(
146144
engineConfig?.initProgressCallback
147145
);
@@ -153,24 +151,48 @@ export async function CreateServiceWorkerEngine(
153151
return serviceWorkerEngine;
154152
}
155153

154+
class PortAdapter implements ChatWorker {
155+
port: chrome.runtime.Port;
156+
private _onmessage!: (message: any) => void;
157+
158+
constructor(port: chrome.runtime.Port) {
159+
this.port = port;
160+
this.port.onMessage.addListener(this.handleMessage.bind(this));
161+
}
162+
163+
// Wrapper to handle incoming messages and delegate to onmessage if available
164+
private handleMessage(message: any) {
165+
if (this._onmessage) {
166+
this._onmessage(message);
167+
}
168+
}
169+
170+
// Getter and setter for onmessage to manage adding/removing listeners
171+
get onmessage(): (message: any) => void {
172+
return this._onmessage;
173+
}
174+
175+
set onmessage(listener: (message: any) => void) {
176+
this._onmessage = listener;
177+
}
178+
179+
// Wrap port.postMessage to maintain 'this' context
180+
postMessage = (message: any): void => {
181+
this.port.postMessage(message);
182+
};
183+
}
184+
156185
/**
157186
* A client of Engine that exposes the same interface
158187
*/
159188
export class ServiceWorkerEngine extends WebWorkerEngine {
160-
missedHeatbeat = 0
161-
162-
constructor(worker: ChatWorker, keepAliveMs=10000) {
163-
super(worker);
164-
clientBroadcastChannel.onmessage = (event) => {
165-
try {
166-
this.onevent.bind(this)(event)
167-
} catch (err: any) {
168-
// This is expected to throw if user has multiple windows open
169-
if (!err.message.startsWith("return from a unknown uuid")) {
170-
console.error("CreateWebServiceWorkerEngine.onmessage", err);
171-
}
172-
}
173-
};
189+
port: chrome.runtime.Port;
190+
191+
constructor(keepAliveMs: number = 10000) {
192+
let port = chrome.runtime.connect({ name: "web_llm_service_worker" });
193+
let chatWorker = new PortAdapter(port);
194+
super(chatWorker);
195+
this.port = port;
174196
setInterval(() => {
175197
this.keepAlive();
176198
}, keepAliveMs);
@@ -180,15 +202,6 @@ export class ServiceWorkerEngine extends WebWorkerEngine {
180202
this.worker.postMessage({ kind: "keepAlive" });
181203
}
182204

183-
onevent(event: MessageEvent): void {
184-
const msg = event.data as WorkerMessage;
185-
if (msg.kind === "heartbeat") {
186-
this.missedHeatbeat = 0
187-
return;
188-
}
189-
this.onmessage(msg);
190-
}
191-
192205
/**
193206
* Initialize the chat with a model.
194207
*
@@ -205,7 +218,7 @@ export class ServiceWorkerEngine extends WebWorkerEngine {
205218
chatOpts?: ChatOptions,
206219
appConfig?: AppConfig
207220
): Promise<void> {
208-
const msg: WorkerMessage = {
221+
const msg: WorkerRequest = {
209222
kind: "init",
210223
uuid: crypto.randomUUID(),
211224
content: {

‎src/index.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,24 +33,23 @@ export {
3333
} from "./web_worker";
3434

3535
export {
36-
WorkerMessage,
36+
WorkerRequest,
37+
WorkerResponse,
3738
CustomRequestParams
3839
} from "./message"
3940

40-
// TODO: Rename to ExtensionServiceWorker
41-
export {
42-
ServiceWorkerEngineHandler,
43-
ServiceWorkerEngine,
44-
CreateServiceWorkerEngine,
45-
} from './service_worker'
46-
47-
// TODO: Rename to ServiceWorker
41+
// TODO: Rename classes to ServiceWorker
4842
export {
4943
ServiceWorkerEngineHandler as WebServiceWorkerEngineHandler,
5044
ServiceWorkerEngine as WebServiceWorkerEngine,
5145
CreateServiceWorkerEngine as CreateWebServiceWorkerEngine,
52-
serviceWorkerBroadcastChannel,
53-
clientBroadcastChannel,
54-
} from "./web_service_worker";
46+
} from "./service_worker";
47+
48+
// TODO: Rename classes to ExtensionServiceWorker
49+
export {
50+
ServiceWorkerEngineHandler,
51+
ServiceWorkerEngine,
52+
CreateServiceWorkerEngine,
53+
} from './extension_service_worker'
5554

5655
export * from './openai_api_protocols/index';

‎src/message.ts

Lines changed: 68 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,36 @@ import {
44
ChatCompletionRequestStreaming,
55
ChatCompletionRequestNonStreaming,
66
ChatCompletion,
7-
ChatCompletionChunk
7+
ChatCompletionChunk,
88
} from "./openai_api_protocols/index";
99

1010
/**
1111
* Message kind used by worker
1212
*/
13-
type RequestKind = (
14-
"return" | "throw" |
15-
"reload" | "generate" | "runtimeStatsText" |
16-
"interruptGenerate" | "unload" | "resetChat" | "init" |
17-
"initProgressCallback" | "generateProgressCallback" | "getMaxStorageBufferBindingSize" |
18-
"getGPUVendor" | "forwardTokensAndSample" | "chatCompletionNonStreaming" | "getMessage" |
19-
"chatCompletionStreamInit" | "chatCompletionStreamNextChunk" | "customRequest" | 'keepAlive' | 'heartbeat');
13+
type RequestKind =
14+
| "reload"
15+
| "generate"
16+
| "runtimeStatsText"
17+
| "interruptGenerate"
18+
| "unload"
19+
| "resetChat"
20+
| "init"
21+
| "getMaxStorageBufferBindingSize"
22+
| "getGPUVendor"
23+
| "forwardTokensAndSample"
24+
| "chatCompletionNonStreaming"
25+
| "getMessage"
26+
| "chatCompletionStreamInit"
27+
| "chatCompletionStreamNextChunk"
28+
| "customRequest"
29+
| "keepAlive"
30+
| "heartbeat";
31+
32+
type ResponseKind =
33+
| "return"
34+
| "throw"
35+
| "initProgressCallback"
36+
| "generateProgressCallback";
2037

2138
export interface ReloadParams {
2239
modelId: string;
@@ -51,28 +68,54 @@ export interface CustomRequestParams {
5168
requestMessage: string;
5269
}
5370
export type MessageContent =
54-
GenerateProgressCallbackParams |
55-
ReloadParams |
56-
GenerateParams |
57-
ResetChatParams |
58-
ForwardTokensAndSampleParams |
59-
ChatCompletionNonStreamingParams |
60-
ChatCompletionStreamInitParams |
61-
CustomRequestParams |
62-
InitProgressReport |
63-
string |
64-
null |
65-
number |
66-
ChatCompletion |
67-
ChatCompletionChunk |
68-
void;
71+
| GenerateProgressCallbackParams
72+
| ReloadParams
73+
| GenerateParams
74+
| ResetChatParams
75+
| ForwardTokensAndSampleParams
76+
| ChatCompletionNonStreamingParams
77+
| ChatCompletionStreamInitParams
78+
| CustomRequestParams
79+
| InitProgressReport
80+
| string
81+
| null
82+
| number
83+
| ChatCompletion
84+
| ChatCompletionChunk
85+
| void;
6986
/**
7087
* The message used in exchange between worker
7188
* and the main thread.
7289
*/
7390

74-
export interface WorkerMessage {
91+
export type WorkerRequest = {
7592
kind: RequestKind;
7693
uuid: string;
7794
content: MessageContent;
78-
}
95+
};
96+
97+
export type OneTimeWorkerResponse = {
98+
kind: "return" | "throw";
99+
uuid: string;
100+
content: MessageContent;
101+
};
102+
103+
export type InitProgressWorkerResponse = {
104+
kind: "initProgressCallback";
105+
uuid: string;
106+
content: InitProgressReport;
107+
};
108+
109+
export type GenerateProgressWorkerResponse = {
110+
kind: "generateProgressCallback";
111+
uuid: string;
112+
content: {
113+
step: number;
114+
currentMessage: string;
115+
};
116+
};
117+
118+
export type WorkerResponse =
119+
| OneTimeWorkerResponse
120+
| InitProgressWorkerResponse
121+
| GenerateProgressWorkerResponse;

‎src/service_worker.ts

Lines changed: 137 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,19 @@
11
import * as tvmjs from "tvmjs";
2-
import { AppConfig, ChatOptions, EngineConfig } from "./config";
3-
import { ReloadParams, WorkerMessage } from "./message";
4-
import { EngineInterface } from "./types";
5-
import {
6-
ChatWorker,
7-
EngineWorkerHandler,
8-
WebWorkerEngine,
9-
PostMessageHandler,
10-
} from "./web_worker";
2+
import { AppConfig, ChatOptions, EngineConfig, ModelRecord } from "./config";
3+
import { ReloadParams, WorkerRequest, WorkerResponse } from "./message";
4+
import { EngineInterface, InitProgressReport } from "./types";
5+
import { EngineWorkerHandler, WebWorkerEngine, ChatWorker } from "./web_worker";
116
import { areAppConfigsEqual, areChatOptionsEqual } from "./utils";
127

13-
/**
14-
* A post message handler that sends messages to a chrome.runtime.Port.
15-
*/
16-
export class PortPostMessageHandler implements PostMessageHandler {
17-
port: chrome.runtime.Port;
18-
enabled: boolean = true;
19-
20-
constructor(port: chrome.runtime.Port) {
21-
this.port = port;
22-
}
23-
24-
/**
25-
* Close the PortPostMessageHandler. This will prevent any further messages
26-
*/
27-
close() {
28-
this.enabled = false;
29-
}
8+
/* Service Worker Script */
309

31-
postMessage(event: any) {
32-
if (this.enabled) {
33-
this.port.postMessage(event);
34-
}
35-
}
36-
}
10+
type IServiceWorker = globalThis.ServiceWorker;
3711

3812
/**
3913
* Worker handler that can be used in a ServiceWorker.
40-
*
14+
*
4115
* @example
42-
*
16+
*
4317
* const engine = new Engine();
4418
* let handler;
4519
* chrome.runtime.onConnect.addListener(function (port) {
@@ -56,29 +30,74 @@ export class ServiceWorkerEngineHandler extends EngineWorkerHandler {
5630
chatOpts?: ChatOptions;
5731
appConfig?: AppConfig;
5832

59-
constructor(engine: EngineInterface, port: chrome.runtime.Port) {
60-
let portHandler = new PortPostMessageHandler(port);
61-
super(engine, portHandler);
33+
private clientRegistry = new Map<
34+
string,
35+
IServiceWorker | Client | MessagePort
36+
>();
37+
private initReuqestUuid?: string;
6238

63-
port.onDisconnect.addListener(() => {
64-
portHandler.close();
65-
});
66-
}
39+
constructor(engine: EngineInterface) {
40+
if (!self || !("addEventListener" in self)) {
41+
throw new Error(
42+
"ServiceWorkerGlobalScope is not defined. ServiceWorkerEngineHandler must be created in service worker script."
43+
);
44+
}
45+
const postMessageHandler = {
46+
postMessage: (message: WorkerResponse) => {
47+
if (this.clientRegistry.has(message.uuid)) {
48+
const client = this.clientRegistry.get(message.uuid);
49+
client?.postMessage(message);
6750

68-
setPort(port: chrome.runtime.Port) {
69-
let portHandler = new PortPostMessageHandler(port);
70-
this.setPostMessageHandler(portHandler);
71-
port.onDisconnect.addListener(() => {
72-
portHandler.close();
51+
if (message.kind === "return" || message.kind === "throw") {
52+
this.clientRegistry.delete(message.uuid);
53+
} else {
54+
// TODO: Delete clientRegistry after complete to avoid memory leak?
55+
}
56+
}
57+
},
58+
};
59+
const initProgressCallback = (report: InitProgressReport) => {
60+
const msg: WorkerResponse = {
61+
kind: "initProgressCallback",
62+
uuid: this.initReuqestUuid || "",
63+
content: report,
64+
};
65+
this.postMessageInternal(msg);
66+
};
67+
super(engine, postMessageHandler, initProgressCallback);
68+
const onmessage = this.onmessage.bind(this);
69+
70+
self.addEventListener("message", (event) => {
71+
const message = event as unknown as ExtendableMessageEvent;
72+
if (message.source) {
73+
this.clientRegistry.set(message.data.uuid, message.source);
74+
}
75+
message.waitUntil(
76+
new Promise((resolve, reject) => {
77+
onmessage(message, resolve, reject);
78+
})
79+
);
7380
});
7481
}
7582

76-
onmessage(event: any): void {
77-
if (event.type === "keepAlive") {
83+
onmessage(
84+
event: ExtendableMessageEvent,
85+
onComplete?: (value: any) => void,
86+
onError?: () => void
87+
): void {
88+
const msg = event.data as WorkerRequest;
89+
90+
if (msg.kind === "keepAlive") {
91+
const reply: WorkerRequest = {
92+
kind: "heartbeat",
93+
uuid: msg.uuid,
94+
content: "",
95+
};
96+
this.postMessageInternal(reply);
97+
onComplete?.(reply);
7898
return;
7999
}
80100

81-
const msg = event as WorkerMessage;
82101
if (msg.kind === "init") {
83102
this.handleTask(msg.uuid, async () => {
84103
const params = msg.content as ReloadParams;
@@ -104,9 +123,11 @@ export class ServiceWorkerEngineHandler extends EngineWorkerHandler {
104123
timeElapsed: 0,
105124
text: "Finish loading on " + gpuLabel,
106125
});
126+
onComplete?.(null);
107127
return null;
108128
}
109129

130+
this.initReuqestUuid = msg.uuid;
110131
await this.engine.reload(
111132
params.modelId,
112133
params.chatOpts,
@@ -115,31 +136,60 @@ export class ServiceWorkerEngineHandler extends EngineWorkerHandler {
115136
this.modelId = params.modelId;
116137
this.chatOpts = params.chatOpts;
117138
this.appConfig = params.appConfig;
139+
onComplete?.(null);
118140
return null;
119141
});
120142
return;
121143
}
122-
super.onmessage(event);
144+
super.onmessage(msg, onComplete, onError);
145+
}
146+
}
147+
148+
/* Webapp Client */
149+
/**
150+
* PostMessageHandler wrapper for sending message from client to service worker
151+
*/
152+
export class ServiceWorker implements ChatWorker {
153+
serviceWorker: IServiceWorker;
154+
155+
constructor(serviceWorker: IServiceWorker) {
156+
this.serviceWorker = serviceWorker;
157+
}
158+
159+
// ServiceWorkerEngine will later overwrite this
160+
onmessage() {}
161+
162+
postMessage(message: WorkerRequest) {
163+
if (!("serviceWorker" in navigator)) {
164+
throw new Error("Service worker API is not available");
165+
}
166+
const serviceWorker = (navigator.serviceWorker as ServiceWorkerContainer)
167+
.controller;
168+
if (!serviceWorker) {
169+
throw new Error("There is no active service worker");
170+
}
171+
serviceWorker.postMessage(message);
123172
}
124173
}
125174

126175
/**
127176
* Create a ServiceWorkerEngine.
128-
*
177+
*
129178
* @param modelId The model to load, needs to either be in `webllm.prebuiltAppConfig`, or in
130179
* `engineConfig.appConfig`.
131180
* @param engineConfig Optionally configures the engine, see `webllm.EngineConfig` for more.
132-
* @param keepAliveMs The interval to send keep alive messages to the service worker.
133-
* See [Service worker lifecycle](https://developer.chrome.com/docs/extensions/develop/concepts/service-workers/lifecycle#idle-shutdown)
134-
* The default is 10s.
135181
* @returns An initialized `WebLLM.ServiceWorkerEngine` with `modelId` loaded.
136182
*/
137183
export async function CreateServiceWorkerEngine(
138184
modelId: string,
139-
engineConfig?: EngineConfig,
140-
keepAliveMs: number = 10000
185+
engineConfig?: EngineConfig
141186
): Promise<ServiceWorkerEngine> {
142-
const serviceWorkerEngine = new ServiceWorkerEngine(keepAliveMs);
187+
if (!("serviceWorker" in navigator)) {
188+
throw new Error("Service worker API is not available");
189+
}
190+
const registration = await (navigator.serviceWorker as ServiceWorkerContainer)
191+
.ready;
192+
const serviceWorkerEngine = new ServiceWorkerEngine(registration.active!);
143193
serviceWorkerEngine.setInitProgressCallback(
144194
engineConfig?.initProgressCallback
145195
);
@@ -151,57 +201,44 @@ export async function CreateServiceWorkerEngine(
151201
return serviceWorkerEngine;
152202
}
153203

154-
class PortAdapter implements ChatWorker {
155-
port: chrome.runtime.Port;
156-
private _onmessage!: (message: any) => void;
157-
158-
constructor(port: chrome.runtime.Port) {
159-
this.port = port;
160-
this.port.onMessage.addListener(this.handleMessage.bind(this));
161-
}
162-
163-
// Wrapper to handle incoming messages and delegate to onmessage if available
164-
private handleMessage(message: any) {
165-
if (this._onmessage) {
166-
this._onmessage(message);
167-
}
168-
}
169-
170-
// Getter and setter for onmessage to manage adding/removing listeners
171-
get onmessage(): (message: any) => void {
172-
return this._onmessage;
173-
}
174-
175-
set onmessage(listener: (message: any) => void) {
176-
this._onmessage = listener;
177-
}
178-
179-
// Wrap port.postMessage to maintain 'this' context
180-
postMessage = (message: any): void => {
181-
this.port.postMessage(message);
182-
};
183-
}
184-
185204
/**
186205
* A client of Engine that exposes the same interface
187206
*/
188207
export class ServiceWorkerEngine extends WebWorkerEngine {
189-
port: chrome.runtime.Port;
208+
missedHeatbeat = 0;
209+
210+
constructor(worker: IServiceWorker, keepAliveMs = 10000) {
211+
if (!("serviceWorker" in navigator)) {
212+
throw new Error("Service worker API is not available");
213+
}
214+
super(new ServiceWorker(worker));
215+
const onmessage = this.onmessage.bind(this);
216+
217+
(navigator.serviceWorker as ServiceWorkerContainer).addEventListener(
218+
"message",
219+
(event: MessageEvent) => {
220+
const msg = event.data;
221+
try {
222+
if (msg.kind === "heartbeat") {
223+
this.missedHeatbeat = 0;
224+
return;
225+
}
226+
onmessage(msg);
227+
} catch (err: any) {
228+
// This is expected to throw if user has multiple windows open
229+
if (!err.message.startsWith("return from a unknown uuid")) {
230+
console.error("CreateWebServiceWorkerEngine.onmessage", err);
231+
}
232+
}
233+
}
234+
);
190235

191-
constructor(keepAliveMs: number = 10000) {
192-
let port = chrome.runtime.connect({ name: "web_llm_service_worker" });
193-
let chatWorker = new PortAdapter(port);
194-
super(chatWorker);
195-
this.port = port;
196236
setInterval(() => {
197-
this.keepAlive();
237+
this.worker.postMessage({ kind: "keepAlive", uuid: crypto.randomUUID() });
238+
this.missedHeatbeat += 1;
198239
}, keepAliveMs);
199240
}
200241

201-
keepAlive() {
202-
this.worker.postMessage({ kind: "keepAlive" });
203-
}
204-
205242
/**
206243
* Initialize the chat with a model.
207244
*
@@ -218,7 +255,7 @@ export class ServiceWorkerEngine extends WebWorkerEngine {
218255
chatOpts?: ChatOptions,
219256
appConfig?: AppConfig
220257
): Promise<void> {
221-
const msg: WorkerMessage = {
258+
const msg: WorkerRequest = {
222259
kind: "init",
223260
uuid: crypto.randomUUID(),
224261
content: {

‎src/web_worker.ts

Lines changed: 178 additions & 92 deletions
Large diffs are not rendered by default.

‎tsconfig.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
"sourceMap": true,
88
"strict": true,
99
"moduleResolution": "Node",
10-
"esModuleInterop": true
10+
"esModuleInterop": true,
11+
"lib": ["dom", "WebWorker"]
1112
},
12-
"typeRoots": [ "./node_modules/@webgpu/types", "./node_modules/@types"],
13+
"typeRoots": ["./node_modules/@webgpu/types", "./node_modules/@types"],
1314
"include": ["src"],
1415
"exclude": ["node_modules", "build", "dist", "rollup.config.cjs"]
1516
}

0 commit comments

Comments
 (0)
Please sign in to comment.