/* eslint-disable eslint-multitab-tt/set-global-only-variable */ import type { FC, FC_withDebug, Props } from './teact'; import React, { DEBUG_resolveComponentName, useEffect } from './teact'; import { requestMeasure } from '../fasterdom/fasterdom'; import { DEBUG, DEBUG_MORE } from '../../config'; import { throttleWithTickEnd } from '../../util/schedulers'; import arePropsShallowEqual, { logUnequalProps } from '../../util/arePropsShallowEqual'; import { orderBy } from '../../util/iteratees'; import { handleError } from '../../util/handleError'; import useForceUpdate from '../../hooks/useForceUpdate'; import useUniqueId from '../../hooks/useUniqueId'; import { isHeavyAnimating } from '../../hooks/useHeavyAnimationCheck'; export default React; type GlobalState = AnyLiteral & { DEBUG_capturedId?: number }; type ActionNames = string; type ActionPayload = any; export interface ActionOptions { forceOnHeavyAnimation?: boolean; // Workaround for iOS gesture history navigation forceSyncOnIOs?: boolean; noUpdate?: boolean; } type Actions = Record void>; type ActionHandler = ( global: GlobalState, actions: Actions, payload: any, ) => GlobalState | void | Promise; type DetachWhenChanged = (current: any) => void; type MapStateToProps = ( (global: GlobalState, ownProps: OwnProps, detachWhenChanged: DetachWhenChanged) => AnyLiteral ); let currentGlobal = {} as GlobalState; // eslint-disable-next-line @typescript-eslint/naming-convention let DEBUG_currentCapturedId: number | undefined; // eslint-disable-next-line @typescript-eslint/naming-convention const DEBUG_releaseCapturedIdThrottled = throttleWithTickEnd(() => { DEBUG_currentCapturedId = undefined; }); const actionHandlers: Record = {}; const callbacks: Function[] = [updateContainers]; const immediateCallbacks: Function[] = []; const actions = {} as Actions; const containers = new Map; ownProps: Props; mappedProps?: Props; forceUpdate: Function; isDetached: boolean; detachReason: any; detachWhenChanged: DetachWhenChanged; DEBUG_updates: number; DEBUG_componentName: string; }>(); const runCallbacksThrottled = throttleWithTickEnd(runCallbacks); let forceOnHeavyAnimation = true; function runImmediateCallbacks() { immediateCallbacks.forEach((cb) => cb(currentGlobal)); } function runCallbacks() { if (forceOnHeavyAnimation) { forceOnHeavyAnimation = false; } else if (isHeavyAnimating()) { requestMeasure(runCallbacksThrottled); return; } callbacks.forEach((cb) => cb(currentGlobal)); } export function setGlobal(newGlobal?: GlobalState, options?: ActionOptions) { if (typeof newGlobal === 'object' && newGlobal !== currentGlobal) { if (DEBUG) { if (newGlobal.DEBUG_capturedId && newGlobal.DEBUG_capturedId !== DEBUG_currentCapturedId) { throw new Error('[TeactN.setGlobal] Attempt to set an outdated global'); } DEBUG_currentCapturedId = undefined; } currentGlobal = newGlobal; if (!options?.noUpdate) runImmediateCallbacks(); if (options?.forceSyncOnIOs) { forceOnHeavyAnimation = true; runCallbacks(); } else { if (options?.forceOnHeavyAnimation) { forceOnHeavyAnimation = true; } runCallbacksThrottled(); } } } export function getGlobal() { if (DEBUG) { DEBUG_currentCapturedId = Math.random(); currentGlobal = { ...currentGlobal, DEBUG_capturedId: DEBUG_currentCapturedId, }; DEBUG_releaseCapturedIdThrottled(); } return currentGlobal; } export function getActions() { return actions; } let actionQueue: NoneToVoidFunction[] = []; function handleAction(name: string, payload?: ActionPayload, options?: ActionOptions) { actionQueue.push(() => { actionHandlers[name]?.forEach((handler) => { const response = handler(DEBUG ? getGlobal() : currentGlobal, actions, payload); if (!response || typeof response.then === 'function') { return; } setGlobal(response as GlobalState, options); }); }); if (actionQueue.length === 1) { try { while (actionQueue.length) { actionQueue[0](); actionQueue.shift(); } } finally { actionQueue = []; } } } function updateContainers() { // eslint-disable-next-line @typescript-eslint/naming-convention let DEBUG_startAt: number | undefined; if (DEBUG) { DEBUG_startAt = performance.now(); } // eslint-disable-next-line no-restricted-syntax for (const container of containers.values()) { const { mapStateToProps, ownProps, mappedProps, forceUpdate, isDetached, detachWhenChanged, } = container; if (isDetached) { continue; } let newMappedProps; try { newMappedProps = mapStateToProps(currentGlobal, ownProps, detachWhenChanged); if (container.isDetached) { continue; } } catch (err: any) { handleError(err); return; } if (DEBUG) { if (Object.values(newMappedProps).some(Number.isNaN)) { // eslint-disable-next-line no-console console.warn( // eslint-disable-next-line max-len `[TeactN] Some of \`${container.DEBUG_componentName}\` mappers contain NaN values. This may cause redundant updates because of incorrect equality check.`, ); } } if (Object.keys(newMappedProps).length && !arePropsShallowEqual(mappedProps!, newMappedProps)) { if (DEBUG_MORE) { logUnequalProps( mappedProps!, newMappedProps, `[TeactN] Will update ${container.DEBUG_componentName} caused by:`, ); } container.mappedProps = newMappedProps; container.DEBUG_updates++; forceUpdate(); } } if (DEBUG) { const updateTime = performance.now() - DEBUG_startAt!; if (updateTime > 7) { // eslint-disable-next-line no-console console.warn(`[TeactN] Slow containers update: ${Math.round(updateTime)} ms`); } } } export function addActionHandler(name: ActionNames, handler: ActionHandler) { if (!actionHandlers[name]) { actionHandlers[name] = []; actions[name] = (payload?: ActionPayload, options?: ActionOptions) => { handleAction(name, payload, options); }; } actionHandlers[name].push(handler); } export function addCallback(cb: Function, isImmediate = false) { (isImmediate ? immediateCallbacks : callbacks).push(cb); } export function removeCallback(cb: Function, isImmediate = false) { const index = (isImmediate ? immediateCallbacks : callbacks).indexOf(cb); if (index !== -1) { (isImmediate ? immediateCallbacks : callbacks).splice(index, 1); } } export function withGlobal( mapStateToProps: MapStateToProps = () => ({}), ) { return (Component: FC) => { function TeactNContainer(props: OwnProps) { const id = useUniqueId(); const forceUpdate = useForceUpdate(); useEffect(() => { return () => { containers.delete(id); }; }, [id]); let container = containers.get(id)!; if (!container) { container = { mapStateToProps, ownProps: props, forceUpdate, isDetached: false, detachReason: undefined, // This allows to ignore changes in global during animation before unmount detachWhenChanged: (current) => { const { detachReason } = container!; if (detachReason === undefined && current !== undefined) { container!.detachReason = current; } else if (detachReason !== undefined && detachReason !== current) { container!.isDetached = true; } }, DEBUG_updates: 0, DEBUG_componentName: Component.name, }; containers.set(id, container); } if (!container.mappedProps || !arePropsShallowEqual(container.ownProps, props)) { container.ownProps = props; if (!container.isDetached) { try { container.mappedProps = mapStateToProps(currentGlobal, props, container.detachWhenChanged); } catch (err: any) { handleError(err); } } } // eslint-disable-next-line react/jsx-props-no-spreading return ; } (TeactNContainer as FC_withDebug).DEBUG_contentComponentName = DEBUG_resolveComponentName(Component); return TeactNContainer; }; } export function typify< ProjectGlobalState, ActionPayloads, >() { type ProjectActionNames = keyof ActionPayloads; // When payload is allowed to be `undefined` we consider it optional type ProjectActions = { [ActionName in ProjectActionNames]: (undefined extends ActionPayloads[ActionName] ? ( (payload?: ActionPayloads[ActionName], options?: ActionOptions) => void ) : ( (payload: ActionPayloads[ActionName], options?: ActionOptions) => void )) }; type ActionHandlers = { [ActionName in keyof ActionPayloads]: ( global: ProjectGlobalState, actions: ProjectActions, payload: ActionPayloads[ActionName], ) => ProjectGlobalState | void | Promise; }; return { getGlobal: getGlobal as () => T, setGlobal: setGlobal as (state: ProjectGlobalState, options?: ActionOptions) => void, getActions: getActions as () => ProjectActions, addActionHandler: addActionHandler as ( name: ActionName, handler: ActionHandlers[ActionName], ) => void, withGlobal: withGlobal as ( mapStateToProps: ( (global: ProjectGlobalState, ownProps: OwnProps, detachWhenChanged: DetachWhenChanged) => AnyLiteral), ) => (Component: FC) => FC, }; } if (DEBUG) { (window as any).getGlobal = getGlobal; (window as any).setGlobal = setGlobal; document.addEventListener('dblclick', () => { // eslint-disable-next-line no-console console.warn( 'GLOBAL CONTAINERS', orderBy( Array.from(containers.values()) .map(({ DEBUG_componentName, DEBUG_updates }) => ({ DEBUG_componentName, DEBUG_updates })), 'DEBUG_updates', 'desc', ), ); }); }