Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor callback context and handling #328

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions src/services/createCallbackContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { RouterPushError } from '@/errors/routerPushError'
import { RouterRejectionError } from '@/errors/routerRejectionError'
import { RegisteredRejectionType, RegisteredRouterPush, RegisteredRouterReject, RegisteredRouterReplace } from '@/types/register'
import { RouterPushOptions } from '@/types/routerPush'
import { isUrl } from '@/types/url'

/**
* Defines the structure of a successful callback response.
*/
export type CallbackSuccessResponse = {
status: 'SUCCESS',
}

/**
* Defines the structure of an aborted callback response.
*/
export type CallbackAbortResponse = {
status: 'ABORT',
}

/**
* Defines the structure of a callback response that results in a push to a new route.
*/
export type CallbackPushResponse = {
status: 'PUSH',
to: Parameters<RegisteredRouterPush>,
}

/**
* Defines the structure of a callback response that results in the rejection of a route transition.
*/
export type CallbackRejectResponse = {
status: 'REJECT',
type: RegisteredRejectionType,
}

export type CallbackResponse = CallbackSuccessResponse | CallbackPushResponse | CallbackRejectResponse | CallbackAbortResponse

export type CallbackContext = {
reject: RegisteredRouterReject,
push: RegisteredRouterPush,
replace: RegisteredRouterReplace,
}

export function createCallbackContext(): CallbackContext {
const reject: RegisteredRouterReject = (type) => {
throw new RouterRejectionError(type)
}

const push: RegisteredRouterPush = (...parameters: any[]) => {
throw new RouterPushError(parameters)
}

const replace: RegisteredRouterPush = (source: any, paramsOrOptions?: any, maybeOptions?: any) => {
if (isUrl(source)) {
const options: RouterPushOptions = paramsOrOptions ?? {}
throw new RouterPushError([source, { ...options, replace: true }])
}

const params = paramsOrOptions
const options: RouterPushOptions = maybeOptions ?? {}
throw new RouterPushError([source, params, { ...options, replace: true }])
}

return { reject, push, replace }
}
54 changes: 24 additions & 30 deletions src/services/createRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { RoutesName } from '@/types/routesMap'
import { Url, isUrl } from '@/types/url'
import { checkDuplicateNames } from '@/utilities/checkDuplicateNames'
import { isNestedArray } from '@/utilities/guards'
import { CallbackResponse } from './createCallbackContext'

type RouterUpdateOptions = {
replace?: boolean,
Expand Down Expand Up @@ -73,7 +74,7 @@ export function createRouter<const TRoutes extends Routes, const TOptions extend
},
})

const { runBeforeRouteHooks, runAfterRouteHooks } = createRouteHookRunners<TRoutes>()
const { runBeforeRouteHooks, runAfterRouteHooks } = createRouteHookRunners()
const {
hooks,
onBeforeRouteEnter,
Expand All @@ -98,54 +99,28 @@ export function createRouter<const TRoutes extends Routes, const TOptions extend
const beforeResponse = await runBeforeRouteHooks({ to, from, hooks })

switch (beforeResponse.status) {
// On abort do nothing
case 'ABORT':
return

// On push update the history, and push new route, and return
case 'PUSH':
history.update(url, options)
await push(...beforeResponse.to)
return

// On reject update the history, the route, and set the rejection type
case 'REJECT':
history.update(url, options)
setRejection(beforeResponse.type)
break

// On success update history, set the route, and clear the rejection
case 'SUCCESS':
history.update(url, options)
setRejection(null)
break

default:
throw new Error(`Switch is not exhaustive for before hook response status: ${JSON.stringify(beforeResponse satisfies never)}`)
}

handleCallbackResponse(beforeResponse)

propStore.setProps(to)

updateRoute(to)

const afterResponse = await runAfterRouteHooks({ to, from, hooks })

switch (afterResponse.status) {
case 'PUSH':
await push(...afterResponse.to)
break

case 'REJECT':
setRejection(afterResponse.type)
break

case 'SUCCESS':
break

default:
const exhaustive: never = afterResponse
throw new Error(`Switch is not exhaustive for after hook response status: ${JSON.stringify(exhaustive)}`)
}
handleCallbackResponse(afterResponse)

history.startListening()
}
Expand Down Expand Up @@ -232,6 +207,25 @@ export function createRouter<const TRoutes extends Routes, const TOptions extend
return routes.find((route) => route.name === name)
}

function handleCallbackResponse(response: CallbackResponse): void {
switch (response.status) {
case 'ABORT':
case 'SUCCESS':
return

case 'PUSH':
push(...response.to)
return

case 'REJECT':
setRejection(response.type)
break

default:
throw new Error(`Switch is not exhaustive callback response status: ${JSON.stringify(response satisfies never)}`)
}
}

function install(app: App): void {
app.component('RouterView', RouterView)
app.component('RouterLink', RouterLink)
Expand Down
51 changes: 15 additions & 36 deletions src/services/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,11 @@ import { getAfterRouteHooksFromRoutes, getBeforeRouteHooksFromRoutes } from '@/s
import { AfterRouteHook, AfterRouteHookResponse, BeforeRouteHook, BeforeRouteHookResponse, RouteHookAbort, RouteHookLifecycle } from '@/types/hooks'
import { RegisteredRouterPush, RegisteredRouterReplace } from '@/types/register'
import { ResolvedRoute } from '@/types/resolved'
import { Routes } from '@/types/route'
import { RouterReject } from '@/types/router'
import { RouterPush, RouterPushOptions } from '@/types/routerPush'
import { RouterReplace } from '@/types/routerReplace'
import { isUrl } from '@/types/url'

type RouteHookRunners<T extends Routes> = {
runBeforeRouteHooks: RouteHookBeforeRunner<T>,
runAfterRouteHooks: RouteHookAfterRunner<T>,
import { createCallbackContext } from './createCallbackContext'

type RouteHookRunners = {
runBeforeRouteHooks: RouteHookBeforeRunner,
runAfterRouteHooks: RouteHookAfterRunner,
}

type BeforeContext = {
Expand All @@ -23,41 +19,24 @@ type BeforeContext = {
hooks: RouteHookStore,
}

type RouteHookBeforeRunner<T extends Routes> = (context: BeforeContext) => Promise<BeforeRouteHookResponse<T>>
type RouteHookBeforeRunner = (context: BeforeContext) => Promise<BeforeRouteHookResponse>

type AfterContext = {
to: ResolvedRoute,
from: ResolvedRoute,
hooks: RouteHookStore,
}

type RouteHookAfterRunner<T extends Routes> = (context: AfterContext) => Promise<AfterRouteHookResponse<T>>
type RouteHookAfterRunner = (context: AfterContext) => Promise<AfterRouteHookResponse>

export function createRouteHookRunners<const T extends Routes>(): RouteHookRunners<T> {
const reject: RouterReject = (type) => {
throw new RouterRejectionError(type)
}

const push: RouterPush<T> = (...parameters: any[]) => {
throw new RouterPushError(parameters)
}

const replace: RouterReplace<T> = (source: any, paramsOrOptions?: any, maybeOptions?: any) => {
if (isUrl(source)) {
const options: RouterPushOptions = paramsOrOptions ?? {}
throw new RouterPushError([source, { ...options, replace: true }])
}

const params = paramsOrOptions
const options: RouterPushOptions = maybeOptions ?? {}
throw new RouterPushError([source, params, { ...options, replace: true }])
}
export function createRouteHookRunners(): RouteHookRunners {
const { reject, push, replace } = createCallbackContext()

const abort: RouteHookAbort = () => {
throw new NavigationAbortError()
}

async function runBeforeRouteHooks({ to, from, hooks }: BeforeContext): Promise<BeforeRouteHookResponse<T>> {
async function runBeforeRouteHooks({ to, from, hooks }: BeforeContext): Promise<BeforeRouteHookResponse> {
const { global, component } = hooks
const route = getBeforeRouteHooksFromRoutes(to, from)

Expand All @@ -76,8 +55,8 @@ export function createRouteHookRunners<const T extends Routes>(): RouteHookRunne
const results = allHooks.map((callback) => callback(to, {
from,
reject,
push: push as RegisteredRouterPush,
replace: replace as RegisteredRouterPush,
push,
replace,
abort,
}))

Expand All @@ -86,7 +65,7 @@ export function createRouteHookRunners<const T extends Routes>(): RouteHookRunne
if (error instanceof RouterPushError) {
return {
status: 'PUSH',
to: error.to as Parameters<RouterPush<T>>,
to: error.to as Parameters<RegisteredRouterPush>,
}
}

Expand All @@ -111,7 +90,7 @@ export function createRouteHookRunners<const T extends Routes>(): RouteHookRunne
}
}

async function runAfterRouteHooks({ to, from, hooks }: AfterContext): Promise<AfterRouteHookResponse<T>> {
async function runAfterRouteHooks({ to, from, hooks }: AfterContext): Promise<AfterRouteHookResponse> {
const { global, component } = hooks
const route = getAfterRouteHooksFromRoutes(to, from)

Expand Down Expand Up @@ -140,7 +119,7 @@ export function createRouteHookRunners<const T extends Routes>(): RouteHookRunne
if (error instanceof RouterPushError) {
return {
status: 'PUSH',
to: error.to as Parameters<RouterPush<T>>,
to: error.to as Parameters<RegisteredRouterPush>,
}
}

Expand Down
42 changes: 5 additions & 37 deletions src/types/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { RegisteredRejectionType, RegisteredRouterPush, RegisteredRouterReplace } from '@/types/register'
import { CallbackAbortResponse, CallbackPushResponse, CallbackRejectResponse, CallbackSuccessResponse } from '@/services/createCallbackContext'
import { RegisteredRouterPush, RegisteredRouterReplace } from '@/types/register'
import { ResolvedRoute } from '@/types/resolved'
import { Routes } from '@/types/route'
import { RouterReject } from '@/types/router'
import { RouterPush } from '@/types/routerPush'
import { MaybePromise } from '@/types/utilities'

/**
Expand Down Expand Up @@ -88,51 +87,20 @@ export type AfterRouteHookLifecycle = 'onAfterRouteEnter' | 'onAfterRouteUpdate'
*/
export type RouteHookLifecycle = BeforeRouteHookLifecycle | AfterRouteHookLifecycle

/**
* Defines the structure of a successful route hook response.
*/
type RouteHookSuccessResponse = {
status: 'SUCCESS',
}

/**
* Defines the structure of an aborted route hook response.
*/
type RouteHookAbortResponse = {
status: 'ABORT',
}

/**
* Defines the structure of a route hook response that results in a push to a new route.
* @template T - The type of the routes configuration.
*/
type RouteHookPushResponse<T extends Routes> = {
status: 'PUSH',
to: Parameters<RouterPush<T>>,
}

/**
* Defines the structure of a route hook response that results in the rejection of a route transition.
*/
type RouteHookRejectResponse = {
status: 'REJECT',
type: RegisteredRejectionType,
}

/**
* Type for responses from a before route hook, which may indicate different outcomes such as success, push, reject, or abort.
* @template TRoutes - The type of the routes configuration.
*/
export type BeforeRouteHookResponse<TRoutes extends Routes> = RouteHookSuccessResponse | RouteHookPushResponse<TRoutes> | RouteHookRejectResponse | RouteHookAbortResponse
export type BeforeRouteHookResponse = CallbackSuccessResponse | CallbackPushResponse | CallbackRejectResponse | CallbackAbortResponse

/**
* Type for responses from an after route hook, which may indicate different outcomes such as success, push, or reject.
* @template TRoutes - The type of the routes configuration.
*/
export type AfterRouteHookResponse<TRoutes extends Routes> = RouteHookSuccessResponse | RouteHookPushResponse<TRoutes> | RouteHookRejectResponse
export type AfterRouteHookResponse = CallbackSuccessResponse | CallbackPushResponse | CallbackRejectResponse

/**
* Union type for all possible route hook responses, covering both before and after scenarios.
* @template TRoutes - The type of the routes configuration.
*/
export type RouteHookResponse<TRoutes extends Routes> = BeforeRouteHookResponse<TRoutes> | AfterRouteHookResponse<TRoutes>
export type RouteHookResponse = BeforeRouteHookResponse | AfterRouteHookResponse
5 changes: 5 additions & 0 deletions src/types/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,8 @@ export type RegisteredRouterPush = RouterPush<RegisteredRoutes>
* Represents the type for router `replace`, with types for routes registered within {@link Register}
*/
export type RegisteredRouterReplace = RouterReplace<RegisteredRoutes>

/**
* Type for Router Reject method. Triggers rejections registered within {@link Register}
*/
export type RegisteredRouterReject = (type: RegisteredRejectionType) => void