TelegramPWA/src/util/createPostMessageInterface.ts
2025-06-04 20:36:48 +02:00

180 lines
4.7 KiB
TypeScript

import type {
ApiUpdate,
CancellableCallback,
OriginMessageData,
OriginMessageEvent,
WorkerPayload,
} from './PostMessageConnector';
import { DEBUG } from '../config';
import { createCallbackManager } from './callbacks';
import { throttleWithTickEnd } from './schedulers';
declare const self: WorkerGlobalScope;
const callbackState = new Map<string, CancellableCallback>();
type ApiConfig =
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
((name: string, ...args: any[]) => any | [any, ArrayBuffer[]])
| Record<string, AnyFunction>;
type SendToOrigin = (data: WorkerPayload, transferables?: Transferable[]) => void;
const messageHandlers = createCallbackManager();
onmessage = messageHandlers.runCallbacks;
export function createWorkerInterface(api: ApiConfig, channel?: string) {
let pendingPayloads: WorkerPayload[] = [];
let pendingTransferables: Transferable[] = [];
const sendToOriginOnTickEnd = throttleWithTickEnd(() => {
const data = { channel, payloads: pendingPayloads };
const transferables = pendingTransferables;
pendingPayloads = [];
pendingTransferables = [];
if (transferables.length) {
postMessage(data, transferables);
} else {
postMessage(data);
}
});
function sendToOrigin(payload: WorkerPayload, transferables?: Transferable[]) {
pendingPayloads.push(payload);
if (transferables) {
pendingTransferables.push(...transferables);
}
sendToOriginOnTickEnd();
}
handleErrors(sendToOrigin);
messageHandlers.addCallback((message: OriginMessageEvent) => {
if (message.data?.channel === channel) {
onMessage(api, message.data, sendToOrigin);
}
});
}
function onMessage(
api: ApiConfig,
data: OriginMessageData,
sendToOrigin: SendToOrigin,
onUpdate?: (update: ApiUpdate) => void,
) {
if (!onUpdate) {
onUpdate = (update: ApiUpdate) => {
sendToOrigin({
type: 'update',
update,
});
};
}
data.payloads.forEach(async (payload) => {
switch (payload.type) {
case 'init': {
const { args } = payload;
if (typeof api === 'function') {
await api('init', onUpdate, ...args);
} else {
await api.init?.(onUpdate, ...args);
}
break;
}
case 'callMethod': {
const {
messageId, name, args, withCallback,
} = payload;
try {
if (typeof api !== 'function' && !api[name]) return;
if (messageId && withCallback) {
const callback = (...callbackArgs: any[]) => {
const lastArg = callbackArgs[callbackArgs.length - 1];
sendToOrigin({
type: 'methodCallback',
messageId,
callbackArgs,
}, isTransferable(lastArg) ? [lastArg] : undefined);
};
callbackState.set(messageId, callback);
args.push(callback as never);
}
const response = typeof api === 'function'
? await api(name, ...args)
: await api[name](...args);
const { arrayBuffer } = (typeof response === 'object' && 'arrayBuffer' in response && response) || {};
if (messageId) {
sendToOrigin(
{
type: 'methodResponse',
messageId,
response,
},
arrayBuffer ? [arrayBuffer] : undefined,
);
}
} catch (error: any) {
if (DEBUG) {
// eslint-disable-next-line no-console
console.error(error);
}
if (messageId) {
sendToOrigin({
type: 'methodResponse',
messageId,
error: { message: error.message },
});
}
}
if (messageId) {
callbackState.delete(messageId);
}
break;
}
case 'cancelProgress': {
const callback = callbackState.get(payload.messageId);
if (callback) {
callback.isCanceled = true;
}
break;
}
}
});
}
function isTransferable(obj: any) {
return obj instanceof ArrayBuffer || obj instanceof ImageBitmap;
}
function handleErrors(sendToOrigin: SendToOrigin) {
self.onerror = (e) => {
// eslint-disable-next-line no-console
console.error(e);
sendToOrigin({ type: 'unhandledError', error: { message: e.error.message || 'Uncaught exception in worker' } });
};
self.addEventListener('unhandledrejection', (e) => {
// eslint-disable-next-line no-console
console.error(e);
sendToOrigin({ type: 'unhandledError', error: { message: e.reason.message || 'Uncaught rejection in worker' } });
});
}