refactor(mobile-web): merge the two history installers into one URL rewriter

mobile-web-history-session-fragment and mobile-web-queryless-history were the
same module twice: same writer/target types, same private WeakSet, same origin
guard and try/catch, differing only in the URL mutation. Both installed at
module scope on the same history object, so every pushState was double-wrapped
and each navigation parsed the URL twice.

One installer now takes an ordered rewrite list behind a single WeakSet and a
single URL parse. The shell-session fragment gate moved from install time into
the rewrite, which is where the hash is read anyway.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
Jinwoo-H
2026-09-04 02:11:37 -04:00
parent 2afc88d027
commit 5fa0e7dff3
8 changed files with 202 additions and 239 deletions
+6 -4
View File
@@ -7,20 +7,22 @@ import {
MobileWebNativeShellProvider,
useMobileWebNativeShell
} from '../../src/mobile-web/src/native-shell-channel'
import { installMobileWebHistorySessionFragment } from '../src/mobile-web/mobile-web-history-session-fragment'
import { HostedPageTopInsetProvider } from '../src/mobile-web/hosted-page-top-inset'
import {
MobileWebNativeBehaviorAdapter,
installMobileWebNativeBehaviorAdapters
} from '../src/mobile-web/mobile-web-native-behavior-adapter'
import { installMobileWebQuerylessHistory } from '../src/mobile-web/mobile-web-queryless-history'
import {
installMobileWebHistoryUrlRewriter,
pinMobileWebShellSessionFragment,
stripMobileWebRouteQuery
} from '../src/mobile-web/mobile-web-history-url-rewriter'
import { colors } from '../src/theme/mobile-theme'
import { RpcClientProvider } from '../src/transport/client-context'
import { MobileWebRouteErrorBoundary } from './mobile-web-route-error-boundary'
import { MobileWebRouteRestorer } from './mobile-web-route-restorer'
installMobileWebHistorySessionFragment()
installMobileWebQuerylessHistory()
installMobileWebHistoryUrlRewriter([stripMobileWebRouteQuery, pinMobileWebShellSessionFragment])
installMobileWebNativeBehaviorAdapters()
export default function HostMobileWebLayout() {
@@ -1,64 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import { installMobileWebHistorySessionFragment } from './mobile-web-history-session-fragment'
describe('mobile web history session fragment', () => {
it('keeps the opaque session on same-origin path and query writes', () => {
const target = historyTarget()
expect(installMobileWebHistorySessionFragment(target)).toBe(true)
target.history.pushState({ page: 1 }, '', '/h/host/session/workspace?name=repo')
target.history.replaceState({ page: 2 }, '', '/h/host/tasks#other')
expect(target.pushState).toHaveBeenCalledWith(
{ page: 1 },
'',
`https://orca-mobile-web.invalid/h/host/session/workspace?name=repo#${SESSION_ID}`
)
expect(target.replaceState).toHaveBeenCalledWith(
{ page: 2 },
'',
`https://orca-mobile-web.invalid/h/host/tasks#${SESSION_ID}`
)
})
it('leaves cross-origin and invalid URLs to browser enforcement', () => {
const target = historyTarget()
installMobileWebHistorySessionFragment(target)
target.history.pushState(null, '', 'https://example.test/path')
target.history.replaceState(null, '', 'http://[')
expect(target.pushState).toHaveBeenCalledWith(null, '', 'https://example.test/path')
expect(target.replaceState).toHaveBeenCalledWith(null, '', 'http://[')
})
it('does not wrap missing, malformed, or already wrapped session history', () => {
const target = historyTarget()
expect(
installMobileWebHistorySessionFragment({
...target,
location: { ...target.location, hash: '#short' }
})
).toBe(false)
expect(installMobileWebHistorySessionFragment(target)).toBe(true)
expect(installMobileWebHistorySessionFragment(target)).toBe(false)
})
})
const SESSION_ID = 'S'.repeat(43)
function historyTarget() {
const pushState = vi.fn()
const replaceState = vi.fn()
return {
pushState,
replaceState,
history: { pushState, replaceState },
location: {
hash: `#${SESSION_ID}`,
href: `https://orca-mobile-web.invalid/#${SESSION_ID}`,
origin: 'https://orca-mobile-web.invalid'
}
}
}
@@ -1,58 +0,0 @@
type MobileWebHistoryWriter = (data: unknown, unused: string, url?: string | URL | null) => void
type MobileWebHistoryTarget = {
history: {
pushState: MobileWebHistoryWriter
replaceState: MobileWebHistoryWriter
}
location: {
hash: string
href: string
origin: string
}
}
const SHELL_SESSION_FRAGMENT_PATTERN = /^#[A-Za-z0-9_-]{43}$/
const installedHistories = new WeakSet<object>()
export function installMobileWebHistorySessionFragment(
target: MobileWebHistoryTarget = window
): boolean {
const { history, location } = target
if (!SHELL_SESSION_FRAGMENT_PATTERN.test(location.hash) || installedHistories.has(history)) {
return false
}
history.pushState = sessionBoundHistoryWriter(history, history.pushState, location)
history.replaceState = sessionBoundHistoryWriter(history, history.replaceState, location)
installedHistories.add(history)
return true
}
function sessionBoundHistoryWriter(
history: MobileWebHistoryTarget['history'],
writer: MobileWebHistoryWriter,
location: MobileWebHistoryTarget['location']
): MobileWebHistoryWriter {
return (data, unused, url) => {
writer.call(history, data, unused, sessionBoundHistoryUrl(url, location))
}
}
function sessionBoundHistoryUrl(
value: string | URL | null | undefined,
location: MobileWebHistoryTarget['location']
): string | URL | null | undefined {
if (value == null) {
return value
}
try {
const candidate = new URL(String(value), location.href)
if (candidate.origin !== location.origin) {
return value
}
candidate.hash = location.hash
return candidate.href
} catch {
return value
}
}
@@ -0,0 +1,105 @@
import { describe, expect, it, vi } from 'vitest'
import {
installMobileWebHistoryUrlRewriter,
pinMobileWebShellSessionFragment,
stripMobileWebRouteQuery
} from './mobile-web-history-url-rewriter'
import { mobileWebRouteQuery } from './mobile-web-route-query-cache'
const SESSION_ID = 'S'.repeat(43)
const REWRITES = [stripMobileWebRouteQuery, pinMobileWebShellSessionFragment]
describe('mobile web history url rewriter', () => {
it('strips the query and pins the opaque session in one write', () => {
const target = historyTarget()
expect(installMobileWebHistoryUrlRewriter(REWRITES, target)).toBe(true)
target.history.pushState({ page: 1 }, '', '/h/host/session/workspace?name=repo')
target.history.replaceState({ page: 2 }, '', '/h/host/tasks#other')
expect(target.pushState).toHaveBeenCalledWith(
{ page: 1 },
'',
`https://orca-mobile-web.invalid/h/host/session/workspace#${SESSION_ID}`
)
expect(target.replaceState).toHaveBeenCalledWith(
{ page: 2 },
'',
`https://orca-mobile-web.invalid/h/host/tasks#${SESSION_ID}`
)
expect(mobileWebRouteQuery('/h/host/session/workspace')).toEqual({ name: 'repo' })
})
it('clears stale route state for a queryless write', () => {
const target = historyTarget()
installMobileWebHistoryUrlRewriter(REWRITES, target)
target.history.replaceState(null, '', '/h/host/tasks?taskSource=linear')
target.history.replaceState(null, '', '/h/host/tasks')
expect(mobileWebRouteQuery('/h/host/tasks')).toEqual({})
})
it('leaves cross-origin and invalid URLs to browser enforcement', () => {
const target = historyTarget()
installMobileWebHistoryUrlRewriter(REWRITES, target)
target.history.pushState(null, '', 'https://example.test/path?secret=value')
target.history.replaceState(null, '', 'http://[')
expect(target.pushState).toHaveBeenCalledWith(
null,
'',
'https://example.test/path?secret=value'
)
expect(target.replaceState).toHaveBeenCalledWith(null, '', 'http://[')
})
it('leaves a fragment that is not a shell session alone', () => {
const target = historyTarget({ hash: '#short' })
installMobileWebHistoryUrlRewriter(REWRITES, target)
target.history.pushState(null, '', '/h/host/tasks#other')
expect(target.pushState).toHaveBeenCalledWith(
null,
'',
'https://orca-mobile-web.invalid/h/host/tasks#other'
)
})
it('applies each rewrite in the order it was given', () => {
const target = historyTarget()
const order: string[] = []
installMobileWebHistoryUrlRewriter(
[() => order.push('first'), () => order.push('second')],
target
)
target.history.pushState(null, '', '/h/host/tasks')
expect(order).toEqual(['first', 'second'])
})
it('does not wrap an already wrapped history', () => {
const target = historyTarget()
expect(installMobileWebHistoryUrlRewriter(REWRITES, target)).toBe(true)
expect(installMobileWebHistoryUrlRewriter(REWRITES, target)).toBe(false)
})
})
function historyTarget(overrides: { hash?: string } = {}) {
const pushState = vi.fn()
const replaceState = vi.fn()
return {
pushState,
replaceState,
history: { pushState, replaceState },
location: {
hash: overrides.hash ?? `#${SESSION_ID}`,
href: `https://orca-mobile-web.invalid/#${SESSION_ID}`,
origin: 'https://orca-mobile-web.invalid'
}
}
}
@@ -0,0 +1,89 @@
import { rememberMobileWebRouteQuery } from './mobile-web-route-query-cache'
type MobileWebHistoryWriter = (data: unknown, unused: string, url?: string | URL | null) => void
export type MobileWebHistoryLocation = {
hash: string
href: string
origin: string
}
export type MobileWebHistoryTarget = {
history: {
pushState: MobileWebHistoryWriter
replaceState: MobileWebHistoryWriter
}
location: MobileWebHistoryLocation
}
export type MobileWebHistoryUrlRewrite = (
candidate: URL,
location: MobileWebHistoryLocation
) => void
const SHELL_SESSION_FRAGMENT_PATTERN = /^#[A-Za-z0-9_-]{43}$/
const installedHistories = new WeakSet<object>()
/** The native shell hands the page its session in the fragment; every same-origin write keeps it. */
export const pinMobileWebShellSessionFragment: MobileWebHistoryUrlRewrite = (
candidate,
location
) => {
if (SHELL_SESSION_FRAGMENT_PATTERN.test(location.hash)) {
candidate.hash = location.hash
}
}
/** Route params live in memory so the visible URL never carries them. */
export const stripMobileWebRouteQuery: MobileWebHistoryUrlRewrite = (candidate) => {
rememberMobileWebRouteQuery(candidate.pathname, candidate.searchParams)
candidate.search = ''
}
export function installMobileWebHistoryUrlRewriter(
rewrites: readonly MobileWebHistoryUrlRewrite[],
target: MobileWebHistoryTarget = window
): boolean {
const { history, location } = target
if (installedHistories.has(history)) {
return false
}
history.pushState = rewritingHistoryWriter(history, history.pushState, location, rewrites)
history.replaceState = rewritingHistoryWriter(history, history.replaceState, location, rewrites)
installedHistories.add(history)
return true
}
function rewritingHistoryWriter(
history: MobileWebHistoryTarget['history'],
writer: MobileWebHistoryWriter,
location: MobileWebHistoryLocation,
rewrites: readonly MobileWebHistoryUrlRewrite[]
): MobileWebHistoryWriter {
return (data, unused, url) => {
writer.call(history, data, unused, rewrittenHistoryUrl(url, location, rewrites))
}
}
function rewrittenHistoryUrl(
value: string | URL | null | undefined,
location: MobileWebHistoryLocation,
rewrites: readonly MobileWebHistoryUrlRewrite[]
): string | URL | null | undefined {
if (value == null) {
return value
}
try {
const candidate = new URL(String(value), location.href)
if (candidate.origin !== location.origin) {
return value
}
for (const rewrite of rewrites) {
rewrite(candidate, location)
}
return candidate.href
} catch {
return value
}
}
@@ -1,54 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import { mobileWebRouteQuery } from './mobile-web-route-query-cache'
import { installMobileWebQuerylessHistory } from './mobile-web-queryless-history'
describe('mobile web queryless history', () => {
it('keeps same-origin page state in memory and out of the URL', () => {
const target = historyTarget()
expect(installMobileWebQuerylessHistory(target)).toBe(true)
target.history.pushState({ route: 'session' }, '', '/h/host/session/workspace?name=repo')
expect(target.pushState).toHaveBeenCalledWith(
{ route: 'session' },
'',
'https://orca-mobile-web.invalid/h/host/session/workspace'
)
expect(mobileWebRouteQuery('/h/host/session/workspace')).toEqual({ name: 'repo' })
})
it('clears stale state for a queryless write and leaves foreign URLs to the browser', () => {
const target = historyTarget()
installMobileWebQuerylessHistory(target)
target.history.replaceState(null, '', '/h/host/tasks?taskSource=linear')
target.history.replaceState(null, '', '/h/host/tasks')
target.history.pushState(null, '', 'https://example.test/path?secret=value')
expect(mobileWebRouteQuery('/h/host/tasks')).toEqual({})
expect(target.pushState).toHaveBeenLastCalledWith(
null,
'',
'https://example.test/path?secret=value'
)
})
it('does not wrap a history twice', () => {
const target = historyTarget()
expect(installMobileWebQuerylessHistory(target)).toBe(true)
expect(installMobileWebQuerylessHistory(target)).toBe(false)
})
})
function historyTarget() {
const pushState = vi.fn()
const replaceState = vi.fn()
return {
pushState,
replaceState,
history: { pushState, replaceState },
location: {
href: 'https://orca-mobile-web.invalid/#shell',
origin: 'https://orca-mobile-web.invalid'
}
}
}
@@ -1,57 +0,0 @@
import { rememberMobileWebRouteQuery } from './mobile-web-route-query-cache'
type MobileWebHistoryWriter = (data: unknown, unused: string, url?: string | URL | null) => void
type MobileWebHistoryTarget = {
history: {
pushState: MobileWebHistoryWriter
replaceState: MobileWebHistoryWriter
}
location: {
href: string
origin: string
}
}
const installedHistories = new WeakSet<object>()
export function installMobileWebQuerylessHistory(target: MobileWebHistoryTarget = window): boolean {
const { history, location } = target
if (installedHistories.has(history)) {
return false
}
history.pushState = querylessHistoryWriter(history, history.pushState, location)
history.replaceState = querylessHistoryWriter(history, history.replaceState, location)
installedHistories.add(history)
return true
}
function querylessHistoryWriter(
history: MobileWebHistoryTarget['history'],
writer: MobileWebHistoryWriter,
location: MobileWebHistoryTarget['location']
): MobileWebHistoryWriter {
return (data, unused, url) => {
writer.call(history, data, unused, querylessHistoryUrl(url, location))
}
}
function querylessHistoryUrl(
value: string | URL | null | undefined,
location: MobileWebHistoryTarget['location']
): string | URL | null | undefined {
if (value == null) {
return value
}
try {
const candidate = new URL(String(value), location.href)
if (candidate.origin !== location.origin) {
return value
}
rememberMobileWebRouteQuery(candidate.pathname, candidate.searchParams)
candidate.search = ''
return candidate.href
} catch {
return value
}
}
@@ -70,10 +70,10 @@ describe('mobile web session screen binding', () => {
})
it('keeps one native-shell channel alive across hosted route navigation', () => {
expect(hostedLayout).toContain('installMobileWebHistorySessionFragment()')
expect(hostedLayout).toContain('installMobileWebHistoryUrlRewriter(')
expect(hostedLayout).toContain('<MobileWebNativeShellProvider>')
expect(hostedLayout).toContain('<Stack screenOptions={{ headerShown: false }} />')
expect(hostedLayout.indexOf('installMobileWebHistorySessionFragment()')).toBeLessThan(
expect(hostedLayout.indexOf('installMobileWebHistoryUrlRewriter(')).toBeLessThan(
hostedLayout.indexOf('export default function HostMobileWebLayout()')
)
expect(hostedLayout.indexOf('<MobileWebNativeShellProvider>')).toBeLessThan(