Improve caching

This commit is contained in:
2026-07-21 18:45:59 +10:00
parent 92c61abcfe
commit 7d134ff8c9
24 changed files with 519 additions and 105 deletions
+93
View File
@@ -21,6 +21,8 @@ async function harness(blockB: boolean | 'redirect' = false) {
{ path: '/left', component: Page, meta: { native: { siblingOrder: 0, siblingHistory: 'replace' } } },
{ path: '/middle', component: Page, meta: { native: { siblingOrder: 1, siblingHistory: 'replace' } } },
{ path: '/right', component: Page, meta: { native: { siblingOrder: 2, siblingHistory: 'replace' } } },
{ path: '/no-cache', component: Page, meta: { native: { cache: false, parent: '/a' } } },
{ path: '/pinned', component: Page, meta: { native: { cache: 'pin', parent: '/a' } } },
],
})
if (blockB) router.beforeEach((to) => to.path === '/b' ? (blockB === 'redirect' ? '/modal' : false) : undefined)
@@ -145,6 +147,97 @@ describe('native router transactions', () => {
await native.cancelInteractive()
})
it('creates sibling views lazily and retains visited replace-style siblings', async () => {
const { native } = await harness()
expect(native.entries.value.map((entry) => entry.route.path)).toEqual(['/a'])
await native.replace('/left', { presentation: 'none' })
expect(native.entries.value.some((entry) => entry.route.path === '/middle')).toBe(false)
await native.sibling('/middle', { replace: true })
expect(native.entries.value.find((entry) => entry.route.path === '/left')).toMatchObject({ mounted: true, status: 'inactive' })
expect(native.entries.value.find((entry) => entry.route.path === '/middle')).toMatchObject({ mounted: true, status: 'active' })
expect(native.entries.value.some((entry) => entry.route.path === '/right')).toBe(false)
})
it('evicts the least-recently-used inactive view when the cache limit is exceeded', async () => {
let clock = 0
vi.spyOn(performance, 'now').mockImplementation(() => ++clock)
const { native } = await harness()
await native.replace('/left', { presentation: 'none' })
await native.sibling('/middle', { replace: true })
await native.sibling('/right', { replace: true })
await native.sibling('/a', { replace: true })
expect(native.entries.value.find((entry) => entry.route.path === '/left')).toMatchObject({
mounted: false,
evictionReason: 'cache-limit',
})
expect(native.entries.value.find((entry) => entry.route.path === '/middle')?.mounted).toBe(true)
expect(native.entries.value.find((entry) => entry.route.path === '/right')?.mounted).toBe(true)
expect(native.cacheStats.value.inactive).toBe(2)
})
it('evicts a pushed route after it is popped out of history', async () => {
const { native } = await harness()
await native.push('/b')
const pushedKey = native.activeKey.value
await native.pop()
expect(native.entries.value.find((entry) => entry.key === pushedKey)).toMatchObject({
mounted: false,
status: 'evicted',
evictionReason: 'popped',
})
})
it('honors cache opt-out even for a route that remains in back history', async () => {
const { native } = await harness()
await native.push('/no-cache')
const noCacheKey = native.activeKey.value
await native.push('/c')
expect(native.entries.value.find((entry) => entry.key === noCacheKey)).toMatchObject({
mounted: false,
evictionReason: 'cache-disabled',
})
expect(await native.beginInteractive('pop')).not.toBeNull()
expect(native.entries.value.find((entry) => entry.key === noCacheKey)?.mounted).toBe(true)
await native.cancelInteractive()
})
it('keeps pinned views during normal trims and releases them when requested', async () => {
const { native } = await harness()
await native.replace('/pinned', { presentation: 'none' })
const pinnedKey = native.activeKey.value
await native.push('/c')
native.trimCache()
expect(native.entries.value.find((entry) => entry.key === pinnedKey)?.mounted).toBe(true)
native.trimCache({ includePinned: true })
expect(native.entries.value.find((entry) => entry.key === pinnedKey)).toMatchObject({
mounted: false,
evictionReason: 'trimmed',
})
})
it('evicts a previously cached target when its guard rejects re-entry', async () => {
const { router, native } = await harness()
await native.replace('/left', { presentation: 'none' })
await native.sibling('/middle', { replace: true })
const cachedLeft = native.entries.value.find((entry) => entry.route.path === '/left')
expect(cachedLeft?.mounted).toBe(true)
const removeGuard = router.beforeEach((to) => to.path === '/left' ? false : undefined)
expect(await native.sibling('/left', { replace: true })).toBe(false)
expect(native.entries.value.find((entry) => entry.key === cachedLeft?.key)).toMatchObject({
mounted: false,
evictionReason: 'navigation-rejected',
})
expect(native.cacheStats.value.totalEvictions).toBeGreaterThan(0)
removeGuard()
})
it('does not preview a stale forward entry after pop then push', async () => {
const { native } = await harness()
await native.push('/b')