412 lines
9.8 KiB
TypeScript
412 lines
9.8 KiB
TypeScript
/* eslint-disable eslint-multitab-tt/set-global-only-variable */
|
|
import { onFullyIdle } from '../lib/teact/teact';
|
|
import { addCallback } from '../lib/teact/teactn';
|
|
import { getActions, getGlobal, setGlobal } from '../global';
|
|
|
|
import type { LocalDb } from '../api/gramjs/localDb';
|
|
import type { MethodArgs, Methods } from '../api/gramjs/methods/types';
|
|
import type { ApiInitialArgs } from '../api/types';
|
|
import type { GlobalState } from '../global/types';
|
|
|
|
import { DATA_BROADCAST_CHANNEL_NAME, MULTITAB_LOCALSTORAGE_KEY } from '../config';
|
|
import { selectTabState } from '../global/selectors';
|
|
import {
|
|
callApiLocal,
|
|
cancelApiProgressMaster,
|
|
handleMethodCallback,
|
|
handleMethodResponse,
|
|
initApi,
|
|
updateFullLocalDb,
|
|
updateLocalDb,
|
|
} from '../api/gramjs';
|
|
import { deepDiff } from './deepDiff';
|
|
import { deepMerge } from './deepMerge';
|
|
import { getCurrentTabId, signalPasscodeHash, subscribeToTokenDied } from './establishMultitabRole';
|
|
import { omit } from './iteratees';
|
|
import { IS_MULTITAB_SUPPORTED } from './windowEnvironment';
|
|
|
|
type BroadcastChannelRefreshLangpack = {
|
|
type: 'langpackRefresh';
|
|
langCode: string;
|
|
};
|
|
|
|
type BroadcastChannelRequestGlobal = {
|
|
type: 'requestGlobal';
|
|
token?: number;
|
|
appVersion: string;
|
|
};
|
|
|
|
type BroadcastChannelGlobalUpdate = {
|
|
type: 'globalUpdate';
|
|
global: GlobalState;
|
|
};
|
|
|
|
type BroadcastChannelCancelApiProgress = {
|
|
type: 'cancelApiProgress';
|
|
token: number;
|
|
messageId: string;
|
|
};
|
|
|
|
type BroadcastChannelCallApi = {
|
|
type: 'callApi';
|
|
token: number;
|
|
messageId: string;
|
|
name: keyof Methods;
|
|
args: MethodArgs<keyof Methods>;
|
|
withCallback?: boolean;
|
|
};
|
|
|
|
type BroadcastChannelMessageResponse = {
|
|
type: 'messageResponse';
|
|
token: number;
|
|
messageId: string;
|
|
response: any;
|
|
};
|
|
|
|
type BroadcastChannelLocalDbUpdate = {
|
|
type: 'localDbUpdate';
|
|
batchedUpdates: {
|
|
name: keyof LocalDb;
|
|
prop: string;
|
|
value: any;
|
|
}[];
|
|
};
|
|
|
|
type BroadcastChannelLocalDbUpdateFull = {
|
|
type: 'localDbUpdateFull';
|
|
localDb: any;
|
|
};
|
|
|
|
type BroadcastChannelMessageCallback = {
|
|
type: 'messageCallback';
|
|
token: number;
|
|
messageId: string;
|
|
callbackArgs: any;
|
|
};
|
|
|
|
type BroadcastChannelGlobalDiff = {
|
|
type: 'globalDiffUpdate';
|
|
diff: any;
|
|
};
|
|
|
|
type BroadcastChannelInitApi = {
|
|
type: 'initApi';
|
|
token: number;
|
|
initialArgs: ApiInitialArgs;
|
|
};
|
|
|
|
const MULTITAB_ESTABLISH_TIMEOUT = 800;
|
|
|
|
export type TypedBroadcastChannel = {
|
|
postMessage: (message: BroadcastChannelMessage) => void;
|
|
addEventListener: (type: 'message', listener: (event: { data: BroadcastChannelMessage }) => void) => void;
|
|
removeEventListener: (type: 'message', listener: (event: { data: BroadcastChannelMessage }) => void) => void;
|
|
};
|
|
|
|
type BroadcastChannelMessage = (
|
|
BroadcastChannelRequestGlobal | BroadcastChannelGlobalUpdate | BroadcastChannelCallApi |
|
|
BroadcastChannelMessageResponse | BroadcastChannelRefreshLangpack |
|
|
BroadcastChannelMessageCallback | BroadcastChannelCancelApiProgress | BroadcastChannelLocalDbUpdate |
|
|
BroadcastChannelLocalDbUpdateFull | BroadcastChannelGlobalDiff | BroadcastChannelInitApi
|
|
);
|
|
|
|
let resolveGlobalPromise: VoidFunction | undefined;
|
|
let isFirstGlobalResolved = false;
|
|
let currentGlobal: GlobalState | undefined;
|
|
let isDisabled = false;
|
|
|
|
const channel = IS_MULTITAB_SUPPORTED
|
|
? new BroadcastChannel(DATA_BROADCAST_CHANNEL_NAME) as TypedBroadcastChannel
|
|
: undefined;
|
|
|
|
let isBroadcastDiffScheduled = false;
|
|
let lastBroadcastDiffGlobal: GlobalState | undefined;
|
|
|
|
function broadcastDiffOnIdle() {
|
|
if (isBroadcastDiffScheduled) return;
|
|
|
|
isBroadcastDiffScheduled = true;
|
|
lastBroadcastDiffGlobal = currentGlobal;
|
|
|
|
onFullyIdle(() => {
|
|
if (!channel) return;
|
|
|
|
const diff = deepDiff(lastBroadcastDiffGlobal, currentGlobal);
|
|
|
|
if (typeof diff !== 'symbol') {
|
|
channel.postMessage({
|
|
type: 'globalDiffUpdate',
|
|
diff,
|
|
});
|
|
}
|
|
|
|
isBroadcastDiffScheduled = false;
|
|
});
|
|
}
|
|
|
|
export function unsubcribeFromMultitabBroadcastChannel() {
|
|
if (channel) {
|
|
channel.removeEventListener('message', handleMessage);
|
|
isDisabled = true;
|
|
}
|
|
}
|
|
|
|
export function subscribeToMultitabBroadcastChannel() {
|
|
if (!channel) return;
|
|
|
|
subscribeToTokenDied((token) => {
|
|
if (token === getCurrentTabId()) {
|
|
unsubcribeFromMultitabBroadcastChannel();
|
|
const global = getGlobal();
|
|
const newGlobal = {
|
|
...global,
|
|
byTabId: omit(global.byTabId, [token]),
|
|
};
|
|
|
|
const diff = deepDiff(global, newGlobal);
|
|
|
|
if (typeof diff !== 'symbol') {
|
|
channel.postMessage({
|
|
type: 'globalDiffUpdate',
|
|
diff,
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
let global = getGlobal();
|
|
global = {
|
|
...global,
|
|
byTabId: omit(global.byTabId, [token]),
|
|
};
|
|
|
|
setGlobal(global);
|
|
});
|
|
|
|
addCallback((global: GlobalState) => {
|
|
if (!isFirstGlobalResolved || isDisabled) {
|
|
currentGlobal = global;
|
|
return;
|
|
}
|
|
|
|
if (currentGlobal === global) {
|
|
return;
|
|
}
|
|
|
|
if (!currentGlobal) {
|
|
currentGlobal = global;
|
|
channel.postMessage({
|
|
type: 'globalUpdate',
|
|
global,
|
|
});
|
|
return;
|
|
}
|
|
|
|
broadcastDiffOnIdle();
|
|
|
|
currentGlobal = global;
|
|
});
|
|
|
|
channel.addEventListener('message', handleMessage);
|
|
}
|
|
|
|
export function handleMessage({ data }: { data: BroadcastChannelMessage }) {
|
|
if (!data || !channel) return;
|
|
|
|
switch (data.type) {
|
|
case 'initApi': {
|
|
const global = getGlobal();
|
|
if (!selectTabState(global).isMasterTab) return;
|
|
|
|
const { initialArgs } = data;
|
|
initApi(getActions().apiUpdate, initialArgs);
|
|
break;
|
|
}
|
|
|
|
case 'globalDiffUpdate': {
|
|
if (!isFirstGlobalResolved) return;
|
|
const { diff } = data;
|
|
const oldGlobal = getGlobal();
|
|
const global = deepMerge(oldGlobal, diff);
|
|
|
|
// @ts-ignore
|
|
global.DEBUG_capturedId = oldGlobal.DEBUG_capturedId;
|
|
|
|
currentGlobal = global;
|
|
setGlobal(global);
|
|
break;
|
|
}
|
|
|
|
case 'globalUpdate': {
|
|
if (isFirstGlobalResolved) return;
|
|
const oldGlobal = getGlobal();
|
|
// @ts-ignore
|
|
data.global.DEBUG_capturedId = oldGlobal.DEBUG_capturedId;
|
|
currentGlobal = data.global;
|
|
setGlobal(data.global);
|
|
if (resolveGlobalPromise) {
|
|
resolveGlobalPromise();
|
|
resolveGlobalPromise = undefined;
|
|
isFirstGlobalResolved = true;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 'requestGlobal': {
|
|
const { appVersion } = data;
|
|
if (appVersion !== APP_VERSION) {
|
|
// If app version on the other tab was updated, reload the current page immediately and don't respond
|
|
// to the other tab's request because our current global might be incompatible with the new version
|
|
window.location.reload();
|
|
return;
|
|
}
|
|
|
|
if (!isFirstGlobalResolved) return;
|
|
const global = getGlobal();
|
|
|
|
if (!selectTabState(global).isMasterTab) return;
|
|
|
|
channel.postMessage({
|
|
type: 'globalUpdate',
|
|
global,
|
|
});
|
|
|
|
signalPasscodeHash();
|
|
break;
|
|
}
|
|
|
|
case 'messageCallback': {
|
|
if (!isFirstGlobalResolved) return;
|
|
const global = getGlobal();
|
|
if (selectTabState(global).isMasterTab) return;
|
|
|
|
handleMethodCallback(data);
|
|
break;
|
|
}
|
|
|
|
case 'localDbUpdate': {
|
|
if (!isFirstGlobalResolved) return;
|
|
const global = getGlobal();
|
|
if (selectTabState(global).isMasterTab) return;
|
|
|
|
const {
|
|
batchedUpdates,
|
|
} = data;
|
|
|
|
batchedUpdates.forEach(({
|
|
name,
|
|
prop,
|
|
value,
|
|
}) => {
|
|
updateLocalDb(name, prop, value);
|
|
});
|
|
break;
|
|
}
|
|
|
|
case 'localDbUpdateFull': {
|
|
if (!isFirstGlobalResolved) return;
|
|
const global = getGlobal();
|
|
if (selectTabState(global).isMasterTab) return;
|
|
|
|
const { localDb } = data;
|
|
|
|
updateFullLocalDb(localDb);
|
|
break;
|
|
}
|
|
|
|
case 'messageResponse': {
|
|
if (!isFirstGlobalResolved) return;
|
|
const global = getGlobal();
|
|
|
|
if (selectTabState(global).isMasterTab) return;
|
|
|
|
handleMethodResponse(data);
|
|
break;
|
|
}
|
|
|
|
case 'cancelApiProgress': {
|
|
if (!isFirstGlobalResolved) return;
|
|
const global = getGlobal();
|
|
if (!selectTabState(global).isMasterTab) return;
|
|
|
|
const { messageId } = data;
|
|
|
|
cancelApiProgressMaster(messageId);
|
|
break;
|
|
}
|
|
|
|
case 'callApi': {
|
|
if (!isFirstGlobalResolved) return;
|
|
const global = getGlobal();
|
|
if (!selectTabState(global).isMasterTab) return;
|
|
|
|
const {
|
|
name, messageId, token, args, withCallback,
|
|
} = data;
|
|
|
|
const argsWithCallback = (withCallback ? [...args, (...callbackArgs: any[]) => {
|
|
channel.postMessage({
|
|
type: 'messageCallback',
|
|
token,
|
|
messageId,
|
|
callbackArgs,
|
|
});
|
|
}] : args) as MethodArgs<keyof Methods>;
|
|
|
|
(async () => {
|
|
const result = await (callApiLocal(name, ...argsWithCallback));
|
|
|
|
channel.postMessage({
|
|
type: 'messageResponse',
|
|
token,
|
|
messageId,
|
|
response: result,
|
|
});
|
|
})();
|
|
|
|
break;
|
|
}
|
|
|
|
case 'langpackRefresh': {
|
|
getActions().refreshLangPackFromCache({ langCode: data.langCode });
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function requestGlobal(appVersion: string): Promise<void> {
|
|
if (channel) {
|
|
channel.postMessage({
|
|
type: 'requestGlobal',
|
|
appVersion,
|
|
});
|
|
}
|
|
|
|
const resolveWithoutGlobal = () => {
|
|
if (resolveGlobalPromise) {
|
|
resolveGlobalPromise();
|
|
resolveGlobalPromise = undefined;
|
|
}
|
|
isFirstGlobalResolved = true;
|
|
};
|
|
|
|
if (localStorage.getItem(MULTITAB_LOCALSTORAGE_KEY)) {
|
|
setTimeout(resolveWithoutGlobal, MULTITAB_ESTABLISH_TIMEOUT);
|
|
} else {
|
|
resolveWithoutGlobal();
|
|
return Promise.resolve();
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
resolveGlobalPromise = resolve;
|
|
});
|
|
}
|
|
|
|
export function notifyLangpackUpdate(langCode: string) {
|
|
if (!channel) return;
|
|
|
|
channel.postMessage({
|
|
type: 'langpackRefresh',
|
|
langCode,
|
|
});
|
|
}
|