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

Merged
merged 8 commits into from
Nov 29, 2024
Merged
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
11 changes: 11 additions & 0 deletions src/errors/callbackContextAbortError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { CallbackAbortResponse } from '@/services/createCallbackContext'

export class CallbackContextAbortError extends Error {
public response: CallbackAbortResponse

public constructor() {
super('Uncaught CallbackContextAbortError')

this.response = { status: 'ABORT' }
}
}
12 changes: 12 additions & 0 deletions src/errors/callbackContextPushError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { RegisteredRouterPush } from '@/types/register'
import { CallbackPushResponse } from '@/services/createCallbackContext'

export class CallbackContextPushError extends Error {
public response: CallbackPushResponse

public constructor(to: unknown[]) {
super('Uncaught CallbackContextPushError')

this.response = { status: 'PUSH', to: to as Parameters<RegisteredRouterPush> }
}
}
12 changes: 12 additions & 0 deletions src/errors/callbackContextRejectionError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { RegisteredRejectionType } from '@/types/register'
import { CallbackRejectResponse } from '@/services/createCallbackContext'

export class CallbackContextRejectionError extends Error {
public response: CallbackRejectResponse

public constructor(type: RegisteredRejectionType) {
super('Uncaught CallbackContextRejectionError')

this.response = { status: 'REJECT', type }
}
}
1 change: 0 additions & 1 deletion src/errors/navigationAbortError.ts

This file was deleted.

9 changes: 0 additions & 9 deletions src/errors/routerPushError.ts

This file was deleted.

11 changes: 0 additions & 11 deletions src/errors/routerRejectionError.ts

This file was deleted.

75 changes: 75 additions & 0 deletions src/services/createCallbackContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { CallbackContextAbortError } from '@/errors/callbackContextAbortError'
import { CallbackContextPushError } from '@/errors/callbackContextPushError'
import { CallbackContextRejectionError } from '@/errors/callbackContextRejectionError'
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,
}

/**
* A function that can be called to abort a routing operation.
*/
export type CallbackContextAbort = () => void

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

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

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

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

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

const abort: CallbackContextAbort = () => {
throw new CallbackContextAbortError()
}

return { reject, push, replace, abort }
}
2 changes: 1 addition & 1 deletion src/services/createRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export function createRouter<const TRoutes extends Routes, const TOptions extend
},
})

const { runBeforeRouteHooks, runAfterRouteHooks } = createRouteHookRunners<TRoutes>()
const { runBeforeRouteHooks, runAfterRouteHooks } = createRouteHookRunners()
const {
hooks,
onBeforeRouteEnter,
Expand Down
98 changes: 29 additions & 69 deletions src/services/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
import { NavigationAbortError } from '@/errors/navigationAbortError'
import { RouterPushError } from '@/errors/routerPushError'
import { RouterRejectionError } from '@/errors/routerRejectionError'
import { CallbackContextAbortError } from '@/errors/callbackContextAbortError'
import { CallbackContextPushError } from '@/errors/callbackContextPushError'
import { CallbackContextRejectionError } from '@/errors/callbackContextRejectionError'
import { RouteHookStore } from '@/services/createRouteHookStore'
import { getAfterRouteHooksFromRoutes, getBeforeRouteHooksFromRoutes } from '@/services/getRouteHooks'
import { AfterRouteHook, AfterRouteHookResponse, BeforeRouteHook, BeforeRouteHookResponse, RouteHookAbort, RouteHookLifecycle } from '@/types/hooks'
import { RegisteredRouterPush, RegisteredRouterReplace } from '@/types/register'
import { AfterRouteHook, AfterRouteHookResponse, BeforeRouteHook, BeforeRouteHookResponse, RouteHookLifecycle } from '@/types/hooks'
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 +18,20 @@ 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>>

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 }])
}
type RouteHookAfterRunner = (context: AfterContext) => Promise<AfterRouteHookResponse>

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

const abort: RouteHookAbort = () => {
throw new NavigationAbortError()
}
export function createRouteHookRunners(): RouteHookRunners {
const { reject, push, replace, abort } = createCallbackContext()

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,31 +50,23 @@ 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,
}))

await Promise.all(results)
} catch (error) {
if (error instanceof RouterPushError) {
return {
status: 'PUSH',
to: error.to as Parameters<RouterPush<T>>,
}
if (error instanceof CallbackContextPushError) {
return error.response
}

if (error instanceof RouterRejectionError) {
return {
status: 'REJECT',
type: error.type,
}
if (error instanceof CallbackContextRejectionError) {
return error.response
}

if (error instanceof NavigationAbortError) {
return {
status: 'ABORT',
}
if (error instanceof CallbackContextAbortError) {
return error.response
}

throw error
Expand All @@ -111,7 +77,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 All @@ -131,24 +97,18 @@ export function createRouteHookRunners<const T extends Routes>(): RouteHookRunne
const results = allHooks.map((callback) => callback(to, {
from,
reject,
push: push as RegisteredRouterPush,
replace: replace as RegisteredRouterReplace,
push: push,
replace: replace,
}))

await Promise.all(results)
} catch (error) {
if (error instanceof RouterPushError) {
return {
status: 'PUSH',
to: error.to as Parameters<RouterPush<T>>,
}
if (error instanceof CallbackContextPushError) {
return error.response
}

if (error instanceof RouterRejectionError) {
return {
status: 'REJECT',
type: error.type,
}
if (error instanceof CallbackContextRejectionError) {
return error.response
}

throw error
Expand Down
49 changes: 6 additions & 43 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, CallbackContextAbort, 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 All @@ -19,11 +18,6 @@ export type AddBeforeRouteHook = (hook: BeforeRouteHook) => RouteHookRemove
*/
export type AddAfterRouteHook = (hook: AfterRouteHook) => RouteHookRemove

/**
* A function that can be called to abort a routing operation.
*/
export type RouteHookAbort = () => void

/**
* Context provided to route hooks, containing context of previous route and functions for triggering rejections and push/replace to another route.
*/
Expand All @@ -39,7 +33,7 @@ type RouteHookContext = {
* as well as aborting current route change.
*/
type BeforeRouteHookContext = RouteHookContext & {
abort: RouteHookAbort,
abort: CallbackContextAbort,
}

/**
Expand Down Expand Up @@ -88,51 +82,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