Skip to content

Commit

Permalink
perf: improve hooks impl
Browse files Browse the repository at this point in the history
  • Loading branch information
dragon-fish committed May 11, 2024
1 parent f32ecb6 commit 39fed00
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 37 deletions.
61 changes: 37 additions & 24 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,7 @@
*/

export class Fexios {
hooks: Record<FexiosEvents, FexiosHook[]> = {
beforeInit: [],
beforeRequest: [],
afterBodyTransformed: [],
beforeActualFetch: [],
afterResponse: [],
}
protected hooks: FexiosHookStore[] = []
readonly DEFAULT_CONFIGS: FexiosConfigs = {
baseURL: '',
timeout: 60 * 1000,
Expand Down Expand Up @@ -195,17 +189,18 @@ export class Fexios {
return headersObject
}

async emit<C = FexiosContext>(event: FexiosEvents, ctx: C) {
const hooks = this.hooks[event] || []
async emit<C = FexiosContext>(event: FexiosLifecycleEvents, ctx: C) {
const hooks = this.hooks.filter((hook) => hook.event === event)
try {
for (const [index, hook] of hooks.entries()) {
const hookName = `${event}#${hook.name || index}`
let index = 0
for (const hook of hooks) {
const hookName = `${event}#${hook.action.name || index}`

// Set a symbol to check if the hook overrides the original context
const symbol = Symbol('FexiosHookContext')
;(ctx as any).__hook_symbol__ = symbol

const newCtx = await (hook as FexiosHook<C>)(ctx)
const newCtx = await (hook.action.bind(this) as FexiosHook<C>)(ctx)

// Check if the hook overrides the original context
if ((ctx as any).__hook_symbol__ !== symbol) {
Expand Down Expand Up @@ -246,38 +241,45 @@ export class Fexios {

// Clean up
delete (ctx as any).__hook_symbol__

index++
}
} catch (e) {
return Promise.reject(e)
}
return ctx
}
on<C = FexiosContext>(
event: FexiosEvents,
hook: FexiosHook<C>,
event: FexiosLifecycleEvents,
action: FexiosHook<C>,
prepend = false
) {
if (typeof hook !== 'function') {
if (typeof action !== 'function') {
throw new FexiosError(
'INVALID_HOOK_CALLBACK',
`Hook "${hook}" should be a function, but got "${typeof hook}"`
`Hook "${action}" should be a function, but got "${typeof action}"`
)
}
this.hooks[event] ??= []
this.hooks[event][prepend ? 'unshift' : 'push'](hook as any)
this.hooks[prepend ? 'unshift' : 'push']({
event,
action: action as FexiosHook,
})
return this
}

private createInterceptor<T extends FexiosEvents>(
private createInterceptor<T extends FexiosLifecycleEvents>(
event: T
): FexiosInterceptor {
return {
handlers: this.hooks[event],
handlers: () =>
this.hooks
.filter((hook) => hook.event === event)
.map((hook) => hook.action),
use: <C = FexiosContext>(hook: FexiosHook<C>, prepend = false) => {
return this.on(event, hook, prepend)
},
clear: () => {
this.hooks[event] = []
this.hooks = this.hooks.filter((hook) => hook.event !== event)
},
}
}
Expand Down Expand Up @@ -314,7 +316,7 @@ export class Fexios {

extends(configs: Partial<FexiosConfigs>) {
const fexios = new Fexios({ ...this.baseConfigs, ...configs })
fexios.hooks = { ...this.hooks }
fexios.hooks = [...this.hooks]
return fexios
}

Expand Down Expand Up @@ -454,14 +456,25 @@ export interface FexiosResponse<T = any> {
data: T
}
export type FexiosHook<C = unknown> = (context: C) => AwaitAble<C | false>
export type FexiosEvents =
export interface FexiosHookStore {
event: FexiosLifecycleEvents
action: FexiosHook
}
export type FexiosLifecycleEvents =
| 'beforeInit'
| 'beforeRequest'
| 'afterBodyTransformed'
| 'beforeActualFetch'
| 'afterResponse'
export interface FexiosHooksNameMap {
beforeInit: FexiosContext
beforeRequest: FexiosContext
afterBodyTransformed: FexiosContext
beforeActualFetch: FexiosContext
afterResponse: FexiosFinalContext
}
export interface FexiosInterceptor {
handlers: FexiosHook[]
handlers: () => FexiosHook[]
use: <C = FexiosContext>(hook: FexiosHook<C>, prepend?: boolean) => Fexios
clear: () => void
}
Expand Down
30 changes: 29 additions & 1 deletion test/1.core.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { describe, expect, it } from 'vitest'
import fexios, { Fexios } from '../src/index'
import fexios, {
Fexios,
FexiosError,
FexiosResponseError,
isFexiosError,
} from '../src/index'
import { EchoResponse } from './MockData'
import { ECHO_BASE_URL } from './constants'

Expand Down Expand Up @@ -58,6 +63,29 @@ describe('Fexios Core', () => {
})
})

it('GET should not have body', async () => {
let error: FexiosError | undefined
try {
await fexios.get<EchoResponse>(`${ECHO_BASE_URL}/get`, { body: 'test' })
} catch (e) {
error = e
}
expect(error).to.be.instanceOf(FexiosError)
expect(isFexiosError(error)).to.be.true
})

it('Bad status should throw ResponseError', async () => {
let error: FexiosResponseError<string> | undefined
try {
await fexios.get<EchoResponse>(`${ECHO_BASE_URL}/_status/404`)
} catch (e) {
error = e
}
expect(error).to.be.instanceOf(FexiosResponseError)
expect(isFexiosError(error)).to.be.false
expect(error?.response.data).to.equal(404)
})

it('POST with JSON', async () => {
const { data } = await fexios.post<EchoResponse>(`${ECHO_BASE_URL}/post`, {
time,
Expand Down
4 changes: 2 additions & 2 deletions test/2.hooks.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ describe('Fexios Hooks', () => {
fexios.on('afterResponse', (ctx) => {
expect(ctx.data).to.be.an('object')
expect(ctx.data.url).to.equal(`${ECHO_BASE_URL}/anything/3?foo=baz`)
ctx.data.uuid = time
ctx.data._meta.foo = time
return ctx
})
const { data } = await fexios.get<EchoResponse>('/anything/1')
expect(data.uuid).to.equal(time)
expect(data._meta.foo).to.equal(time)
})

it('[HOOKS] ignore unexpected hooks', async () => {
Expand Down
16 changes: 15 additions & 1 deletion test/3.file.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,31 @@ function dataURLtoFile(dataurl: string, filename: string): File {
return new File([u8arr], filename, { type: mime })
}

describe('Fexios Download Binary Files', () => {
it('GET binary file', async () => {
const { data } = await fexios.get<Blob>(
`${ECHO_BASE_URL}/assets/_blank.png`,
{
responseType: 'blob',
}
)
expect(data).to.be.instanceOf(Blob)
expect(data.type).to.equal('image/png')
})
})

describe('Fexios File Uploads', () => {
it('Upload file directly', async () => {
const { data } = await fexios.post<EchoResponse>(
`${ECHO_BASE_URL}/post`,
fileFile
)
console.info('Upload file directly:', data)

const fileInfo = data.binaryFiles?.[0]!
expect(fileInfo).not.to.be.undefined
expect(fileInfo.type).to.equal('image/png')
expect(fileInfo.dataURL).to.equal(fileDataURL)
expect(fileInfo.base64).to.equal(fileBase64)
})

it('Upload file with Form', async () => {
Expand Down
54 changes: 45 additions & 9 deletions test/MockData.d.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,58 @@
export type EchoResponse = {
uuid: string
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'
id: string
method: HTTPMethod
url: string
protocol: string
pathname: string
origin: string
hostname: string
searchParams: Record<string, string>
headers: Record<string, string>
port: string
pathname: string
search: string
searchParams: EchoResponseKeyValRecord
headers: EchoResponseKeyValRecord
body: any
formData?: Record<string, string>
formData: EchoResponseKeyValRecord | null
binaryFiles: EchoResponseFileInfo[]
_meta: EchoResponseMeta
}

export type EchoResponseKeyValRecord = Record<string, string | string[]>

export type EchoResponseFileInfo = {
uuid: string
id: string
name: string
type: string
size: number
dataURL: string
base64: string
sha256: string
}

export interface EchoResponseMeta {
starttime: number
endtime: number
duration: number
bodyType: EchoResponseMetaBodyType
FORM_DATA_FLAG: string
BINARY_FILES_FLAG: string
[key: string]: any
}

export enum EchoResponseMetaBodyType {
NOT_ACCEPTABLE = 'NOT_ACCEPTABLE',
JSON = 'JSON',
TEXT = 'TEXT',
FORM = 'FORM',
BINARY = 'BINARY',
EMPTY = 'EMPTY',
UNKNOWN = 'UNKNOWN',
}

export type HTTPMethod =
| 'GET'
| 'POST'
| 'PUT'
| 'DELETE'
| 'PATCH'
| 'HEAD'
| 'OPTIONS'
| 'CONNECT'
| 'TRACE'

0 comments on commit 39fed00

Please sign in to comment.