Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MA-19001: fix run-sync #426

Merged
merged 2 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@vkontakte/vk-mini-apps-router",
"version": "1.4.5",
"version": "1.4.6",
"description": "React-роутер для мини-приложений ВКонтакте, построенных на VKUI",
"main": "./dist/index.js",
"typings": "./dist/index.d.ts",
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
Loading