[Perf] API and Post Message Connector: Batch postMessage calls
f-u
This commit is contained in:
parent
8b717b010f
commit
7b86865d67
@ -3,17 +3,17 @@ import type { TypedBroadcastChannel } from '../../../util/multitab';
|
|||||||
import type { ApiInitialArgs, ApiOnProgress, OnApiUpdate } from '../../types';
|
import type { ApiInitialArgs, ApiOnProgress, OnApiUpdate } from '../../types';
|
||||||
import type { LocalDb } from '../localDb';
|
import type { LocalDb } from '../localDb';
|
||||||
import type { MethodArgs, MethodResponse, Methods } from '../methods/types';
|
import type { MethodArgs, MethodResponse, Methods } from '../methods/types';
|
||||||
import type { OriginRequest, ThenArg, WorkerMessageEvent } from './types';
|
import type { OriginPayload, ThenArg, WorkerMessageEvent } from './types';
|
||||||
|
|
||||||
import { DATA_BROADCAST_CHANNEL_NAME, DEBUG } from '../../../config';
|
import { DATA_BROADCAST_CHANNEL_NAME, DEBUG } from '../../../config';
|
||||||
import { logDebugMessage } from '../../../util/debugConsole';
|
import { logDebugMessage } from '../../../util/debugConsole';
|
||||||
import Deferred from '../../../util/Deferred';
|
import Deferred from '../../../util/Deferred';
|
||||||
import { getCurrentTabId, subscribeToMasterChange } from '../../../util/establishMultitabRole';
|
import { getCurrentTabId, subscribeToMasterChange } from '../../../util/establishMultitabRole';
|
||||||
import generateUniqueId from '../../../util/generateUniqueId';
|
import generateUniqueId from '../../../util/generateUniqueId';
|
||||||
import { pause } from '../../../util/schedulers';
|
import { pause, throttleWithTickEnd } from '../../../util/schedulers';
|
||||||
import { IS_MULTITAB_SUPPORTED } from '../../../util/windowEnvironment';
|
import { IS_MULTITAB_SUPPORTED } from '../../../util/windowEnvironment';
|
||||||
|
|
||||||
type RequestStates = {
|
type RequestState = {
|
||||||
messageId: string;
|
messageId: string;
|
||||||
resolve: Function;
|
resolve: Function;
|
||||||
reject: Function;
|
reject: Function;
|
||||||
@ -26,8 +26,12 @@ const HEALTH_CHECK_MIN_DELAY = 5 * 1000; // 5 sec
|
|||||||
const NO_QUEUE_BEFORE_INIT = new Set(['destroy']);
|
const NO_QUEUE_BEFORE_INIT = new Set(['destroy']);
|
||||||
|
|
||||||
let worker: Worker | undefined;
|
let worker: Worker | undefined;
|
||||||
const requestStates = new Map<string, RequestStates>();
|
|
||||||
const requestStatesByCallback = new Map<AnyToVoidFunction, RequestStates>();
|
const requestStates = new Map<string, RequestState>();
|
||||||
|
const requestStatesByCallback = new Map<AnyToVoidFunction, RequestState>();
|
||||||
|
|
||||||
|
let pendingPayloads: OriginPayload[] = [];
|
||||||
|
|
||||||
const savedLocalDb: LocalDb = {
|
const savedLocalDb: LocalDb = {
|
||||||
chats: {},
|
chats: {},
|
||||||
users: {},
|
users: {},
|
||||||
@ -48,6 +52,17 @@ const channel = IS_MULTITAB_SUPPORTED
|
|||||||
? new BroadcastChannel(DATA_BROADCAST_CHANNEL_NAME) as TypedBroadcastChannel
|
? new BroadcastChannel(DATA_BROADCAST_CHANNEL_NAME) as TypedBroadcastChannel
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const postMessagesOnTickEnd = throttleWithTickEnd(() => {
|
||||||
|
const payloads = pendingPayloads;
|
||||||
|
pendingPayloads = [];
|
||||||
|
worker?.postMessage({ payloads });
|
||||||
|
});
|
||||||
|
|
||||||
|
function postMessageOnTickEnd(payload: OriginPayload) {
|
||||||
|
pendingPayloads.push(payload);
|
||||||
|
postMessagesOnTickEnd();
|
||||||
|
}
|
||||||
|
|
||||||
export function initApiOnMasterTab(initialArgs: ApiInitialArgs) {
|
export function initApiOnMasterTab(initialArgs: ApiInitialArgs) {
|
||||||
if (!channel) return;
|
if (!channel) return;
|
||||||
|
|
||||||
@ -250,7 +265,7 @@ export function cancelApiProgress(progressCallback: ApiOnProgress) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function cancelApiProgressMaster(messageId: string) {
|
export function cancelApiProgressMaster(messageId: string) {
|
||||||
worker?.postMessage({
|
postMessageOnTickEnd({
|
||||||
type: 'cancelProgress',
|
type: 'cancelProgress',
|
||||||
messageId,
|
messageId,
|
||||||
});
|
});
|
||||||
@ -258,34 +273,35 @@ export function cancelApiProgressMaster(messageId: string) {
|
|||||||
|
|
||||||
function subscribeToWorker(onUpdate: OnApiUpdate) {
|
function subscribeToWorker(onUpdate: OnApiUpdate) {
|
||||||
worker?.addEventListener('message', ({ data }: WorkerMessageEvent) => {
|
worker?.addEventListener('message', ({ data }: WorkerMessageEvent) => {
|
||||||
if (!data) return;
|
data?.payloads.forEach((payload) => {
|
||||||
if (data.type === 'updates') {
|
if (payload.type === 'updates') {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
let DEBUG_startAt: number | undefined;
|
let DEBUG_startAt: number | undefined;
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
DEBUG_startAt = performance.now();
|
DEBUG_startAt = performance.now();
|
||||||
}
|
|
||||||
|
|
||||||
data.updates.forEach(onUpdate);
|
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
const duration = performance.now() - DEBUG_startAt!;
|
|
||||||
if (duration > 5) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.warn(`[API] Slow updates processing: ${data.updates.length} updates in ${duration} ms`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
payload.updates.forEach(onUpdate);
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
const duration = performance.now() - DEBUG_startAt!;
|
||||||
|
if (duration > 5) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn(`[API] Slow updates processing: ${payload.updates.length} updates in ${duration} ms`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (payload.type === 'methodResponse') {
|
||||||
|
handleMethodResponse(payload);
|
||||||
|
} else if (payload.type === 'methodCallback') {
|
||||||
|
handleMethodCallback(payload);
|
||||||
|
} else if (payload.type === 'unhandledError') {
|
||||||
|
throw new Error(payload.error?.message);
|
||||||
|
} else if (payload.type === 'sendBeacon') {
|
||||||
|
navigator.sendBeacon(payload.url, payload.data);
|
||||||
|
} else if (payload.type === 'debugLog') {
|
||||||
|
logDebugMessage(payload.level, ...payload.args);
|
||||||
}
|
}
|
||||||
} else if (data.type === 'methodResponse') {
|
});
|
||||||
handleMethodResponse(data);
|
|
||||||
} else if (data.type === 'methodCallback') {
|
|
||||||
handleMethodCallback(data);
|
|
||||||
} else if (data.type === 'unhandledError') {
|
|
||||||
throw new Error(data.error?.message);
|
|
||||||
} else if (data.type === 'sendBeacon') {
|
|
||||||
navigator.sendBeacon(data.url, data.data);
|
|
||||||
} else if (data.type === 'debugLog') {
|
|
||||||
logDebugMessage(data.level, ...data.args);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -323,7 +339,7 @@ function makeRequestToMaster(message: {
|
|||||||
...message,
|
...message,
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestState = { messageId } as RequestStates;
|
const requestState = { messageId } as RequestState;
|
||||||
|
|
||||||
// Re-wrap type because of `postMessage`
|
// Re-wrap type because of `postMessage`
|
||||||
const promise: Promise<MethodResponse<keyof Methods>> = new Promise((resolve, reject) => {
|
const promise: Promise<MethodResponse<keyof Methods>> = new Promise((resolve, reject) => {
|
||||||
@ -355,14 +371,14 @@ function makeRequestToMaster(message: {
|
|||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeRequest(message: OriginRequest) {
|
function makeRequest(message: OriginPayload) {
|
||||||
const messageId = generateUniqueId();
|
const messageId = generateUniqueId();
|
||||||
const payload: OriginRequest = {
|
const payload: OriginPayload = {
|
||||||
messageId,
|
messageId,
|
||||||
...message,
|
...message,
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestState = { messageId } as RequestStates;
|
const requestState = { messageId } as RequestState;
|
||||||
|
|
||||||
// Re-wrap type because of `postMessage`
|
// Re-wrap type because of `postMessage`
|
||||||
const promise: Promise<MethodResponse<keyof Methods>> = new Promise((resolve, reject) => {
|
const promise: Promise<MethodResponse<keyof Methods>> = new Promise((resolve, reject) => {
|
||||||
@ -391,7 +407,7 @@ function makeRequest(message: OriginRequest) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
worker?.postMessage(payload);
|
postMessageOnTickEnd(payload);
|
||||||
|
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,59 +5,85 @@ import type { MethodArgs, MethodResponse, Methods } from '../methods/types';
|
|||||||
|
|
||||||
export type ThenArg<T> = T extends Promise<infer U> ? U : T;
|
export type ThenArg<T> = T extends Promise<infer U> ? U : T;
|
||||||
|
|
||||||
|
export type WorkerPayload =
|
||||||
|
{
|
||||||
|
type: 'updates';
|
||||||
|
updates: ApiUpdate[];
|
||||||
|
}
|
||||||
|
|
|
||||||
|
{
|
||||||
|
type: 'methodResponse';
|
||||||
|
messageId: string;
|
||||||
|
response?: ThenArg<MethodResponse<keyof Methods>>;
|
||||||
|
error?: { message: string };
|
||||||
|
}
|
||||||
|
|
|
||||||
|
{
|
||||||
|
type: 'methodCallback';
|
||||||
|
messageId: string;
|
||||||
|
callbackArgs: any[];
|
||||||
|
}
|
||||||
|
|
|
||||||
|
{
|
||||||
|
type: 'unhandledError';
|
||||||
|
error?: { message: string };
|
||||||
|
}
|
||||||
|
|
|
||||||
|
{
|
||||||
|
type: 'sendBeacon';
|
||||||
|
url: string;
|
||||||
|
data: ArrayBuffer;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
{
|
||||||
|
type: 'debugLog';
|
||||||
|
level: DebugLevel;
|
||||||
|
args: any[];
|
||||||
|
};
|
||||||
|
|
||||||
export type WorkerMessageData = {
|
export type WorkerMessageData = {
|
||||||
type: 'updates';
|
payloads: WorkerPayload[];
|
||||||
updates: ApiUpdate[];
|
|
||||||
} | {
|
|
||||||
type: 'methodResponse';
|
|
||||||
messageId: string;
|
|
||||||
response?: ThenArg<MethodResponse<keyof Methods>>;
|
|
||||||
error?: { message: string };
|
|
||||||
} | {
|
|
||||||
type: 'methodCallback';
|
|
||||||
messageId: string;
|
|
||||||
callbackArgs: any[];
|
|
||||||
} | {
|
|
||||||
type: 'unhandledError';
|
|
||||||
error?: { message: string };
|
|
||||||
} | {
|
|
||||||
type: 'sendBeacon';
|
|
||||||
url: string;
|
|
||||||
data: ArrayBuffer;
|
|
||||||
} | {
|
|
||||||
type: 'debugLog';
|
|
||||||
level: DebugLevel;
|
|
||||||
args: any[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface WorkerMessageEvent {
|
export type WorkerMessageEvent = {
|
||||||
data: WorkerMessageData;
|
data: WorkerMessageData;
|
||||||
}
|
|
||||||
|
|
||||||
export type OriginRequest = {
|
|
||||||
type: 'initApi';
|
|
||||||
messageId?: string;
|
|
||||||
args: [ApiInitialArgs, LocalDb];
|
|
||||||
} | {
|
|
||||||
type: 'callMethod';
|
|
||||||
messageId?: string;
|
|
||||||
name: keyof Methods;
|
|
||||||
args: MethodArgs<keyof Methods>;
|
|
||||||
withCallback?: boolean;
|
|
||||||
} | {
|
|
||||||
type: 'ping';
|
|
||||||
messageId?: string;
|
|
||||||
} | {
|
|
||||||
type: 'toggleDebugMode';
|
|
||||||
messageId?: string;
|
|
||||||
isEnabled?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OriginMessageData = OriginRequest | {
|
export type OriginPayload =
|
||||||
type: 'cancelProgress';
|
{
|
||||||
messageId: string;
|
type: 'initApi';
|
||||||
|
messageId?: string;
|
||||||
|
args: [ApiInitialArgs, LocalDb];
|
||||||
|
}
|
||||||
|
|
|
||||||
|
{
|
||||||
|
type: 'callMethod';
|
||||||
|
messageId?: string;
|
||||||
|
name: keyof Methods;
|
||||||
|
args: MethodArgs<keyof Methods>;
|
||||||
|
withCallback?: boolean;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
{
|
||||||
|
type: 'ping';
|
||||||
|
messageId?: string;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
{
|
||||||
|
type: 'toggleDebugMode';
|
||||||
|
messageId?: string;
|
||||||
|
isEnabled?: boolean;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
{
|
||||||
|
type: 'cancelProgress';
|
||||||
|
messageId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OriginMessageData = {
|
||||||
|
payloads: OriginPayload[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface OriginMessageEvent {
|
export type OriginMessageEvent = {
|
||||||
data: OriginMessageData;
|
data: OriginMessageData;
|
||||||
}
|
};
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import type { DebugLevel } from '../../../util/debugConsole';
|
import type { DebugLevel } from '../../../util/debugConsole';
|
||||||
import type { ApiOnProgress, ApiUpdate } from '../../types';
|
import type { ApiOnProgress, ApiUpdate } from '../../types';
|
||||||
import type { OriginMessageEvent, WorkerMessageData } from './types';
|
import type { OriginMessageEvent, WorkerPayload } from './types';
|
||||||
|
|
||||||
import { DEBUG } from '../../../config';
|
import { DEBUG } from '../../../config';
|
||||||
import { DEBUG_LEVELS } from '../../../util/debugConsole';
|
import { DEBUG_LEVELS } from '../../../util/debugConsole';
|
||||||
@ -39,108 +39,111 @@ function disableDebugLog() {
|
|||||||
|
|
||||||
handleErrors();
|
handleErrors();
|
||||||
|
|
||||||
|
let pendingPayloads: WorkerPayload[] = [];
|
||||||
|
let pendingTransferables: Transferable[] = [];
|
||||||
|
|
||||||
const callbackState = new Map<string, ApiOnProgress>();
|
const callbackState = new Map<string, ApiOnProgress>();
|
||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
console.log('>>> FINISH LOAD WORKER');
|
console.log('>>> FINISH LOAD WORKER');
|
||||||
}
|
}
|
||||||
|
|
||||||
onmessage = async (message: OriginMessageEvent) => {
|
onmessage = ({ data }: OriginMessageEvent) => {
|
||||||
const { data } = message;
|
data.payloads.forEach(async (payload) => {
|
||||||
|
switch (payload.type) {
|
||||||
switch (data.type) {
|
case 'initApi': {
|
||||||
case 'initApi': {
|
const { messageId, args } = payload;
|
||||||
const { messageId, args } = data;
|
await initApi(onUpdate, args[0], args[1]);
|
||||||
await initApi(onUpdate, args[0], args[1]);
|
|
||||||
if (messageId) {
|
|
||||||
sendToOrigin({
|
|
||||||
type: 'methodResponse',
|
|
||||||
messageId,
|
|
||||||
response: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'callMethod': {
|
|
||||||
const {
|
|
||||||
messageId, name, args, withCallback,
|
|
||||||
} = data;
|
|
||||||
try {
|
|
||||||
if (messageId && withCallback) {
|
|
||||||
const callback = (...callbackArgs: any[]) => {
|
|
||||||
const lastArg = callbackArgs[callbackArgs.length - 1];
|
|
||||||
|
|
||||||
sendToOrigin({
|
|
||||||
type: 'methodCallback',
|
|
||||||
messageId,
|
|
||||||
callbackArgs,
|
|
||||||
}, lastArg instanceof ArrayBuffer ? lastArg : undefined);
|
|
||||||
};
|
|
||||||
|
|
||||||
callbackState.set(messageId, callback);
|
|
||||||
|
|
||||||
args.push(callback as never);
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await callApi(name, ...args);
|
|
||||||
|
|
||||||
if (DEBUG && typeof response === 'object' && 'CONSTRUCTOR_ID' in response) {
|
|
||||||
log('UNEXPECTED RESPONSE', `${name}: ${response.className}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { arrayBuffer } = (typeof response === 'object' && 'arrayBuffer' in response && response) || {};
|
|
||||||
|
|
||||||
if (messageId) {
|
if (messageId) {
|
||||||
sendToOrigin({
|
sendToOrigin({
|
||||||
type: 'methodResponse',
|
type: 'methodResponse',
|
||||||
messageId,
|
messageId,
|
||||||
response,
|
response: true,
|
||||||
}, arrayBuffer);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
if (DEBUG) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (messageId) {
|
|
||||||
sendToOrigin({
|
|
||||||
type: 'methodResponse',
|
|
||||||
messageId,
|
|
||||||
error: { message: error.message },
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
case 'callMethod': {
|
||||||
|
const {
|
||||||
|
messageId, name, args, withCallback,
|
||||||
|
} = payload;
|
||||||
|
try {
|
||||||
|
if (messageId && withCallback) {
|
||||||
|
const callback = (...callbackArgs: any[]) => {
|
||||||
|
const lastArg = callbackArgs[callbackArgs.length - 1];
|
||||||
|
|
||||||
if (messageId) {
|
sendToOrigin({
|
||||||
callbackState.delete(messageId);
|
type: 'methodCallback',
|
||||||
|
messageId,
|
||||||
|
callbackArgs,
|
||||||
|
}, lastArg instanceof ArrayBuffer ? lastArg : undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
callbackState.set(messageId, callback);
|
||||||
|
|
||||||
|
args.push(callback as never);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await callApi(name, ...args);
|
||||||
|
|
||||||
|
if (DEBUG && typeof response === 'object' && 'CONSTRUCTOR_ID' in response) {
|
||||||
|
log('UNEXPECTED RESPONSE', `${name}: ${response.className}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { arrayBuffer } = (typeof response === 'object' && 'arrayBuffer' in response && response) || {};
|
||||||
|
|
||||||
|
if (messageId) {
|
||||||
|
sendToOrigin({
|
||||||
|
type: 'methodResponse',
|
||||||
|
messageId,
|
||||||
|
response,
|
||||||
|
}, arrayBuffer);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
if (DEBUG) {
|
||||||
|
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) {
|
||||||
|
cancelApiProgress(callback);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
case 'cancelProgress': {
|
|
||||||
const callback = callbackState.get(data.messageId);
|
|
||||||
if (callback) {
|
|
||||||
cancelApiProgress(callback);
|
|
||||||
}
|
}
|
||||||
|
case 'ping': {
|
||||||
|
sendToOrigin({
|
||||||
|
type: 'methodResponse',
|
||||||
|
messageId: payload.messageId!,
|
||||||
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'ping': {
|
case 'toggleDebugMode': {
|
||||||
sendToOrigin({
|
if (payload.isEnabled) {
|
||||||
type: 'methodResponse',
|
enableDebugLog();
|
||||||
messageId: data.messageId!,
|
} else {
|
||||||
});
|
disableDebugLog();
|
||||||
|
}
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'toggleDebugMode': {
|
|
||||||
if (data.isEnabled) {
|
|
||||||
enableDebugLog();
|
|
||||||
} else {
|
|
||||||
disableDebugLog();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleErrors() {
|
function handleErrors() {
|
||||||
@ -176,10 +179,26 @@ function onUpdate(update: ApiUpdate) {
|
|||||||
sendUpdatesOnTickEnd();
|
sendUpdatesOnTickEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendToOrigin(data: WorkerMessageData, arrayBuffer?: ArrayBuffer) {
|
const sendToOriginOnTickEnd = throttleWithTickEnd(() => {
|
||||||
if (arrayBuffer) {
|
const data = { payloads: pendingPayloads };
|
||||||
postMessage(data, [arrayBuffer]);
|
const transferables = pendingTransferables;
|
||||||
|
|
||||||
|
pendingPayloads = [];
|
||||||
|
pendingTransferables = [];
|
||||||
|
|
||||||
|
if (transferables.length) {
|
||||||
|
postMessage(data, transferables);
|
||||||
} else {
|
} else {
|
||||||
postMessage(data);
|
postMessage(data);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function sendToOrigin(payload: WorkerPayload, transferable?: Transferable) {
|
||||||
|
pendingPayloads.push(payload);
|
||||||
|
|
||||||
|
if (transferable) {
|
||||||
|
pendingTransferables.push(transferable);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendToOriginOnTickEnd();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -173,6 +173,6 @@ const api = {
|
|||||||
'rlottie:destroy': destroy,
|
'rlottie:destroy': destroy,
|
||||||
};
|
};
|
||||||
|
|
||||||
createWorkerInterface(api);
|
createWorkerInterface(api, 'media');
|
||||||
|
|
||||||
export type RLottieApi = typeof api;
|
export type RLottieApi = typeof api;
|
||||||
|
|||||||
@ -81,6 +81,6 @@ const api = {
|
|||||||
'video-preview:destroy': destroy,
|
'video-preview:destroy': destroy,
|
||||||
};
|
};
|
||||||
|
|
||||||
createWorkerInterface(api);
|
createWorkerInterface(api, 'media');
|
||||||
|
|
||||||
export type VideoPreviewApi = typeof api;
|
export type VideoPreviewApi = typeof api;
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import generateUniqueId from './generateUniqueId';
|
import generateUniqueId from './generateUniqueId';
|
||||||
|
import { throttleWithTickEnd } from './schedulers';
|
||||||
|
|
||||||
export interface CancellableCallback {
|
export interface CancellableCallback {
|
||||||
(
|
(
|
||||||
@ -8,16 +9,13 @@ export interface CancellableCallback {
|
|||||||
isCanceled?: boolean;
|
isCanceled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type InitData = {
|
type InitPayload = {
|
||||||
channel?: string;
|
|
||||||
type: 'init';
|
type: 'init';
|
||||||
messageId?: string;
|
messageId?: string;
|
||||||
name: 'init';
|
|
||||||
args: any;
|
args: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CallMethodData = {
|
type CallMethodPayload = {
|
||||||
channel?: string;
|
|
||||||
type: 'callMethod';
|
type: 'callMethod';
|
||||||
messageId?: string;
|
messageId?: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -25,12 +23,21 @@ type CallMethodData = {
|
|||||||
withCallback?: boolean;
|
withCallback?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OriginMessageData = InitData | CallMethodData | {
|
type CancelProgressPayload = {
|
||||||
channel?: string;
|
|
||||||
type: 'cancelProgress';
|
type: 'cancelProgress';
|
||||||
messageId: string;
|
messageId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type OriginPayload =
|
||||||
|
InitPayload
|
||||||
|
| CallMethodPayload
|
||||||
|
| CancelProgressPayload;
|
||||||
|
|
||||||
|
export type OriginMessageData = {
|
||||||
|
channel?: string;
|
||||||
|
payloads: OriginPayload[];
|
||||||
|
};
|
||||||
|
|
||||||
export interface OriginMessageEvent {
|
export interface OriginMessageEvent {
|
||||||
data: OriginMessageData;
|
data: OriginMessageData;
|
||||||
}
|
}
|
||||||
@ -39,32 +46,44 @@ export type ApiUpdate =
|
|||||||
{ type: string }
|
{ type: string }
|
||||||
& any;
|
& any;
|
||||||
|
|
||||||
|
export type WorkerPayload =
|
||||||
|
{
|
||||||
|
channel?: string;
|
||||||
|
type: 'update';
|
||||||
|
update: ApiUpdate;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
{
|
||||||
|
channel?: string;
|
||||||
|
type: 'methodResponse';
|
||||||
|
messageId: string;
|
||||||
|
response?: any;
|
||||||
|
error?: { message: string };
|
||||||
|
}
|
||||||
|
|
|
||||||
|
{
|
||||||
|
channel?: string;
|
||||||
|
type: 'methodCallback';
|
||||||
|
messageId: string;
|
||||||
|
callbackArgs: any[];
|
||||||
|
}
|
||||||
|
|
|
||||||
|
{
|
||||||
|
channel?: string;
|
||||||
|
type: 'unhandledError';
|
||||||
|
error?: { message: string };
|
||||||
|
};
|
||||||
|
|
||||||
export type WorkerMessageData = {
|
export type WorkerMessageData = {
|
||||||
channel?: string;
|
channel?: string;
|
||||||
type: 'update';
|
payloads: WorkerPayload[];
|
||||||
update: ApiUpdate;
|
|
||||||
} | {
|
|
||||||
channel?: string;
|
|
||||||
type: 'methodResponse';
|
|
||||||
messageId: string;
|
|
||||||
response?: any;
|
|
||||||
error?: { message: string };
|
|
||||||
} | {
|
|
||||||
channel?: string;
|
|
||||||
type: 'methodCallback';
|
|
||||||
messageId: string;
|
|
||||||
callbackArgs: any[];
|
|
||||||
} | {
|
|
||||||
channel?: string;
|
|
||||||
type: 'unhandledError';
|
|
||||||
error?: { message: string };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface WorkerMessageEvent {
|
export interface WorkerMessageEvent {
|
||||||
data: WorkerMessageData;
|
data: WorkerMessageData;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RequestStates {
|
interface RequestState {
|
||||||
messageId: string;
|
messageId: string;
|
||||||
resolve: Function;
|
resolve: Function;
|
||||||
reject: Function;
|
reject: Function;
|
||||||
@ -82,9 +101,11 @@ export type RequestTypes<T extends InputRequestTypes> = Values<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
class ConnectorClass<T extends InputRequestTypes> {
|
class ConnectorClass<T extends InputRequestTypes> {
|
||||||
private requestStates = new Map<string, RequestStates>();
|
private requestStates = new Map<string, RequestState>();
|
||||||
|
|
||||||
private requestStatesByCallback = new Map<AnyToVoidFunction, RequestStates>();
|
private requestStatesByCallback = new Map<AnyToVoidFunction, RequestState>();
|
||||||
|
|
||||||
|
private pendingPayloads: OriginPayload[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public target: Worker,
|
public target: Worker,
|
||||||
@ -98,7 +119,7 @@ class ConnectorClass<T extends InputRequestTypes> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init(...args: any[]) {
|
init(...args: any[]) {
|
||||||
this.postMessage({
|
this.postMessageOnTickEnd({
|
||||||
type: 'init',
|
type: 'init',
|
||||||
args,
|
args,
|
||||||
});
|
});
|
||||||
@ -108,13 +129,13 @@ class ConnectorClass<T extends InputRequestTypes> {
|
|||||||
const { requestStates, requestStatesByCallback } = this;
|
const { requestStates, requestStatesByCallback } = this;
|
||||||
|
|
||||||
const messageId = generateUniqueId();
|
const messageId = generateUniqueId();
|
||||||
const payload: CallMethodData = {
|
const payload: CallMethodPayload = {
|
||||||
type: 'callMethod',
|
type: 'callMethod',
|
||||||
messageId,
|
messageId,
|
||||||
...messageData,
|
...messageData,
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestState = { messageId } as RequestStates;
|
const requestState = { messageId } as RequestState;
|
||||||
|
|
||||||
// Re-wrap type because of `postMessage`
|
// Re-wrap type because of `postMessage`
|
||||||
const promise: Promise<any> = new Promise((resolve, reject) => {
|
const promise: Promise<any> = new Promise((resolve, reject) => {
|
||||||
@ -140,7 +161,7 @@ class ConnectorClass<T extends InputRequestTypes> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.postMessage(payload);
|
this.postMessageOnTickEnd(payload);
|
||||||
|
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
@ -153,7 +174,7 @@ class ConnectorClass<T extends InputRequestTypes> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.postMessage({
|
this.postMessageOnTickEnd({
|
||||||
type: 'cancelProgress',
|
type: 'cancelProgress',
|
||||||
messageId,
|
messageId,
|
||||||
});
|
});
|
||||||
@ -165,31 +186,43 @@ class ConnectorClass<T extends InputRequestTypes> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.type === 'update' && this.onUpdate) {
|
data.payloads.forEach((payload) => {
|
||||||
this.onUpdate(data.update);
|
if (payload.type === 'update' && this.onUpdate) {
|
||||||
}
|
this.onUpdate(payload.update);
|
||||||
if (data.type === 'methodResponse') {
|
|
||||||
const requestState = requestStates.get(data.messageId);
|
|
||||||
if (requestState) {
|
|
||||||
if (data.error) {
|
|
||||||
requestState.reject(data.error);
|
|
||||||
} else {
|
|
||||||
requestState.resolve(data.response);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (data.type === 'methodCallback') {
|
if (payload.type === 'methodResponse') {
|
||||||
const requestState = requestStates.get(data.messageId);
|
const requestState = requestStates.get(payload.messageId);
|
||||||
requestState?.callback?.(...data.callbackArgs);
|
if (requestState) {
|
||||||
} else if (data.type === 'unhandledError') {
|
if (payload.error) {
|
||||||
throw new Error(data.error?.message);
|
requestState.reject(payload.error);
|
||||||
}
|
} else {
|
||||||
|
requestState.resolve(payload.response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (payload.type === 'methodCallback') {
|
||||||
|
const requestState = requestStates.get(payload.messageId);
|
||||||
|
requestState?.callback?.(...payload.callbackArgs);
|
||||||
|
} else if (payload.type === 'unhandledError') {
|
||||||
|
throw new Error(payload.error?.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private postMessage(data: AnyLiteral) {
|
private postMessageOnTickEnd(payload: OriginPayload) {
|
||||||
data.channel = this.channel;
|
this.pendingPayloads.push(payload);
|
||||||
|
this.postMessagesOnTickEnd();
|
||||||
this.target.postMessage(data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private postMessagesOnTickEnd = throttleWithTickEnd(() => {
|
||||||
|
const payloads = this.pendingPayloads;
|
||||||
|
|
||||||
|
this.pendingPayloads = [];
|
||||||
|
|
||||||
|
this.target.postMessage({
|
||||||
|
channel: this.channel,
|
||||||
|
payloads,
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createConnector<T extends InputRequestTypes>(
|
export function createConnector<T extends InputRequestTypes>(
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
import type {
|
import type {
|
||||||
ApiUpdate,
|
ApiUpdate,
|
||||||
CancellableCallback, OriginMessageData, OriginMessageEvent, WorkerMessageData,
|
CancellableCallback,
|
||||||
|
OriginMessageData,
|
||||||
|
OriginMessageEvent,
|
||||||
|
WorkerPayload,
|
||||||
} from './PostMessageConnector';
|
} from './PostMessageConnector';
|
||||||
|
|
||||||
import { DEBUG } from '../config';
|
import { DEBUG } from '../config';
|
||||||
import { createCallbackManager } from './callbacks';
|
import { createCallbackManager } from './callbacks';
|
||||||
|
import { throttleWithTickEnd } from './schedulers';
|
||||||
|
|
||||||
declare const self: WorkerGlobalScope;
|
declare const self: WorkerGlobalScope;
|
||||||
|
|
||||||
@ -13,20 +17,37 @@ const callbackState = new Map<string, CancellableCallback>();
|
|||||||
type ApiConfig =
|
type ApiConfig =
|
||||||
((name: string, ...args: any[]) => any | [any, ArrayBuffer[]])
|
((name: string, ...args: any[]) => any | [any, ArrayBuffer[]])
|
||||||
| Record<string, Function>;
|
| Record<string, Function>;
|
||||||
type SendToOrigin = (data: WorkerMessageData, transferables?: Transferable[]) => void;
|
type SendToOrigin = (data: WorkerPayload, transferables?: Transferable[]) => void;
|
||||||
|
|
||||||
const messageHandlers = createCallbackManager();
|
const messageHandlers = createCallbackManager();
|
||||||
onmessage = messageHandlers.runCallbacks;
|
onmessage = messageHandlers.runCallbacks;
|
||||||
|
|
||||||
export function createWorkerInterface(api: ApiConfig, channel?: string) {
|
export function createWorkerInterface(api: ApiConfig, channel?: string) {
|
||||||
function sendToOrigin(data: WorkerMessageData, transferables?: Transferable[]) {
|
let pendingPayloads: WorkerPayload[] = [];
|
||||||
data.channel = channel;
|
let pendingTransferables: Transferable[] = [];
|
||||||
|
|
||||||
if (transferables) {
|
const sendToOriginOnTickEnd = throttleWithTickEnd(() => {
|
||||||
|
const data = { channel, payloads: pendingPayloads };
|
||||||
|
const transferables = pendingTransferables;
|
||||||
|
|
||||||
|
pendingPayloads = [];
|
||||||
|
pendingTransferables = [];
|
||||||
|
|
||||||
|
if (transferables.length) {
|
||||||
postMessage(data, transferables);
|
postMessage(data, transferables);
|
||||||
} else {
|
} else {
|
||||||
postMessage(data);
|
postMessage(data);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function sendToOrigin(payload: WorkerPayload, transferables?: Transferable[]) {
|
||||||
|
pendingPayloads.push(payload);
|
||||||
|
|
||||||
|
if (transferables) {
|
||||||
|
pendingTransferables.push(...transferables);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendToOriginOnTickEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleErrors(sendToOrigin);
|
handleErrors(sendToOrigin);
|
||||||
@ -38,7 +59,7 @@ export function createWorkerInterface(api: ApiConfig, channel?: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onMessage(
|
function onMessage(
|
||||||
api: ApiConfig,
|
api: ApiConfig,
|
||||||
data: OriginMessageData,
|
data: OriginMessageData,
|
||||||
sendToOrigin: SendToOrigin,
|
sendToOrigin: SendToOrigin,
|
||||||
@ -53,84 +74,89 @@ async function onMessage(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (data.type) {
|
data.payloads.forEach(async (payload) => {
|
||||||
case 'init': {
|
switch (payload.type) {
|
||||||
const { args } = data;
|
case 'init': {
|
||||||
const promise = typeof api === 'function'
|
const { args } = payload;
|
||||||
? api('init', onUpdate, ...args)
|
if (typeof api === 'function') {
|
||||||
: api.init?.(onUpdate, ...args);
|
await api('init', onUpdate, ...args);
|
||||||
await promise;
|
} else {
|
||||||
|
await api.init?.(onUpdate, ...args);
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'callMethod': {
|
|
||||||
const {
|
|
||||||
messageId, name, args, withCallback,
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
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'
|
break;
|
||||||
? await api(name, ...args)
|
}
|
||||||
: await api[name](...args);
|
|
||||||
const { arrayBuffer } = (typeof response === 'object' && 'arrayBuffer' in response && response) || {};
|
case 'callMethod': {
|
||||||
if (messageId) {
|
const {
|
||||||
sendToOrigin(
|
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',
|
type: 'methodResponse',
|
||||||
messageId,
|
messageId,
|
||||||
response,
|
error: { message: error.message },
|
||||||
},
|
});
|
||||||
arrayBuffer ? [arrayBuffer] : undefined,
|
}
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
if (DEBUG) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error(error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messageId) {
|
if (messageId) {
|
||||||
sendToOrigin({
|
callbackState.delete(messageId);
|
||||||
type: 'methodResponse',
|
|
||||||
messageId,
|
|
||||||
error: { message: error.message },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messageId) {
|
case 'cancelProgress': {
|
||||||
callbackState.delete(messageId);
|
const callback = callbackState.get(payload.messageId);
|
||||||
}
|
if (callback) {
|
||||||
|
callback.isCanceled = true;
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case 'cancelProgress': {
|
});
|
||||||
const callback = callbackState.get(data.messageId);
|
|
||||||
if (callback) {
|
|
||||||
callback.isCanceled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTransferable(obj: any) {
|
function isTransferable(obj: any) {
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export default function launchMediaWorkers() {
|
|||||||
instances = new Array(MAX_WORKERS).fill(undefined).map(
|
instances = new Array(MAX_WORKERS).fill(undefined).map(
|
||||||
() => {
|
() => {
|
||||||
const worker = new Worker(new URL('../lib/mediaWorker/index.worker.ts', import.meta.url));
|
const worker = new Worker(new URL('../lib/mediaWorker/index.worker.ts', import.meta.url));
|
||||||
const connector = createConnector<MediaWorkerApi>(worker);
|
const connector = createConnector<MediaWorkerApi>(worker, undefined, 'media');
|
||||||
return { worker, connector };
|
return { worker, connector };
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user