Skip to content

Commit

Permalink
feat: Propagate pushState/replaceState inside worker (#172)
Browse files Browse the repository at this point in the history
* feat: forward history.pushState/replaceState calls to worker

* fix: keep passing doc.baseURI in LocationUpdateData

* feat: add basic PopStateEvent and HashChangeEvent implementation

* refactor: remove popstate/hashchange event handlers from worker-location

* refactor: simplify logic responsible for preventing history change propagation back to main thread

* refactor: remove duplicated declaration

* test: propagate pushState and replaceState inside worker

* fix: reset $propagateHistoryChange$ even after pushState or replaceState throws

* test: assign originalPushState right before patching it
  • Loading branch information
slawekkolodziej committed May 24, 2022
1 parent a03255b commit 5b90408
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 17 deletions.
42 changes: 29 additions & 13 deletions src/lib/sandbox/main-register-window.ts
@@ -1,6 +1,6 @@
import { debug } from '../utils';
import { logMain, normalizedWinId } from '../log';
import { MainWindow, PartytownWebWorker, WinId, WorkerMessageType } from '../types';
import { MainWindow, PartytownWebWorker, WinId, WorkerMessageType, LocationUpdateType } from '../types';
import { winCtxs, windowIds } from './main-constants';

export const registerWindow = (
Expand Down Expand Up @@ -29,23 +29,39 @@ export const registerWindow = (
const pushState = history.pushState.bind(history);
const replaceState = history.replaceState.bind(history);

const onLocationChange = () =>
setTimeout(() =>
worker.postMessage([WorkerMessageType.LocationUpdate, $winId$, doc.baseURI])
);
const onLocationChange = (type: LocationUpdateType, state: object, newUrl?: string, oldUrl?: string) => {
setTimeout(() =>{
worker.postMessage([WorkerMessageType.LocationUpdate, {
$winId$,
type,
state,
url: doc.baseURI,
newUrl,
oldUrl
}])
});
}

history.pushState = (data, _, url) => {
pushState(data, _, url);
onLocationChange();
history.pushState = (state, _, newUrl) => {
pushState(state, _, newUrl);
onLocationChange(
LocationUpdateType.PushState,
state,
newUrl?.toString()
);
};

history.replaceState = (data, _, url) => {
replaceState(data, _, url);
onLocationChange();
history.replaceState = (state, _, newUrl) => {
replaceState(state, _, newUrl);
onLocationChange(LocationUpdateType.ReplaceState, state, newUrl?.toString());
};

$window$.addEventListener('popstate', onLocationChange);
$window$.addEventListener('hashchange', onLocationChange);
$window$.addEventListener('popstate', (event) => {
onLocationChange(LocationUpdateType.PopState, event.state);
});
$window$.addEventListener('hashchange', (event) => {
onLocationChange(LocationUpdateType.HashChange, {}, event.newURL, event.oldURL);
});
doc.addEventListener('visibilitychange', () =>
worker.postMessage([WorkerMessageType.DocumentVisibilityState, $winId$, doc.visibilityState])
);
Expand Down
20 changes: 18 additions & 2 deletions src/lib/types.ts
Expand Up @@ -50,7 +50,7 @@ export type MessageFromSandboxToWorker =
| [type: WorkerMessageType.InitializedScripts, winId: WinId]
| [type: WorkerMessageType.RefHandlerCallback, callbackData: RefHandlerCallbackData]
| [type: WorkerMessageType.ForwardMainTrigger, triggerData: ForwardMainTriggerData]
| [type: WorkerMessageType.LocationUpdate, winId: WinId, documentBaseURI: string]
| [type: WorkerMessageType.LocationUpdate, locationChangeData: LocationUpdateData]
| [type: WorkerMessageType.DocumentVisibilityState, winId: WinId, visibilityState: string]
| [
type: WorkerMessageType.CustomElementCallback,
Expand Down Expand Up @@ -79,6 +79,22 @@ export const enum WorkerMessageType {
CustomElementCallback,
}

export const enum LocationUpdateType {
PushState,
ReplaceState,
PopState,
HashChange
}

export interface LocationUpdateData {
$winId$: WinId;
type: LocationUpdateType,
state: object;
url: string;
newUrl?: string;
oldUrl?: string;
}

export interface ForwardMainTriggerData {
$winId$: WinId;
$forward$: string[];
Expand Down Expand Up @@ -178,6 +194,7 @@ export interface WebWorkerEnvironment {
$runWindowLoadEvent$?: number;
$isSameOrigin$?: boolean;
$isTopWindow$?: boolean;
$propagateHistoryChange$?: boolean;
}

export interface MembersInterfaceTypeInfo {
Expand Down Expand Up @@ -332,7 +349,6 @@ export type SerializedTransfer =
| SerializedNodeListTransfer
| SerializedObjectTransfer
| SerializedPrimitiveTransfer
| SerializedPrimitiveTransfer
| SerializedRefTransfer
| [];

Expand Down
8 changes: 7 additions & 1 deletion src/lib/web-worker/index.ts
Expand Up @@ -8,6 +8,7 @@ import { initNextScriptsInWebWorker } from './worker-exec';
import { initWebWorker } from './init-web-worker';
import { logWorker, normalizedWinId } from '../log';
import { workerForwardedTriggerHandle } from './worker-forwarded-trigger';
import { forwardLocationChange } from "./worker-location";

const queuedEvents: MessageEvent<MessageFromSandboxToWorker>[] = [];

Expand Down Expand Up @@ -40,7 +41,12 @@ const receiveMessageFromSandboxToWorker = (ev: MessageEvent<MessageFromSandboxTo
} else if (msgType === WorkerMessageType.DocumentVisibilityState) {
environments[msgValue].$visibilityState$ = msg[2];
} else if (msgType === WorkerMessageType.LocationUpdate) {
environments[msgValue].$location$.href = msg[2];
const $winId$ = msgValue.$winId$;
const env = environments[$winId$];

forwardLocationChange(msgValue.$winId$, env, msgValue);

env.$location$.href = msgValue.url;
} else if (msgType === WorkerMessageType.CustomElementCallback) {
callCustomElementCallback(...msg);
}
Expand Down
24 changes: 24 additions & 0 deletions src/lib/web-worker/worker-location.ts
@@ -0,0 +1,24 @@
import { LocationUpdateData, LocationUpdateType, WebWorkerEnvironment } from "../types";

export function forwardLocationChange($winId$: number, env: WebWorkerEnvironment, data: LocationUpdateData) {
const history = env.$window$.history;

switch (data.type) {
case LocationUpdateType.PushState: {
env.$propagateHistoryChange$ = false;
try {
history.pushState(data.state, '', data.newUrl)
} catch (e) {}
env.$propagateHistoryChange$ = true;
break;
}
case LocationUpdateType.ReplaceState: {
env.$propagateHistoryChange$ = false;
try {
history.replaceState(data.state, '', data.newUrl)
} catch (e) {}
env.$propagateHistoryChange$ = true;
break;
}
}
}
15 changes: 15 additions & 0 deletions src/lib/web-worker/worker-window.ts
Expand Up @@ -433,6 +433,21 @@ export const createWindow = (
},
length: 0,
};
win.indexeddb = undefined;
} else {
const originalPushState: Window['history']['pushState'] = win.history.pushState.bind(win.history);
const originalReplaceState: Window['history']['replaceState'] = win.history.replaceState.bind(win.history);

win.history.pushState = (stateObj: any, _: string, newUrl?: string) => {
if (env.$propagateHistoryChange$ !== false) {
originalPushState(stateObj, _, newUrl)
}
}
win.history.replaceState = (stateObj: any, _: string, newUrl?: string) => {
if (env.$propagateHistoryChange$ !== false) {
originalReplaceState(stateObj, _, newUrl)
}
}
}

win.Worker = undefined;
Expand Down
10 changes: 10 additions & 0 deletions tests/platform/history/history.spec.ts
Expand Up @@ -58,4 +58,14 @@ test('history', async ({ page }) => {
const buttonIframeReplaceState = page.locator('#buttonIframeReplaceState');
await buttonIframeReplaceState.click();
await expect(testIframeReplaceState).toHaveText('88');

const testMainPushStateEcho = page.locator('#testMainPushStateEcho');
const buttonMainPushState = page.locator('#buttonMainPushState');
await buttonMainPushState.click();
await expect(testMainPushStateEcho).toHaveText('{"state":42,"url":"/tests/platform/history/pushed-state"}');

const testMainReplaceStateEcho = page.locator('#testMainReplaceStateEcho');
const buttonMainReplaceState = page.locator('#buttonMainReplaceState');
await buttonMainReplaceState.click();
await expect(testMainReplaceStateEcho).toHaveText('{"state":23,"url":"/tests/platform/history"}');
});
58 changes: 57 additions & 1 deletion tests/platform/history/index.html
Expand Up @@ -186,8 +186,10 @@ <h1>History</h1>
(function () {
const buttonPatchPushState = document.getElementById('buttonPatchPushState');
buttonPatchPushState.addEventListener('click', () => {
history.pushState = function (data, title) {
const originalPushState = history.pushState.bind(history);
history.pushState = function (data, title, url) {
document.getElementById('testPatchPushState').textContent = data + ' Valley';
originalPushState(data, title, url);
};
history.pushState('Hill', '');
});
Expand Down Expand Up @@ -216,6 +218,60 @@ <h1>History</h1>
</script>
</li>

<li>
<strong>main pushState</strong>
<button id="buttonMainPushState">main pushState</button>
<code id="testMainPushStateEcho"></code>
<script type="text/partytown">
(function () {
const elm = document.getElementById('testMainPushStateEcho');

const originalPushState = history.pushState.bind(history);

history.pushState = function(state, _, url) {
console.log('[debug]', state, _, url);
originalPushState(state, _, url);
elm.textContent = JSON.stringify({ state, url });
}
})();
</script>
<script type="text/javascript">
(function () {
const buttonMainPushState = document.getElementById('buttonMainPushState');
buttonMainPushState.addEventListener('click', () => {
console.log('[debug] history pushState =', history.pushState)
history.pushState(42, '', '/tests/platform/history/pushed-state');
});
})();
</script>
</li>

<li>
<strong>main replaceState</strong>
<button id="buttonMainReplaceState">main replaceState</button>
<code id="testMainReplaceStateEcho"></code>
<script type="text/partytown">
(function () {
const elm = document.getElementById('testMainReplaceStateEcho');

const originalReplaceState = history.replaceState.bind(history);
history.replaceState = function(state, _, url) {
originalReplaceState(state, _, url);
elm.textContent = JSON.stringify({ state, url });
}
})();
</script>
<script type="text/javascript">
(function () {
const buttonMainReplaceState = document.getElementById('buttonMainReplaceState');

buttonMainReplaceState.addEventListener('click', () => {
history.replaceState(23, '', '/tests/platform/history');
});
})();
</script>
</li>

<script type="text/partytown">
(function () {
document.body.classList.add('completed');
Expand Down

1 comment on commit 5b90408

@vercel
Copy link

@vercel vercel bot commented on 5b90408 May 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.