mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user