Improve caching
This commit is contained in:
@@ -6,6 +6,8 @@ import type { NativePlatformAdapter, NativeRouterRuntime } from '@native-vue-rou
|
||||
export interface CapacitorAdapterOptions {
|
||||
exitAtRoot?: boolean
|
||||
haptics?: boolean
|
||||
/** Release inactive component trees when the native app backgrounds. Defaults to true. */
|
||||
trimCacheOnPause?: boolean
|
||||
deepLinkPath?: (url: URL) => string
|
||||
}
|
||||
|
||||
@@ -31,7 +33,10 @@ export function createCapacitorAdapter(options: CapacitorAdapterOptions = {}): N
|
||||
const path = options.deepLinkPath?.(parsed) ?? `${parsed.pathname}${parsed.search}${parsed.hash}`
|
||||
void runtime.push(path || '/')
|
||||
}),
|
||||
App.addListener('pause', () => void runtime.cancelInteractive()),
|
||||
App.addListener('pause', () => {
|
||||
void runtime.cancelInteractive()
|
||||
if (options.trimCacheOnPause !== false) runtime.trimCache({ reason: 'memory-pressure' })
|
||||
}),
|
||||
])
|
||||
const launch = await App.getLaunchUrl()
|
||||
if (launch?.url) {
|
||||
|
||||
@@ -4,11 +4,14 @@ import {
|
||||
h,
|
||||
inject,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
onScopeDispose,
|
||||
provide,
|
||||
ref,
|
||||
shallowReactive,
|
||||
watch,
|
||||
type PropType,
|
||||
type InjectionKey,
|
||||
type VNode,
|
||||
} from 'vue'
|
||||
import {
|
||||
@@ -24,8 +27,12 @@ import type {
|
||||
NativeRouterRuntime,
|
||||
NativeSourceRect,
|
||||
NativeViewEntry,
|
||||
NativeViewLifecycle,
|
||||
NativeViewRole,
|
||||
} from './types'
|
||||
|
||||
const nativeViewLifecycleKey: InjectionKey<NativeViewLifecycle> = Symbol('native-view-lifecycle')
|
||||
|
||||
export function useNativeRouter() {
|
||||
const runtime = inject<NativeRouterRuntime>(nativeRouterKey)
|
||||
if (!runtime) throw new Error('Native Vue Router is not installed. Call app.use(nativeRouter).')
|
||||
@@ -33,27 +40,103 @@ export function useNativeRouter() {
|
||||
}
|
||||
|
||||
export function useNativeViewLifecycle() {
|
||||
const runtime = useNativeRouter()
|
||||
return {
|
||||
activeKey: runtime.activeKey,
|
||||
transaction: runtime.transaction,
|
||||
const lifecycle = inject<NativeViewLifecycle>(nativeViewLifecycleKey)
|
||||
if (!lifecycle) throw new Error('Native view lifecycle APIs must be used inside NativeRouterView.')
|
||||
return lifecycle
|
||||
}
|
||||
|
||||
type NativeViewHook = () => void
|
||||
|
||||
function onNativeViewState(source: Readonly<{ value: boolean }>, entering: boolean, hook: NativeViewHook) {
|
||||
onMounted(() => {
|
||||
if (entering && source.value) hook()
|
||||
})
|
||||
watch(() => source.value, (value, previous) => {
|
||||
if (value === entering && previous !== entering) hook()
|
||||
}, { flush: 'sync' })
|
||||
}
|
||||
|
||||
export function onNativeViewActivate(hook: NativeViewHook) {
|
||||
onNativeViewState(useNativeViewLifecycle().isActive, true, hook)
|
||||
}
|
||||
|
||||
export function onNativeViewDeactivate(hook: NativeViewHook) {
|
||||
onNativeViewState(useNativeViewLifecycle().isActive, false, hook)
|
||||
}
|
||||
|
||||
export function onNativeViewShow(hook: NativeViewHook) {
|
||||
onNativeViewState(useNativeViewLifecycle().isVisible, true, hook)
|
||||
}
|
||||
|
||||
export function onNativeViewHide(hook: NativeViewHook) {
|
||||
onNativeViewState(useNativeViewLifecycle().isVisible, false, hook)
|
||||
}
|
||||
|
||||
export function onNativeViewEvict(hook: (reason: NativeViewLifecycle['evictionReason']['value']) => void) {
|
||||
const lifecycle = useNativeViewLifecycle()
|
||||
onBeforeUnmount(() => {
|
||||
if (lifecycle.status.value === 'evicted') hook(lifecycle.evictionReason.value)
|
||||
})
|
||||
}
|
||||
|
||||
function useNativeViewEffect(
|
||||
source: Readonly<{ value: boolean }>,
|
||||
effect: () => void | (() => void),
|
||||
) {
|
||||
let cleanup: void | (() => void)
|
||||
const stopEffect = () => {
|
||||
cleanup?.()
|
||||
cleanup = undefined
|
||||
}
|
||||
const stopWatch = watch(() => source.value, (enabled) => {
|
||||
stopEffect()
|
||||
if (enabled) cleanup = effect()
|
||||
}, { immediate: true, flush: 'sync' })
|
||||
onScopeDispose(() => {
|
||||
stopWatch()
|
||||
stopEffect()
|
||||
})
|
||||
}
|
||||
|
||||
/** Runs an effect only while this route is the semantically active route. */
|
||||
export function useNativeViewActiveEffect(effect: () => void | (() => void)) {
|
||||
useNativeViewEffect(useNativeViewLifecycle().isActive, effect)
|
||||
}
|
||||
|
||||
/** Runs an effect while this route is active or participating in a transition. */
|
||||
export function useNativeViewVisibleEffect(effect: () => void | (() => void)) {
|
||||
useNativeViewEffect(useNativeViewLifecycle().isVisible, effect)
|
||||
}
|
||||
|
||||
const NativeRouteScope = defineComponent({
|
||||
name: 'NativeRouteScope',
|
||||
props: {
|
||||
route: { type: Object as PropType<RouteLocationNormalizedLoaded>, required: true },
|
||||
entryKey: { type: String, required: true },
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
const runtime = useNativeRouter()
|
||||
const scopedRoute = shallowReactive({ ...props.route }) as RouteLocationNormalizedLoaded
|
||||
watch(() => props.route, (route) => Object.assign(scopedRoute, route), { immediate: true })
|
||||
provide(routeLocationKey, scopedRoute)
|
||||
const entry = computed(() => runtime.entries.value.find((candidate) => candidate.key === props.entryKey))
|
||||
const role = computed<NativeViewRole>(() => entry.value ? interactiveRole(entry.value, runtime) : 'inactive')
|
||||
provide<NativeViewLifecycle>(nativeViewLifecycleKey, {
|
||||
key: props.entryKey,
|
||||
route: computed(() => entry.value?.route ?? props.route),
|
||||
status: computed(() => entry.value?.status ?? 'evicted'),
|
||||
role,
|
||||
isActive: computed(() => runtime.activeKey.value === props.entryKey),
|
||||
isVisible: computed(() => role.value !== 'inactive'),
|
||||
isPreview: computed(() => runtime.transaction.value?.toKey === props.entryKey),
|
||||
isCached: computed(() => Boolean(entry.value?.mounted && role.value === 'inactive')),
|
||||
evictionReason: computed(() => entry.value?.evictionReason),
|
||||
})
|
||||
return () => slots.default?.()
|
||||
},
|
||||
})
|
||||
|
||||
function interactiveRole(entry: NativeViewEntry, runtime: NativeRouterRuntime) {
|
||||
function interactiveRole(entry: NativeViewEntry, runtime: NativeRouterRuntime): NativeViewRole {
|
||||
const transaction = runtime.transaction.value
|
||||
if (!transaction) return entry.key === runtime.activeKey.value ? 'active' : 'inactive'
|
||||
if (entry.key === transaction.fromKey) return 'from'
|
||||
@@ -96,7 +179,7 @@ export const NativeRouterView = defineComponent({
|
||||
default: ({ Component, route }: { Component: VNode | null; route: RouteLocationNormalizedLoaded }) => {
|
||||
if (slots.default) return slots.default({ Component, route, entry })
|
||||
return Component
|
||||
? h(NativeRouteScope, { route }, { default: () => Component })
|
||||
? h(NativeRouteScope, { route, entryKey: entry.key }, { default: () => Component })
|
||||
: null
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from 'vue-router'
|
||||
import type {
|
||||
NativeDirection,
|
||||
NativeEvictionReason,
|
||||
NativeGestureKind,
|
||||
NativeNavigationOptions,
|
||||
NativePlatformAdapter,
|
||||
@@ -81,6 +82,7 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
|
||||
readonly activeKey
|
||||
readonly transaction
|
||||
readonly canGoBack
|
||||
readonly cacheStats
|
||||
|
||||
private readonly mutableEntries = shallowRef<NativeViewEntry[]>([])
|
||||
private readonly mutableActiveKey = ref('')
|
||||
@@ -95,14 +97,32 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
|
||||
private removeAfterEach?: () => void
|
||||
private platformCleanup?: () => void
|
||||
private pendingPop?: (failure?: NavigationFailure | void) => void
|
||||
private totalEvictions = 0
|
||||
private lastEviction?: { key: string; route: string; reason: NativeEvictionReason }
|
||||
private memoryPressureCleanup?: () => void
|
||||
|
||||
constructor(options: NativeRouterOptions) {
|
||||
this.router = options.router
|
||||
this.maxInactive = options.cache?.maxInactive ?? 8
|
||||
this.maxInactive = Math.max(0, options.cache?.maxInactive ?? 4)
|
||||
this.platform = options.platform
|
||||
this.entries = computed(() => this.mutableEntries.value)
|
||||
this.activeKey = computed(() => this.mutableActiveKey.value)
|
||||
this.transaction = computed(() => this.mutableTransaction.value)
|
||||
this.cacheStats = computed(() => {
|
||||
const entries = this.mutableEntries.value
|
||||
const mounted = entries.filter((entry) => entry.mounted)
|
||||
const inactive = mounted.filter((entry) => entry.key !== this.mutableActiveKey.value)
|
||||
return {
|
||||
maxInactive: this.maxInactive,
|
||||
descriptors: entries.length,
|
||||
mounted: mounted.length,
|
||||
inactive: inactive.length,
|
||||
pinned: inactive.filter((entry) => entry.route.meta.native?.cache === 'pin').length,
|
||||
evicted: entries.filter((entry) => !entry.mounted).length,
|
||||
totalEvictions: this.totalEvictions,
|
||||
lastEviction: this.lastEviction,
|
||||
}
|
||||
})
|
||||
this.canGoBack = computed(() => {
|
||||
const transaction = this.mutableTransaction.value
|
||||
const committingForwardEntry = transaction
|
||||
@@ -141,6 +161,11 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
|
||||
if (cleanup) this.platformCleanup = cleanup
|
||||
})
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
const trimForPressure = () => this.trimCache({ reason: 'memory-pressure' })
|
||||
window.addEventListener('memorypressure', trimForPressure)
|
||||
this.memoryPressureCleanup = () => window.removeEventListener('memorypressure', trimForPressure)
|
||||
}
|
||||
}
|
||||
|
||||
async preload(to: RouteLocationRaw) {
|
||||
@@ -221,6 +246,7 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
|
||||
target.route = route
|
||||
target.mounted = true
|
||||
target.status = 'inactive'
|
||||
target.evictionReason = undefined
|
||||
this.touchEntries()
|
||||
}
|
||||
} else {
|
||||
@@ -236,6 +262,7 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
|
||||
target.status = 'preview'
|
||||
target.synthetic = false
|
||||
target.lastUsed = now()
|
||||
target.evictionReason = undefined
|
||||
this.touchEntries()
|
||||
} else {
|
||||
target = entryFor(route, 'preview')
|
||||
@@ -309,7 +336,7 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
|
||||
if (!this.isCurrent(current.id)) return !failed
|
||||
if (failed) {
|
||||
await this.animateProgress(0, 0)
|
||||
if (this.isCurrent(current.id)) this.finalizeCancelled(current)
|
||||
if (this.isCurrent(current.id)) this.finalizeCancelled(current, true)
|
||||
return false
|
||||
}
|
||||
this.finalizeCommitted(current)
|
||||
@@ -326,6 +353,16 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
|
||||
this.finalizeCancelled(current)
|
||||
}
|
||||
|
||||
trimCache(options: { includePinned?: boolean; reason?: NativeEvictionReason } = {}) {
|
||||
const reason = options.reason ?? 'trimmed'
|
||||
for (const entry of this.mutableEntries.value) {
|
||||
if (entry.key === this.mutableActiveKey.value || !entry.mounted || entry.status !== 'inactive') continue
|
||||
if (!options.includePinned && entry.route.meta.native?.cache === 'pin') continue
|
||||
this.evictEntry(entry, reason)
|
||||
}
|
||||
this.touchEntries()
|
||||
}
|
||||
|
||||
registerPresentation(definition: NativePresentationDefinition) {
|
||||
this.presentations.set(definition.name, definition)
|
||||
}
|
||||
@@ -350,6 +387,7 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
|
||||
dispose() {
|
||||
this.removeAfterEach?.()
|
||||
this.platformCleanup?.()
|
||||
this.memoryPressureCleanup?.()
|
||||
}
|
||||
|
||||
private activeEntry() {
|
||||
@@ -414,15 +452,17 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
|
||||
velocity: 0,
|
||||
phase: 'settling',
|
||||
}
|
||||
if (failed) this.finalizeCancelled(transaction)
|
||||
if (failed) this.finalizeCancelled(transaction, true)
|
||||
else this.finalizeCommitted(transaction)
|
||||
}
|
||||
|
||||
private finalizeCancelled(transaction: NativeTransaction) {
|
||||
private finalizeCancelled(transaction: NativeTransaction, evictTarget = false) {
|
||||
if (!this.isCurrent(transaction.id)) return
|
||||
this.removePreview(transaction.toKey)
|
||||
this.clearTransaction()
|
||||
if (evictTarget) this.discardTarget(transaction.toKey, 'navigation-rejected')
|
||||
else this.removePreview(transaction.toKey)
|
||||
this.markStatuses()
|
||||
this.enforceCache()
|
||||
}
|
||||
|
||||
private finalizeCommitted(transaction: NativeTransaction) {
|
||||
@@ -430,9 +470,10 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
|
||||
const target = this.entryByKey(transaction.toKey)
|
||||
if (target && this.router.currentRoute.value.fullPath !== target.route.fullPath) {
|
||||
// A redirect is already authoritative; discard only the stale preview.
|
||||
this.removePreview(transaction.toKey)
|
||||
this.clearTransaction()
|
||||
this.discardTarget(transaction.toKey, 'navigation-rejected')
|
||||
this.markStatuses()
|
||||
this.enforceCache()
|
||||
return
|
||||
}
|
||||
if (target) {
|
||||
@@ -486,11 +527,13 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
|
||||
target.status = 'active'
|
||||
target.committed = true
|
||||
target.lastUsed = now()
|
||||
target.evictionReason = undefined
|
||||
this.touchEntries()
|
||||
}
|
||||
this.mutableActiveKey.value = target.key
|
||||
this.acceptHistory(target, transaction)
|
||||
this.markStatuses()
|
||||
if (!transaction) this.enforceCache()
|
||||
}
|
||||
|
||||
private acceptHistory(target: NativeViewEntry, transaction: NativeTransaction | null) {
|
||||
@@ -542,16 +585,49 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
private discardTarget(key: string, reason: NativeEvictionReason) {
|
||||
const entry = this.entryByKey(key)
|
||||
if (!entry || entry.key === this.mutableActiveKey.value) return
|
||||
if (!entry.committed || entry.synthetic) {
|
||||
this.mutableEntries.value = this.mutableEntries.value.filter((candidate) => candidate.key !== key)
|
||||
return
|
||||
}
|
||||
this.evictEntry(entry, reason)
|
||||
}
|
||||
|
||||
private evictEntry(entry: NativeViewEntry, reason: NativeEvictionReason) {
|
||||
if (!entry.mounted || entry.key === this.mutableActiveKey.value) return
|
||||
entry.mounted = false
|
||||
entry.status = 'evicted'
|
||||
entry.evictionReason = reason
|
||||
this.totalEvictions += 1
|
||||
this.lastEviction = { key: entry.key, route: entry.route.fullPath, reason }
|
||||
}
|
||||
|
||||
private shouldRetainInactive(entry: NativeViewEntry) {
|
||||
const policy = entry.route.meta.native?.cache
|
||||
if (policy === false) return false
|
||||
if (policy === 'pin') return true
|
||||
if (this.mutableHistoryKeys.value.includes(entry.key)) return true
|
||||
return entry.route.meta.native?.siblingHistory === 'replace'
|
||||
}
|
||||
|
||||
private enforceCache() {
|
||||
const inactive = this.mutableEntries.value
|
||||
.filter((entry) => entry.key !== this.mutableActiveKey.value && entry.mounted && entry.status === 'inactive')
|
||||
.sort((a, b) => b.lastUsed - a.lastUsed)
|
||||
for (const entry of inactive.slice(this.maxInactive)) {
|
||||
if (entry.route.meta.native?.cache === false || inactive.length > this.maxInactive) {
|
||||
entry.mounted = false
|
||||
entry.status = 'evicted'
|
||||
for (const entry of inactive) {
|
||||
if (!this.shouldRetainInactive(entry)) {
|
||||
const reason: NativeEvictionReason = entry.route.meta.native?.cache === false
|
||||
? 'cache-disabled'
|
||||
: 'popped'
|
||||
this.evictEntry(entry, reason)
|
||||
}
|
||||
}
|
||||
const retained = inactive.filter((entry) => entry.mounted && entry.route.meta.native?.cache !== 'pin')
|
||||
for (const entry of retained.slice(this.maxInactive)) {
|
||||
this.evictEntry(entry, 'cache-limit')
|
||||
}
|
||||
this.touchEntries()
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,15 @@ export type NativePresentationName =
|
||||
export type NativeGestureKind = 'push' | 'pop' | 'sibling' | 'present' | 'dismiss'
|
||||
export type NativeDirection = 'forward' | 'back' | 'up' | 'down'
|
||||
export type NativeViewStatus = 'active' | 'inactive' | 'preview' | 'evicted'
|
||||
export type NativeViewRole = 'active' | 'inactive' | 'from' | 'to'
|
||||
export type NativeCachePolicy = boolean | 'pin'
|
||||
export type NativeEvictionReason =
|
||||
| 'cache-disabled'
|
||||
| 'cache-limit'
|
||||
| 'navigation-rejected'
|
||||
| 'popped'
|
||||
| 'trimmed'
|
||||
| 'memory-pressure'
|
||||
|
||||
export interface NativeRouteOptions {
|
||||
navigator?: string
|
||||
@@ -27,7 +36,8 @@ export interface NativeRouteOptions {
|
||||
siblingGroup?: string
|
||||
siblingOrder?: number
|
||||
siblingHistory?: 'push' | 'replace'
|
||||
cache?: boolean
|
||||
/** `false` disables retention; `pin` exempts the route from LRU trimming. */
|
||||
cache?: NativeCachePolicy
|
||||
gesture?: boolean | 'edge' | 'full'
|
||||
}
|
||||
|
||||
@@ -48,6 +58,30 @@ export interface NativeViewEntry {
|
||||
lastUsed: number
|
||||
scrollX: number
|
||||
scrollY: number
|
||||
evictionReason?: NativeEvictionReason
|
||||
}
|
||||
|
||||
export interface NativeCacheStats {
|
||||
maxInactive: number
|
||||
descriptors: number
|
||||
mounted: number
|
||||
inactive: number
|
||||
pinned: number
|
||||
evicted: number
|
||||
totalEvictions: number
|
||||
lastEviction?: { key: string; route: string; reason: NativeEvictionReason }
|
||||
}
|
||||
|
||||
export interface NativeViewLifecycle {
|
||||
readonly key: string
|
||||
readonly route: Readonly<{ value: RouteLocationNormalizedLoaded }>
|
||||
readonly status: Readonly<{ value: NativeViewStatus }>
|
||||
readonly role: Readonly<{ value: NativeViewRole }>
|
||||
readonly isActive: Readonly<{ value: boolean }>
|
||||
readonly isVisible: Readonly<{ value: boolean }>
|
||||
readonly isPreview: Readonly<{ value: boolean }>
|
||||
readonly isCached: Readonly<{ value: boolean }>
|
||||
readonly evictionReason: Readonly<{ value: NativeEvictionReason | undefined }>
|
||||
}
|
||||
|
||||
export interface NativeSourceRect {
|
||||
@@ -114,6 +148,7 @@ export interface NativeRouterRuntime {
|
||||
readonly activeKey: Readonly<{ value: string }>
|
||||
readonly transaction: Readonly<{ value: NativeTransaction | null }>
|
||||
readonly canGoBack: Readonly<{ value: boolean }>
|
||||
readonly cacheStats: Readonly<{ value: NativeCacheStats }>
|
||||
install(app: App): void
|
||||
push(to: RouteLocationRaw, options?: NativeNavigationOptions): Promise<boolean>
|
||||
replace(to: RouteLocationRaw, options?: NativeNavigationOptions): Promise<boolean>
|
||||
@@ -126,6 +161,8 @@ export interface NativeRouterRuntime {
|
||||
updateInteractive(progress: number, velocity?: number): void
|
||||
finishInteractive(forceCommit?: boolean): Promise<boolean>
|
||||
cancelInteractive(): Promise<void>
|
||||
/** Unmount inactive cached views while retaining route/history descriptors. */
|
||||
trimCache(options?: { includePinned?: boolean; reason?: NativeEvictionReason }): void
|
||||
registerPresentation(definition: NativePresentationDefinition): void
|
||||
presentationFor(name: NativePresentationName): NativePresentationDefinition | undefined
|
||||
dispose(): void
|
||||
|
||||
@@ -13,6 +13,7 @@ declare global {
|
||||
nativeVueHost?: {
|
||||
onBack(callback: () => void): () => void
|
||||
onForward?(callback: () => void): () => void
|
||||
onMemoryPressure?(callback: () => void): () => void
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,9 +26,13 @@ export function createElectronRendererAdapter(): NativePlatformAdapter {
|
||||
if (runtime.canGoBack.value) void runtime.pop()
|
||||
})
|
||||
const removeForward = window.nativeVueHost?.onForward?.(() => runtime.router.forward())
|
||||
const removeMemoryPressure = window.nativeVueHost?.onMemoryPressure?.(() => {
|
||||
runtime.trimCache({ reason: 'memory-pressure' })
|
||||
})
|
||||
return () => {
|
||||
removeBack?.()
|
||||
removeForward?.()
|
||||
removeMemoryPressure?.()
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user