Skip to content

Commit

Permalink
feat(data-loaders): add abort signal to navigation
Browse files Browse the repository at this point in the history
  • Loading branch information
posva committed Dec 19, 2023
1 parent 5129e44 commit a175fa7
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 2 deletions.
7 changes: 7 additions & 0 deletions src/data-fetching_new/meta-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
LOADER_ENTRIES_KEY,
LOADER_SET_KEY,
PENDING_LOCATION_KEY,
ABORT_CONTROLLER_KEY,
} from './symbols'

/**
Expand Down Expand Up @@ -55,6 +56,12 @@ declare module 'vue-router' {
* @internal
*/
[LOADER_ENTRIES_KEY]?: _DefineLoaderEntryMap

/**
* The signal that is aborted when the navigation is canceled or an error occurs.
* @internal
*/
[ABORT_CONTROLLER_KEY]?: AbortController
}
}

Expand Down
43 changes: 42 additions & 1 deletion src/data-fetching_new/navigation-guard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ import { setCurrentContext } from './utils'
import { getRouter } from 'vue-router-mock'
import { DataLoaderPlugin } from './navigation-guard'
import { mockedLoader } from '~/tests/utils'
import { LOADER_SET_KEY } from './symbols'
import { ABORT_CONTROLLER_KEY, LOADER_SET_KEY } from './symbols'
import {
useDataOne,
useDataTwo,
} from '~/tests/data-loaders/ComponentWithLoader.vue'
import * as _utils from '~/src/data-fetching_new/utils'
import type { NavigationFailure } from 'vue-router'

vi.mock(
'~/src/data-fetching_new/utils.ts',
Expand Down Expand Up @@ -313,4 +314,44 @@ describe('navigation-guard', () => {
await expect(p).rejects.toThrow('ko')
expect(router.currentRoute.value.path).not.toBe('/fetch')
})

describe('signal', () => {
it('aborts the signal if the navigation throws', async () => {
const router = getRouter()

router.setNextGuardReturn(new Error('canceled'))
let signal!: AbortSignal
router.beforeEach((to) => {
signal = to.meta[ABORT_CONTROLLER_KEY]!.signal
})

await expect(router.push('/#other')).rejects.toThrow('canceled')

expect(router.currentRoute.value.hash).not.toBe('#other')
expect(signal.aborted).toBe(true)
expect(signal.reason).toBeInstanceOf(Error)
expect(signal.reason!.message).toBe('canceled')
})

it('aborts the signal if the navigation is canceled', async () => {
const router = getRouter()

router.setNextGuardReturn(false)
let signal!: AbortSignal
router.beforeEach((to) => {
signal = to.meta[ABORT_CONTROLLER_KEY]!.signal
})

let reason: NavigationFailure | undefined | void
router.afterEach((_to, _from, failure) => {
reason = failure
})

await router.push('/#other')

expect(router.currentRoute.value.hash).not.toBe('#other')
expect(signal.aborted).toBe(true)
expect(signal.reason).toBe(reason)
})
})
})
22 changes: 21 additions & 1 deletion src/data-fetching_new/navigation-guard.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Router } from 'vue-router'
import { effectScope, type App, type EffectScope } from 'vue'
import {
ABORT_CONTROLLER_KEY,
APP_KEY,
LOADER_ENTRIES_KEY,
LOADER_SET_KEY,
Expand Down Expand Up @@ -45,6 +46,8 @@ export function setupLoaderGuard(
to.meta[LOADER_SET_KEY] = new Set()
// reference the loader entries map for convenience
to.meta[LOADER_ENTRIES_KEY] = router[LOADER_ENTRIES_KEY]
// adds an abort controller that can pass a signal to loaders
to.meta[ABORT_CONTROLLER_KEY] = new AbortController()

// Collect all the lazy loaded components to await them in parallel
const lazyLoadingPromises = []
Expand Down Expand Up @@ -148,7 +151,13 @@ export function setupLoaderGuard(

// listen to duplicated navigation failures to reset the pendingTo and pendingLoad
// since they won't trigger the beforeEach or beforeResolve defined above
router.afterEach((_to, _from, failure) => {
const removeAfterEach = router.afterEach((_to, _from, failure) => {
// abort the signal of a failed navigation
// we need to check if it exists because the navigation guard that creates
// the abort controller could not be triggered depending on the failure
if (failure && _to.meta[ABORT_CONTROLLER_KEY]) {
_to.meta[ABORT_CONTROLLER_KEY].abort(failure)
}
if (
isNavigationFailure(failure, 16 /* NavigationFailureType.duplicated */)
) {
Expand All @@ -163,11 +172,22 @@ export function setupLoaderGuard(
}
})

// abort the signal on thrown errors
const removeOnError = router.onError((error, to) => {
// same as with afterEach, we check if it exists because the navigation guard
// that creates the abort controller could not be triggered depending on the error
if (to.meta[ABORT_CONTROLLER_KEY]) {
to.meta[ABORT_CONTROLLER_KEY].abort(error)
}
})

return () => {
// @ts-expect-error: must be there in practice
delete router[LOADER_ENTRIES_KEY]
removeLoaderGuard()
removeDataLoaderGuard()
removeAfterEach()
removeOnError()
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/data-fetching_new/symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,9 @@ export const STAGED_NO_VALUE = Symbol()
* @internal
*/
export const APP_KEY = Symbol()

/**
* Gives access to an AbortController that aborts when the navigation is canceled.
* @internal
*/
export const ABORT_CONTROLLER_KEY = Symbol()

0 comments on commit a175fa7

Please sign in to comment.