mirror of
https://github.com/immich-app/immich.git
synced 2026-01-18 15:55:56 -08:00
151 lines
4.7 KiB
TypeScript
151 lines
4.7 KiB
TypeScript
/**
|
|
* Low-level protocol for communicating with the service worker via postMessage.
|
|
*
|
|
* Protocol:
|
|
* 1. Main thread sends request: { type: string, requestId: string, ...data }
|
|
* 2. SW sends ack: { type: 'ack', requestId: string }
|
|
* 3. SW sends response (optional): { type: 'response', requestId: string, result?: any, error?: string }
|
|
*/
|
|
|
|
interface PendingRequest {
|
|
resolveAck: () => void;
|
|
resolveResponse?: (result: unknown) => void;
|
|
rejectResponse?: (error: Error) => void;
|
|
ackTimeout: ReturnType<typeof setTimeout>;
|
|
ackReceived: boolean;
|
|
}
|
|
|
|
export class ServiceWorkerMessenger {
|
|
readonly #pendingRequests = new Map<string, PendingRequest>();
|
|
readonly #ackTimeoutMs: number;
|
|
#requestCounter = 0;
|
|
#onTimeout?: (type: string, data: Record<string, unknown>) => void;
|
|
|
|
constructor(_channelName: string, ackTimeoutMs = 5000) {
|
|
this.#ackTimeoutMs = ackTimeoutMs;
|
|
|
|
// Listen for messages from the service worker
|
|
if ('serviceWorker' in navigator) {
|
|
navigator.serviceWorker.addEventListener('message', (event) => {
|
|
this.#handleMessage(event.data);
|
|
});
|
|
}
|
|
}
|
|
|
|
#handleMessage(data: unknown) {
|
|
if (typeof data !== 'object' || data === null) {
|
|
return;
|
|
}
|
|
|
|
const message = data as { requestId?: string; type?: string; error?: string; result?: unknown };
|
|
const requestId = message.requestId;
|
|
if (!requestId) {
|
|
return;
|
|
}
|
|
|
|
const pending = this.#pendingRequests.get(requestId);
|
|
if (!pending) {
|
|
return;
|
|
}
|
|
|
|
if (message.type === 'ack') {
|
|
pending.ackReceived = true;
|
|
clearTimeout(pending.ackTimeout);
|
|
pending.resolveAck();
|
|
return;
|
|
}
|
|
|
|
if (message.type === 'response') {
|
|
clearTimeout(pending.ackTimeout);
|
|
this.#pendingRequests.delete(requestId);
|
|
|
|
if (message.error) {
|
|
pending.rejectResponse?.(new Error(message.error));
|
|
return;
|
|
}
|
|
|
|
pending.resolveResponse?.(message.result);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set a callback to be invoked when an ack timeout occurs.
|
|
* This can be used to detect and disable faulty service workers.
|
|
*/
|
|
onAckTimeout(callback: (type: string, data: Record<string, unknown>) => void): void {
|
|
this.#onTimeout = callback;
|
|
}
|
|
|
|
/**
|
|
* Send a message to the service worker.
|
|
* - send(): waits for ack, resolves when acknowledged
|
|
* - request(): waits for response, throws on error/timeout
|
|
*/
|
|
#sendInternal<T>(type: string, data: Record<string, unknown>, waitForResponse: boolean): Promise<T> {
|
|
const requestId = `${type}-${++this.#requestCounter}-${Date.now()}`;
|
|
|
|
const promise = new Promise<T>((resolve, reject) => {
|
|
const ackTimeout = setTimeout(() => {
|
|
const pending = this.#pendingRequests.get(requestId);
|
|
if (pending && !pending.ackReceived) {
|
|
this.#pendingRequests.delete(requestId);
|
|
console.warn(`[ServiceWorker] ${type} request not acknowledged:`, data);
|
|
this.#onTimeout?.(type, data);
|
|
// Only reject if we're waiting for a response
|
|
if (waitForResponse) {
|
|
reject(new Error(`Service worker did not acknowledge ${type} request`));
|
|
} else {
|
|
pending.resolveAck();
|
|
}
|
|
}
|
|
}, this.#ackTimeoutMs);
|
|
|
|
this.#pendingRequests.set(requestId, {
|
|
resolveAck: waitForResponse ? () => {} : () => resolve(undefined as T),
|
|
resolveResponse: waitForResponse ? (result: unknown) => resolve(result as T) : undefined,
|
|
rejectResponse: waitForResponse ? reject : undefined,
|
|
ackTimeout,
|
|
ackReceived: false,
|
|
});
|
|
|
|
// Send message to the active service worker
|
|
// Feature detection is done in constructor and at call sites (sw-messaging.ts:isValidSwContext)
|
|
// eslint-disable-next-line compat/compat
|
|
navigator.serviceWorker.controller?.postMessage({
|
|
type,
|
|
requestId,
|
|
...data,
|
|
});
|
|
});
|
|
|
|
return promise;
|
|
}
|
|
|
|
/**
|
|
* Send a one-way message to the service worker.
|
|
* Returns a promise that resolves after the service worker acknowledges receipt.
|
|
* Rejects if no ack is received within the timeout period.
|
|
*/
|
|
send(type: string, data: Record<string, unknown>): Promise<void> {
|
|
return this.#sendInternal<void>(type, data, false);
|
|
}
|
|
|
|
/**
|
|
* Send a request and wait for ack + response.
|
|
* Returns a promise that resolves with the response data or rejects on error/timeout.
|
|
*/
|
|
request<T = void>(type: string, data: Record<string, unknown>): Promise<T> {
|
|
return this.#sendInternal<T>(type, data, true);
|
|
}
|
|
|
|
/**
|
|
* Clean up pending requests
|
|
*/
|
|
close(): void {
|
|
for (const pending of this.#pendingRequests.values()) {
|
|
clearTimeout(pending.ackTimeout);
|
|
}
|
|
this.#pendingRequests.clear();
|
|
}
|
|
}
|