data-transport
Version:
A simple and responsive transport
151 lines (139 loc) • 4.13 kB
text/typescript
import {
TransportOptions,
TransferableWorker,
ListenerOptions,
BaseInteraction,
} from '../interface';
import { Transport } from '../transport';
import { detectSafari } from '../utils';
// follow issue: https://github.com/microsoft/TypeScript/issues/20595
// workaround: `tsc --skipLibCheck`.
declare var self: ServiceWorkerGlobalScope;
interface ServiceWorkerClientId extends TransferableWorker {
_clientId?: string;
}
export interface ServiceWorkerClientTransportOptions
extends Partial<TransportOptions<TransferableWorker>> {
/**
* A service worker instance for data transport.
*/
worker: ServiceWorker;
/**
* Compatibility with unstable serialization in Safari
*/
useOnSafari?: boolean;
}
export interface ServiceWorkerServiceTransportOptions
extends Partial<TransportOptions<ServiceWorkerClientId>> {
/**
* Compatibility with unstable serialization in Safari
*/
useOnSafari?: boolean;
}
const DEFAULT_USE_ON_SAFARI = true;
const decode = (data: any, useOnSafari: boolean) => {
try {
return useOnSafari && detectSafari() ? JSON.stringify(data) : data;
} catch (e) {
console.error(`Failed to stringify:`, data);
throw e;
}
};
const encode = (data: any, useOnSafari: boolean) => {
try {
return typeof data === 'string' && useOnSafari && detectSafari()
? JSON.parse(data as any)
: data;
} catch (e) {
console.error(`Failed to parse:`, data);
}
return data;
};
export abstract class ServiceWorkerClientTransport<
T extends BaseInteraction = any
> extends Transport<T> {
constructor(_options: ServiceWorkerClientTransportOptions) {
const {
worker,
useOnSafari = DEFAULT_USE_ON_SAFARI,
listener = (callback) => {
const handler = ({
data,
}: MessageEvent<ListenerOptions<TransferableWorker>>) => {
callback(encode(data, useOnSafari));
};
navigator.serviceWorker.addEventListener('message', handler);
return () => {
navigator.serviceWorker.removeEventListener('message', handler);
};
},
sender = (message) => {
const transfer = message.transfer ?? [];
delete message.transfer;
worker.postMessage(message, transfer);
},
...options
} = _options;
super({
...options,
listener,
sender,
});
}
}
export abstract class ServiceWorkerServiceTransport<
T extends BaseInteraction = any
> extends Transport<T> {
constructor(_options: ServiceWorkerServiceTransportOptions = {}) {
const {
useOnSafari = DEFAULT_USE_ON_SAFARI,
listener = (callback) => {
const handler = ({ data, source }: ExtendableMessageEvent) => {
data._clientId = (source as Client).id as string;
callback(data);
};
self.addEventListener('message', handler);
return () => self.removeEventListener('message', handler);
},
sender = async (message) => {
const transfer = message.transfer || [];
delete message.transfer;
const data = decode(message, useOnSafari);
if (message._clientId) {
const client = await self.clients.get(message._clientId);
if (!client) {
console.warn(`The client "${message._clientId}" is closed.`);
return;
}
delete message._clientId;
client.postMessage(data, transfer);
return;
}
const client = message._extra?._client;
if (client) {
delete message._extra!._client;
client.postMessage(data, transfer);
return;
}
self.clients
.matchAll()
.then((all) =>
all.map((client) => client.postMessage(data, transfer))
);
},
...options
} = _options;
super({
...options,
listener,
sender,
});
self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim());
});
}
}
export const ServiceWorkerTransport = {
Client: ServiceWorkerClientTransport,
Service: ServiceWorkerServiceTransport,
};