Skip to content

Commit

Permalink
MA-19001: fix run-sync
Browse files Browse the repository at this point in the history
  • Loading branch information
pavel-nikitin-2022 committed Aug 8, 2024
1 parent cb14765 commit 0ac634d
Show file tree
Hide file tree
Showing 11 changed files with 110 additions and 77 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
v1.4.6
-
- Устранены проблемы, связанные с работой runSync, все переданные переходы, выполняются

v1.4.5
-
- Устранена проблема с синхронизацией состояния роутера при многократном вызове хуков
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ export const OnboardingThree = ({ nav }: { nav: string }) => {
const onClick = async () => {
routeNavigator.runSync([
() => routeNavigator.backToFirst(),
() => routeNavigator.replace('/'),
() => routeNavigator.push('/'),
() => routeNavigator.back(),
() => routeNavigator.push('/persik'),
() => routeNavigator.push('/persik/persik_modal'),
]);
};
return (
Expand All @@ -19,7 +18,10 @@ export const OnboardingThree = ({ nav }: { nav: string }) => {
<Div>Когда персик огорчен, он выглядит так:</Div>
<img height={130} className="Persik" src={persik_sad} alt="Persik The Cat" />
<Group>
<CellButton onClick={onClick}>Закончить</CellButton>
<Div>1) Возвращаемся в начало навигации</Div>
<Div>2) /persik</Div>
<Div>3) /persik/persik_modal</Div>
<CellButton onClick={onClick}>Выполнить runSync</CellButton>
</Group>
</ModalPage>
);
Expand Down
50 changes: 21 additions & 29 deletions src/components/RouterProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { Action, Router } from '@remix-run/router';
import { PopoutContext, RouteContext, RouterContext } from '../contexts';
import { ReactElement, ReactNode, useEffect, useLayoutEffect, useMemo, useState } from 'react';
import { DefaultRouteNavigator } from '../services/DefaultRouteNavigator';
import { Action, Router } from '@remix-run/router';
import bridge from '@vkontakte/vk-bridge';
import { DefaultNotFound } from './DefaultNotFound';
import { getRouteContext, useForceUpdate } from '../utils/utils';
import { ViewHistory } from '../services/ViewHistory';
import { useBlockForwardToModals } from '../hooks/useBlockForwardToModals';
import { SEARCH_PARAM_INFLATE, STATE_KEY_SHOW_POPOUT, UNIVERSAL_URL } from '../const';
import { RouteNavigator } from '../services/RouteNavigator.type';
import { TransactionExecutor } from '../services/TransactionExecutor';
import { fillHistory } from '../utils/fillHistory';
import { createSearchParams } from '../utils/createSearchParams';
import { PopoutContext, RouteContext, RouterContext } from '../contexts';
import { getRouteContext, fillHistory, createSearchParams, getHrefWithoutHash } from '../utils';
import {
DefaultRouteNavigator,
ContextThrottleService,
TransactionExecutor,
RouteNavigator,
ViewHistory,
} from '../services';
import { useBlockForwardToModals } from '../hooks/useBlockForwardToModals';
import { DefaultNotFound } from './DefaultNotFound';
import { RouteLeaf } from '../type';
import { getHrefWithoutHash } from '../utils/getHrefWithoutHash';
import { ContextThrottleService } from '../services/ContextThrottleService';

export interface RouterProviderProps {
router: Router;
Expand All @@ -37,24 +36,19 @@ export function RouterProvider({
useBridge = true,
throttled = true,
}: RouterProviderProps): ReactElement {
const forceUpdate = useForceUpdate();
const [popout, setPopout] = useState<JSX.Element | null>(null);
const [viewHistory] = useState<ViewHistory>(new ViewHistory());
const [panelsHistory, setPanelsHistory] = useState<string[]>([]);
const [transactionExecutor, setTransactionExecutor] = useState<TransactionExecutor>(
new TransactionExecutor(forceUpdate),
);
const isPopoutShown = router.state.location.state?.[STATE_KEY_SHOW_POPOUT];

const dataRouterContext = useMemo(() => {
const routeNavigator: RouteNavigator = new DefaultRouteNavigator(
router,
viewHistory,
transactionExecutor,
setPopout,
);
return { router, routeNavigator, viewHistory };
}, [router, viewHistory, transactionExecutor, setPopout]);
}, [router, viewHistory, setPopout]);

const routeContext = useMemo(
() => getRouteContext(router.state, panelsHistory),
Expand All @@ -70,14 +64,15 @@ export function RouterProvider({
// Отключаем браузерное восстановление скролла, используем решения от VKUI
history.scrollRestoration = 'manual';

TransactionExecutor.resetTransactions();
viewHistory.resetHistory();
viewHistory.updateNavigation({ ...router.state, historyAction: Action.Push });
setPanelsHistory(viewHistory.panelsHistory);

router.subscribe((state) => {
viewHistory.updateNavigation(state);
setPanelsHistory(viewHistory.panelsHistory);
transactionExecutor.doNext();
TransactionExecutor.doNext();
});

if (useBridge) {
Expand All @@ -86,31 +81,28 @@ export function RouterProvider({
router.navigate(event.detail.data.location, { replace: true });
}
});

router.subscribe((state) => {
const href = router.createHref(state.location);
const hrefWithoutHash = getHrefWithoutHash();
const location = href.replace(hrefWithoutHash, '').replace(/^#/, '');

bridge.send('VKWebAppSetLocation', { location, replace_state: true });
});
}

const executor = new TransactionExecutor(forceUpdate);
setTransactionExecutor(executor);
const searchParams = createSearchParams(router.state.location.search);
const enableFilling = Boolean(searchParams.get(SEARCH_PARAM_INFLATE));
hierarchy &&
enableFilling &&
fillHistory(hierarchy, dataRouterContext.routeNavigator, routeContext, executor);
if (hierarchy && enableFilling) {
fillHistory(hierarchy, dataRouterContext.routeNavigator, routeContext);
}
}, [router]);

useLayoutEffect(() => {
ContextThrottleService.updateThrottledServiceSettings({
interval,
firstActionDelay: transactionExecutor.initialDelay,
enable: throttled || Boolean(transactionExecutor.initialDelay),
throttled,
});
}, [transactionExecutor.initialDelay, interval, throttled]);
}, [interval, throttled]);

const routeNotFound = Boolean(
!routeContext.match ||
Expand Down
25 changes: 14 additions & 11 deletions src/services/ContextThrottleService.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
import { EventBus } from './EventBus';
import { TransactionExecutor } from './TransactionExecutor';

interface ContextThrottleInfo {
prevValue: unknown;
updateTimerId: number;
throttledValue: unknown;
lastUpdateTimestamp: number;
updateTimerId: number;
}

interface ContextThrottleServiceSettings {
interval: number;
firstActionDelay: number;
enable: boolean;
throttled: boolean;
}

export class ContextThrottleService {
private static instance?: ContextThrottleService;
private enable = true;
private interval = 0;
private firstActionDelay = 0;
private throttled = true;
private contextThrottleMap: Record<string, ContextThrottleInfo> = {};

// eslint-disable-next-line @typescript-eslint/no-empty-function
Expand Down Expand Up @@ -51,8 +50,7 @@ export class ContextThrottleService {
private getTimeUntilNextUpdate(lastUpdateTimestamp: number) {
const timeSinceLastUpdate = Date.now() - lastUpdateTimestamp;
const delayUntilNextUpdate = this.interval - timeSinceLastUpdate;
const initialDelay = delayUntilNextUpdate <= 0 ? this.firstActionDelay : 0;
return Math.max(initialDelay, delayUntilNextUpdate);
return delayUntilNextUpdate;
}

private updateContextValue<T>(contextName: string, newValue: T) {
Expand All @@ -65,27 +63,33 @@ export class ContextThrottleService {

private throttleUpdateContextValue<T>(contextName: string, newValue: T) {
const contextData = this.getContextThrottleInfoByName(contextName);
clearTimeout(contextData.updateTimerId);
if (this.isRunSyncActive()) return;

const lastUpdateTimestamp = contextData.lastUpdateTimestamp;
const timeUntilNextUpdate = this.getTimeUntilNextUpdate(lastUpdateTimestamp);

if (timeUntilNextUpdate <= 0) {
this.updateContextValue(contextName, newValue);
} else {
clearTimeout(contextData.updateTimerId);
contextData.updateTimerId = setTimeout(() => {
this.updateContextValue(contextName, newValue);
}, timeUntilNextUpdate);
}
}

private isRunSyncActive() {
return TransactionExecutor.isRunSyncActive;
}

public static triggerContextUpdate<T>(contextName: string, newValue: T) {
const throttledService = ContextThrottleService.getInstance();

if (!throttledService.isContextChange(contextName, newValue)) {
return;
}

if (!throttledService.enable) {
if (!throttledService.throttled && !throttledService.isRunSyncActive()) {
throttledService.updateContextValue(contextName, newValue);
} else {
throttledService.throttleUpdateContextValue(contextName, newValue);
Expand All @@ -94,8 +98,7 @@ export class ContextThrottleService {

public static updateThrottledServiceSettings(settings: ContextThrottleServiceSettings) {
const throttledService = ContextThrottleService.getInstance();
throttledService.enable = settings.enable;
throttledService.interval = settings.interval;
throttledService.firstActionDelay = settings.firstActionDelay;
throttledService.throttled = settings.throttled;
}
}
13 changes: 6 additions & 7 deletions src/services/DefaultRouteNavigator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ export class DefaultRouteNavigator implements RouteNavigator {
constructor(
router: Router,
private viewHistory: ViewHistory,
private transactionExecutor: TransactionExecutor,
setPopout: (popout: JSX.Element | null) => void,
) {
this.router = router;
Expand Down Expand Up @@ -67,22 +66,22 @@ export class DefaultRouteNavigator implements RouteNavigator {
if (this.viewHistory.position > 0) {
await this.go(-this.viewHistory.position);
} else {
await this.transactionExecutor.doNext();
await TransactionExecutor.doNext();
}
}

public async go(to: number): Promise<void> {
if (to === 0) {
await this.transactionExecutor.doNext();
await TransactionExecutor.doNext();
} else {
await this.router.navigate(to);
}
}

public runSync(actions: VoidFunction[]): Promise<void> {
const transaction = new NavigationTransaction(actions);
this.transactionExecutor.add(transaction);
this.transactionExecutor.doNext();
TransactionExecutor.add(transaction);
TransactionExecutor.doNext();
return transaction.donePromise;
}

Expand All @@ -108,7 +107,7 @@ Make sure this route exists or use hideModal with pushPanel set to false.`);
}
await this.navigate(path, { keepSearchParams: true });
} else {
await this.transactionExecutor.doNext();
await TransactionExecutor.doNext();
}
}
}
Expand Down Expand Up @@ -142,7 +141,7 @@ Make sure this route exists or use hideModal with pushPanel set to false.`);
await this.router.navigate(-1);
}
} else {
await this.transactionExecutor.doNext();
await TransactionExecutor.doNext();
}
}

Expand Down
48 changes: 34 additions & 14 deletions src/services/TransactionExecutor.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,49 @@
import { NavigationTransaction } from '../entities/NavigationTransaction';

export class TransactionExecutor {
private static instance?: TransactionExecutor;
private transactions: NavigationTransaction[] = [];

constructor(private forceUpdate: () => void) {}
// eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {}

get initialDelay(): number {
return this.transactions.length > 1 ||
(this.transactions.length > 0 && this.transactions[0].isMultiAction)
? 100
: 0;
public static getInstance() {
if (!TransactionExecutor.instance) {
TransactionExecutor.instance = new TransactionExecutor();
}

return TransactionExecutor.instance;
}

public static get isRunSyncActive() {
const transactionExecutor = TransactionExecutor.getInstance();
const hasMultipleTransactions = transactionExecutor.transactions.length > 1;
const hasSingleMultiActionTransaction =
transactionExecutor.transactions.length === 1 &&
transactionExecutor.transactions[0].isMultiAction;

return hasMultipleTransactions || hasSingleMultiActionTransaction;
}

public static add(transaction: NavigationTransaction) {
const transactionExecutor = TransactionExecutor.getInstance();
transactionExecutor.transactions.push(transaction);
}

add(transaction: NavigationTransaction): void {
this.transactions.push(transaction);
this.forceUpdate();
public static resetTransactions() {
const transactionExecutor = TransactionExecutor.getInstance();
transactionExecutor.transactions = [];
}

async doNext(): Promise<void> {
public static async doNext(): Promise<void> {
const transactionExecutor = TransactionExecutor.getInstance();
const transactions = transactionExecutor.transactions;
// Нужно делать асинхронно, иначе будет бесконечный цикл навигация-изменение стейта-навигация...
setTimeout(() => {
if (this.transactions.length) {
this.transactions[0].doNext();
if (this.transactions[0].finished) {
this.transactions.shift();
if (transactions.length) {
transactions[0].doNext();
if (transactions[0].finished) {
transactions.shift();
}
}
});
Expand Down
9 changes: 9 additions & 0 deletions src/services/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export * from './BridgeService';
export * from './ContextThrottleService';
export * from './DefaultRouteNavigator';
export * from './EventBus';
export * from './InitialLocation';
export * from './RouteNavigator.type';
export * from './TransactionExecutor';
export * from './ViewHistory';
export * from './ViewNavigationRecord.type';
5 changes: 2 additions & 3 deletions src/utils/fillHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ export function fillHistory(
config: RouteLeaf[],
routeNavigator: RouteNavigator,
context: RouteContextObject,
transactionExecutor: TransactionExecutor,
) {
const leafs = flattenBranch(config, []);
const currentLocation = context.state.location;
Expand All @@ -49,8 +48,8 @@ export function fillHistory(
() => routeNavigator.push(to),
];
const transaction = new NavigationTransaction(actions);
transactionExecutor.add(transaction);
transactionExecutor.doNext();
TransactionExecutor.add(transaction);
TransactionExecutor.doNext();
}
});
}
9 changes: 9 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export * from './buildPanelPathFromModalMatch';
export * from './createBrowserRouter';
export * from './createHashParamRouter';
export * from './createHashRouter';
export * from './createSearchParams';
export * from './fillHistory';
export * from './getHrefWithoutHash';
export * from './react-router-override';
export * from './utils';
5 changes: 5 additions & 0 deletions src/utils/react-router-override/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './HashParamHistory';
export * from './UrlHistoryOptions.type';
export * from './createLocation';
export * from './getHistoryState';
export * from './getUrlBasedHistory';
Loading

0 comments on commit 0ac634d

Please sign in to comment.