@rws-framework/client
Version:
This package provides the core client-side framework for Realtime Web Suit (RWS), enabling modular, asynchronous web components, state management, and integration with backend services. It is located in `.dev/client`.
135 lines (108 loc) • 5.29 kB
text/typescript
import IRWSUser from '../../src/types/IRWSUser';
type SWMsgType<T = unknown> = {
command: string,
asset_type?: string,
params: T
};
abstract class RWSServiceWorker<UserType extends IRWSUser> {
protected user: UserType = null;
protected ignoredUrls: RegExp[] = [];
protected regExTypes: { [key: string]: RegExp };
public workerScope: ServiceWorkerGlobalScope;
protected static _instances: { [key: string]: RWSServiceWorker<IRWSUser> } | null = {};
private confirmedHandlers: Map<string, (params: unknown) => Promise<void> | void> = new Map();
onInit(): Promise<void> { return; }
onInstall(): Promise<void> { return; }
onActivate(): Promise<void> { return; }
/**
* Register a handler for a SW message command. After the handler resolves,
* the base class automatically replies to the source client with
* `{ command: '<originalCommand>_confirmed' }` so the main thread can await it.
*/
registerConfirmedHandler<T = unknown>(command: string, handler: (params: T) => Promise<void> | void): void {
this.confirmedHandlers.set(command, handler as (params: unknown) => Promise<void> | void);
}
constructor(workerScope: ServiceWorkerGlobalScope) {
this.workerScope = workerScope;
// Central router for confirmed-message handlers registered via registerConfirmedHandler.
// Must be set up before onInit so handlers registered there are active from the start.
this.workerScope.addEventListener('message', (event: ExtendableMessageEvent) => {
// Built-in: allow the main thread to force a waiting SW to skip waiting.
// This handles old SWs that don't have automatic skipWaiting in their install handler.
if (event.data?.command === 'SKIP_WAITING') {
console.log('[SW] Received SKIP_WAITING — calling skipWaiting()');
this.workerScope.skipWaiting();
return;
}
if (!event.data?.command) return;
const handler = this.confirmedHandlers.get(event.data.command);
if (!handler) return;
event.waitUntil(
Promise.resolve(handler(event.data.params)).then(() => {
if (event.source) {
(event.source as Client).postMessage({ command: `${event.data.command}_confirmed` });
}
})
);
});
// install and activate MUST be registered synchronously in the constructor.
// If they are inside onInit().then(), the browser can fire these lifecycle events
// before the async callback runs, causing skipWaiting/claim to never execute.
this.workerScope.addEventListener('install', (event: ExtendableEvent) => {
console.log('[SW] Service Worker: Installing — calling skipWaiting()');
// Take over immediately — don't wait for the existing SW to be released.
// This ensures controllerchange fires promptly so the main thread can
// deliver the auth token before any image fetches start.
event.waitUntil(
Promise.resolve(this.workerScope.skipWaiting()).then(() => {
console.log('[SW] skipWaiting complete');
return this.onInstall() || Promise.resolve();
})
);
});
this.workerScope.addEventListener('activate', (event: ExtendableEvent) => {
console.log('[SW] Service Worker: Activating — calling clients.claim()');
this.onActivate();
event.waitUntil(
workerScope.clients.claim().then(() => {
console.log('[SW] clients.claim() complete — broadcasting sw_activated');
return workerScope.clients.matchAll().then(clients => {
clients.forEach(client => {
client.postMessage({ command: 'sw_activated' });
});
});
})
);
});
// onInit runs after lifecycle listeners are in place, so subclass setup
// (fetch handlers, confirmed handlers) is ready when the SW becomes active.
this.onInit();
}
sendMessageToClient = <T = unknown>(clientId: string, payload: T) => {
return this.workerScope.clients.get(clientId)
.then((client: Client | undefined) => {
if (client) {
client.postMessage(payload);
}
});
};
getUser(): UserType
{
return this.user;
}
setUser(user: UserType): RWSServiceWorker<UserType>
{
this.user = user;
return this;
}
static create<T extends new (...args: unknown[]) => RWSServiceWorker<IRWSUser>>(this: T, workerScope: ServiceWorkerGlobalScope): InstanceType<T>
{
const className = this.name;
if (!RWSServiceWorker._instances[className]) {
RWSServiceWorker._instances[className] = new this(workerScope);
}
return RWSServiceWorker._instances[className] as InstanceType<T>;
}
}
export default RWSServiceWorker;
export { SWMsgType };