Skip to content

Commit d64da15

Browse files
committed
refactor: use SharedWorker (closes #13)
1 parent 7fd079a commit d64da15

File tree

4 files changed

+99
-94
lines changed

4 files changed

+99
-94
lines changed

packages/idb-cache/src/encryptionTasks.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
} from "./errors";
99

1010
/**
11-
* Encrypts a chunk of data using the worker.
11+
* Encrypts a chunk of data using the SharedWorker.
1212
* @param port - The MessagePort instance.
1313
* @param value - The plaintext string to encrypt.
1414
* @param pendingRequests - Map of pending requests awaiting responses.
@@ -50,7 +50,7 @@ export async function encryptChunk(
5050
}
5151

5252
/**
53-
* Decrypts a chunk of data using the worker.
53+
* Decrypts a chunk of data using the SharedWorker.
5454
* @param port - The MessagePort instance.
5555
* @param iv - The Initialization Vector used during encryption.
5656
* @param ciphertext - The encrypted data.

packages/idb-cache/src/encryptionWorkerFn.ts

Lines changed: 69 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33

44
import type { WorkerMessage } from "./types";
55

6+
declare const self: SharedWorkerGlobalScope;
7+
68
export function encryptionWorkerFunction() {
79
let cacheKey: Uint8Array | null = null;
810
const derivedKeyCache: Map<string, CryptoKey> = new Map();
911
let pbkdf2Iterations = 100000;
10-
let port: MessagePort | null = null;
1112
let fixedSalt: ArrayBuffer | null = null;
1213

1314
async function getKeyFromCacheKey(
@@ -59,12 +60,16 @@ export function encryptionWorkerFunction() {
5960
throw new Error("Cache key not provided for encryption worker");
6061
}
6162
try {
62-
port?.postMessage({ type: "ready" });
63+
for (const port of ports) {
64+
port.postMessage({ type: "ready" });
65+
}
6366
} catch (error: unknown) {
6467
console.error("Worker: Failed to initialize AES key:", error);
6568
const errorMessage =
6669
error instanceof Error ? error.message : "Unknown initialization error";
67-
port?.postMessage({ type: "initError", error: errorMessage });
70+
for (const port of ports) {
71+
port.postMessage({ type: "initError", error: errorMessage });
72+
}
6873
}
6974
}
7075

@@ -158,7 +163,7 @@ export function encryptionWorkerFunction() {
158163
}
159164
}
160165

161-
function handleEncrypt(requestId: string, value: string) {
166+
function handleEncrypt(requestId: string, value: string, port: MessagePort) {
162167
enqueueTask(async () => {
163168
try {
164169
const encrypted = await encrypt(value);
@@ -186,7 +191,8 @@ export function encryptionWorkerFunction() {
186191
function handleDecrypt(
187192
requestId: string,
188193
iv: ArrayBuffer,
189-
ciphertext: ArrayBuffer
194+
ciphertext: ArrayBuffer,
195+
port: MessagePort
190196
) {
191197
enqueueTask(async () => {
192198
try {
@@ -209,73 +215,71 @@ export function encryptionWorkerFunction() {
209215
});
210216
}
211217

212-
async function onMessage(e: MessageEvent<WorkerMessage>) {
213-
const { type, payload, requestId } = e.data;
214-
215-
switch (type) {
216-
case "initialize":
217-
{
218-
const {
219-
cacheKey: incomingCacheKey,
220-
pbkdf2Iterations: incomingIterations,
221-
cacheBuster,
222-
} = payload;
223-
cacheKey = new TextEncoder().encode(incomingCacheKey);
224-
pbkdf2Iterations = incomingIterations || 100000;
225-
fixedSalt = new TextEncoder().encode(cacheBuster).buffer;
226-
await initializeKey();
227-
}
228-
break;
229-
230-
case "encrypt":
231-
{
232-
const { value } = payload;
233-
await handleEncrypt(requestId, value);
234-
}
235-
break;
218+
const ports: MessagePort[] = [];
236219

237-
case "decrypt":
238-
{
239-
const { iv, ciphertext } = payload;
240-
await handleDecrypt(requestId, iv, ciphertext);
241-
}
242-
break;
220+
function onConnect(e: MessageEvent) {
221+
const port = e.ports[0];
222+
ports.push(port);
223+
port.onmessage = (event: MessageEvent<WorkerMessage>) => {
224+
const { type, payload, requestId } = event.data;
243225

244-
case "destroy":
245-
{
246-
if (cacheKey) {
247-
cacheKey.fill(0);
248-
cacheKey = null;
249-
}
250-
if (fixedSalt) {
251-
const saltArray = new Uint8Array(fixedSalt);
252-
saltArray.fill(0);
253-
fixedSalt = null;
226+
switch (type) {
227+
case "initialize":
228+
{
229+
const {
230+
cacheKey: incomingCacheKey,
231+
pbkdf2Iterations: incomingIterations,
232+
cacheBuster,
233+
} = payload;
234+
cacheKey = new TextEncoder().encode(incomingCacheKey);
235+
pbkdf2Iterations = incomingIterations || 100000;
236+
fixedSalt = new TextEncoder().encode(cacheBuster).buffer;
237+
initializeKey().catch((error) => {
238+
console.error("Worker: Initialization failed:", error);
239+
});
254240
}
255-
if (port) {
256-
port.close();
257-
port = null;
241+
break;
242+
243+
case "encrypt":
244+
{
245+
const { value } = payload;
246+
handleEncrypt(requestId, value, port);
258247
}
259-
self.close();
260-
}
261-
break;
248+
break;
262249

263-
default:
264-
console.warn(
265-
`Worker: Unknown message type received: ${type}. Ignoring the message.`
266-
);
267-
}
268-
}
250+
case "decrypt":
251+
{
252+
const { iv, ciphertext } = payload;
253+
handleDecrypt(requestId, iv, ciphertext, port);
254+
}
255+
break;
269256

270-
function handleInit(e: MessageEvent) {
271-
const { type } = e.data;
257+
case "destroy":
258+
{
259+
if (cacheKey) {
260+
cacheKey.fill(0);
261+
cacheKey = null;
262+
}
263+
if (fixedSalt) {
264+
const saltArray = new Uint8Array(fixedSalt);
265+
saltArray.fill(0);
266+
fixedSalt = null;
267+
}
268+
for (const p of ports) {
269+
p.close();
270+
}
271+
self.close();
272+
}
273+
break;
272274

273-
if (type === "init" && e.ports && e.ports.length > 0) {
274-
port = e.ports[0];
275-
port.onmessage = onMessage;
276-
port.start();
277-
}
275+
default:
276+
console.warn(
277+
`Worker: Unknown message type received: ${type}. Ignoring the message.`
278+
);
279+
}
280+
};
281+
port.start();
278282
}
279283

280-
self.onmessage = handleInit;
284+
self.onconnect = onConnect;
281285
}

packages/idb-cache/src/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ const isSubtleCryptoSupported = crypto?.subtle;
104104
export class IDBCache implements IDBCacheInterface {
105105
dbReadyPromise: Promise<import("idb").IDBPDatabase<IDBCacheSchema>>;
106106
private storeName: STORE;
107-
private worker: Worker | null = null;
107+
private worker: SharedWorker | null = null;
108108
private port: MessagePort | null = null;
109109
private pendingRequests: Map<
110110
string,
@@ -183,7 +183,7 @@ export class IDBCache implements IDBCacheInterface {
183183
}
184184

185185
/**
186-
* Initializes the worker by creating it, setting up communication, and handling initialization.
186+
* Initializes the SharedWorker by creating it, setting up communication, and handling initialization.
187187
* @param cacheKey - The cache key used for encryption/decryption.
188188
* @param cacheBuster - The cacheBuster used as a fixed salt.
189189
* @throws {WorkerInitializationError} If the worker fails to initialize.
@@ -324,7 +324,7 @@ export class IDBCache implements IDBCacheInterface {
324324

325325
// Define key range for this baseKey
326326
const lowerBound = `${baseKey}-chunk-000000-`;
327-
const upperBound = `${baseKey}-chunk-999999\uffff`;
327+
const upperBound = `${baseKey}-chunk-999999`;
328328
const range = IDBKeyRange.bound(
329329
lowerBound,
330330
upperBound,
@@ -800,7 +800,7 @@ export class IDBCache implements IDBCacheInterface {
800800
}
801801

802802
/**
803-
* Destroys the IDBCache instance by clearing data (optional), releasing resources, and terminating the worker.
803+
* Destroys the IDBCache instance by clearing data (optional), releasing resources, and terminating the SharedWorker.
804804
* @param options - Configuration options for destruction.
805805
* @param options.clearData - Whether to clear all cached data before destruction.
806806
* @throws {DatabaseError} If there is an issue accessing the database during data clearing.
@@ -831,7 +831,7 @@ export class IDBCache implements IDBCacheInterface {
831831
}
832832

833833
if (this.worker) {
834-
this.worker.terminate();
834+
this.worker.port.close();
835835
this.worker = null;
836836
}
837837

packages/idb-cache/src/workerUtils.ts

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import {
1414
IDBCacheError,
1515
} from "./errors";
1616

17+
/**
18+
* Utility type guards for Worker responses
19+
*/
1720
function isReadyResponse(
1821
message: WorkerResponse
1922
): message is { type: "ready" } {
@@ -51,7 +54,7 @@ function isErrorResponse(
5154
}
5255

5356
/**
54-
* Creates a worker from a given function and sets up initial communication.
57+
* Creates a SharedWorker from a given function and sets up initial communication.
5558
* @param fn - The worker function to execute.
5659
* @param rejectAll - Function to call to reject all pending requests in case of failure.
5760
* @returns An object containing the worker instance and its message port.
@@ -60,38 +63,34 @@ export function createWorkerFromFunction(
6063
fn: () => void,
6164
rejectAll: (errorMessage: string) => void
6265
): {
63-
worker: Worker;
66+
worker: SharedWorker;
6467
port: MessagePort;
6568
} {
6669
const blob = new Blob([`(${fn.toString()})()`], {
6770
type: "application/javascript",
6871
});
6972
const url = URL.createObjectURL(blob);
70-
const worker = new Worker(url);
71-
72-
const channel = new MessageChannel();
73+
const worker = new SharedWorker(url);
7374

74-
worker.postMessage({ type: "init" }, [channel.port2]);
75+
const port = worker.port;
7576

76-
worker.onmessage = () => {
77-
URL.revokeObjectURL(url);
78-
};
77+
port.start();
7978

8079
worker.onerror = (event) => {
81-
console.error("Worker encountered an error:", event.message);
82-
rejectAll("Worker encountered an error and was terminated.");
83-
worker.terminate();
80+
console.error("SharedWorker encountered an error:", event.message);
81+
rejectAll("SharedWorker encountered an error and was terminated.");
82+
worker.port.close();
8483
};
8584

86-
channel.port1.onmessageerror = () => {
85+
port.onmessageerror = () => {
8786
console.warn(
88-
"MessagePort encountered a message error. Worker may have been terminated."
87+
"MessagePort encountered a message error. SharedWorker may have been terminated."
8988
);
90-
rejectAll("Worker was terminated unexpectedly.");
91-
channel.port1.close();
89+
rejectAll("SharedWorker was terminated unexpectedly.");
90+
port.close();
9291
};
9392

94-
return { worker, port: channel.port1 };
93+
return { worker, port };
9594
}
9695

9796
/**
@@ -176,12 +175,14 @@ export function initializeWorker(
176175
};
177176

178177
port.onmessageerror = (e: MessageEvent) => {
179-
console.error("Worker encountered a message error:", e);
180-
const error = new WorkerInitializationError("Worker failed to initialize");
178+
console.error("SharedWorker encountered a message error:", e);
179+
const error = new WorkerInitializationError(
180+
"SharedWorker failed to communicate properly."
181+
);
181182
rejectReady(error);
182183
rejectAllPendingRequests(
183184
pendingRequests,
184-
"Worker encountered an error and was terminated."
185+
"SharedWorker encountered an error and was terminated."
185186
);
186187
port.close();
187188
};
@@ -229,13 +230,13 @@ export async function sendMessageToWorker<T extends WorkerMessage["type"]>(
229230
port.postMessage(message);
230231
}
231232
} catch (error) {
232-
console.error("Failed to post message to worker:", error);
233+
console.error("Failed to post message to SharedWorker:", error);
233234
const pending = pendingRequests.get(requestId);
234235
if (pending) {
235236
clearTimeout(pending.timer);
236237
pending.reject(
237238
new WorkerInitializationError(
238-
"Failed to communicate with the worker."
239+
"Failed to communicate with the SharedWorker."
239240
)
240241
);
241242
pendingRequests.delete(requestId);

0 commit comments

Comments
 (0)