@orpc/server 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-ACLC6USM.js +262 -0
- package/dist/chunk-ACLC6USM.js.map +1 -0
- package/dist/fetch.js +648 -0
- package/dist/fetch.js.map +1 -0
- package/dist/index.js +403 -0
- package/dist/index.js.map +1 -0
- package/dist/src/adapters/fetch.d.ts +36 -0
- package/dist/src/adapters/fetch.d.ts.map +1 -0
- package/dist/src/builder.d.ts +49 -0
- package/dist/src/builder.d.ts.map +1 -0
- package/dist/src/index.d.ts +15 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/middleware.d.ts +16 -0
- package/dist/src/middleware.d.ts.map +1 -0
- package/dist/src/procedure-builder.d.ts +31 -0
- package/dist/src/procedure-builder.d.ts.map +1 -0
- package/dist/src/procedure-caller.d.ts +33 -0
- package/dist/src/procedure-caller.d.ts.map +1 -0
- package/dist/src/procedure-implementer.d.ts +19 -0
- package/dist/src/procedure-implementer.d.ts.map +1 -0
- package/dist/src/procedure.d.ts +29 -0
- package/dist/src/procedure.d.ts.map +1 -0
- package/dist/src/router-builder.d.ts +22 -0
- package/dist/src/router-builder.d.ts.map +1 -0
- package/dist/src/router-caller.d.ts +36 -0
- package/dist/src/router-caller.d.ts.map +1 -0
- package/dist/src/router-implementer.d.ts +20 -0
- package/dist/src/router-implementer.d.ts.map +1 -0
- package/dist/src/router.d.ts +14 -0
- package/dist/src/router.d.ts.map +1 -0
- package/dist/src/types.d.ts +18 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/utils.d.ts +4 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +57 -0
- package/src/adapters/fetch.test.ts +399 -0
- package/src/adapters/fetch.ts +228 -0
- package/src/builder.test.ts +362 -0
- package/src/builder.ts +236 -0
- package/src/index.ts +16 -0
- package/src/middleware.test.ts +279 -0
- package/src/middleware.ts +119 -0
- package/src/procedure-builder.test.ts +219 -0
- package/src/procedure-builder.ts +158 -0
- package/src/procedure-caller.test.ts +210 -0
- package/src/procedure-caller.ts +165 -0
- package/src/procedure-implementer.test.ts +215 -0
- package/src/procedure-implementer.ts +103 -0
- package/src/procedure.test.ts +312 -0
- package/src/procedure.ts +251 -0
- package/src/router-builder.test.ts +106 -0
- package/src/router-builder.ts +120 -0
- package/src/router-caller.test.ts +173 -0
- package/src/router-caller.ts +95 -0
- package/src/router-implementer.test.ts +116 -0
- package/src/router-implementer.ts +110 -0
- package/src/router.test.ts +142 -0
- package/src/router.ts +69 -0
- package/src/types.test.ts +18 -0
- package/src/types.ts +28 -0
- package/src/utils.test.ts +243 -0
- package/src/utils.ts +95 -0
@@ -0,0 +1,142 @@
|
|
1
|
+
import { oc } from '@orpc/contract'
|
2
|
+
import { z } from 'zod'
|
3
|
+
import { os, type RouterWithContract, toContractRouter } from '.'
|
4
|
+
|
5
|
+
it('require procedure match context', () => {
|
6
|
+
const osw = os.context<{ auth: boolean; userId: string }>()
|
7
|
+
|
8
|
+
osw.router({
|
9
|
+
ping: osw.context<{ auth: boolean }>().handler(() => {
|
10
|
+
return { pong: 'ping' }
|
11
|
+
}),
|
12
|
+
|
13
|
+
// @ts-expect-error userId is not match
|
14
|
+
ping2: osw.context<{ userId: number }>().handler(() => {
|
15
|
+
return { name: 'dinwwwh' }
|
16
|
+
}),
|
17
|
+
|
18
|
+
nested: {
|
19
|
+
ping: osw.context<{ auth: boolean }>().handler(() => {
|
20
|
+
return { pong: 'ping' }
|
21
|
+
}),
|
22
|
+
|
23
|
+
// @ts-expect-error userId is not match
|
24
|
+
ping2: osw.context<{ userId: number }>().handler(() => {
|
25
|
+
return { name: 'dinwwwh' }
|
26
|
+
}),
|
27
|
+
},
|
28
|
+
})
|
29
|
+
})
|
30
|
+
|
31
|
+
it('require match contract', () => {
|
32
|
+
const pingContract = oc.route({ method: 'GET', path: '/ping' })
|
33
|
+
const pongContract = oc.input(z.string()).output(z.string())
|
34
|
+
const ping = os.contract(pingContract).handler(() => {
|
35
|
+
return 'ping'
|
36
|
+
})
|
37
|
+
const pong = os.contract(pongContract).handler(() => {
|
38
|
+
return 'pong'
|
39
|
+
})
|
40
|
+
|
41
|
+
const contract = oc.router({
|
42
|
+
ping: pingContract,
|
43
|
+
pong: pongContract,
|
44
|
+
|
45
|
+
nested: oc.router({
|
46
|
+
ping: pingContract,
|
47
|
+
pong: pongContract,
|
48
|
+
}),
|
49
|
+
})
|
50
|
+
|
51
|
+
const _1: RouterWithContract<undefined, typeof contract> = {
|
52
|
+
ping,
|
53
|
+
pong,
|
54
|
+
|
55
|
+
nested: {
|
56
|
+
ping,
|
57
|
+
pong,
|
58
|
+
},
|
59
|
+
}
|
60
|
+
|
61
|
+
const _2: RouterWithContract<undefined, typeof contract> = {
|
62
|
+
ping,
|
63
|
+
pong,
|
64
|
+
|
65
|
+
nested: os.contract(contract.nested).router({
|
66
|
+
ping,
|
67
|
+
pong,
|
68
|
+
}),
|
69
|
+
}
|
70
|
+
|
71
|
+
const _3: RouterWithContract<undefined, typeof contract> = {
|
72
|
+
ping,
|
73
|
+
pong,
|
74
|
+
|
75
|
+
// @ts-expect-error missing nested.ping
|
76
|
+
nested: {
|
77
|
+
pong,
|
78
|
+
},
|
79
|
+
}
|
80
|
+
|
81
|
+
const _4: RouterWithContract<undefined, typeof contract> = {
|
82
|
+
ping,
|
83
|
+
pong,
|
84
|
+
|
85
|
+
nested: {
|
86
|
+
ping,
|
87
|
+
// @ts-expect-error nested.pong is mismatch
|
88
|
+
pong: os.handler(() => 'ping'),
|
89
|
+
},
|
90
|
+
}
|
91
|
+
|
92
|
+
// @ts-expect-error missing pong
|
93
|
+
const _5: RouterWithContract<undefined, typeof contract> = {
|
94
|
+
ping,
|
95
|
+
|
96
|
+
nested: {
|
97
|
+
ping,
|
98
|
+
pong,
|
99
|
+
},
|
100
|
+
}
|
101
|
+
})
|
102
|
+
|
103
|
+
it('toContractRouter', () => {
|
104
|
+
const p1 = oc.input(z.string()).output(z.string())
|
105
|
+
const p2 = oc.output(z.string())
|
106
|
+
const p3 = oc.route({ method: 'GET', path: '/test' })
|
107
|
+
|
108
|
+
const contract = oc.router({
|
109
|
+
p1: p1,
|
110
|
+
|
111
|
+
nested: oc.router({
|
112
|
+
p2: p2,
|
113
|
+
}),
|
114
|
+
|
115
|
+
nested2: {
|
116
|
+
p3: p3,
|
117
|
+
},
|
118
|
+
})
|
119
|
+
|
120
|
+
const osw = os.contract(contract)
|
121
|
+
|
122
|
+
const router = osw.router({
|
123
|
+
p1: osw.p1.handler(() => {
|
124
|
+
return 'dinwwwh'
|
125
|
+
}),
|
126
|
+
|
127
|
+
nested: osw.nested.router({
|
128
|
+
p2: osw.nested.p2.handler(() => {
|
129
|
+
return 'dinwwwh'
|
130
|
+
}),
|
131
|
+
}),
|
132
|
+
|
133
|
+
nested2: {
|
134
|
+
p3: osw.nested2.p3.handler(() => {
|
135
|
+
return 'dinwwwh'
|
136
|
+
}),
|
137
|
+
},
|
138
|
+
})
|
139
|
+
|
140
|
+
expect(toContractRouter(router)).toEqual(contract)
|
141
|
+
expect(toContractRouter(contract)).toEqual(contract)
|
142
|
+
})
|
package/src/router.ts
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
import {
|
2
|
+
type ContractProcedure,
|
3
|
+
type ContractRouter,
|
4
|
+
isContractProcedure,
|
5
|
+
} from '@orpc/contract'
|
6
|
+
import {
|
7
|
+
type DecoratedProcedure,
|
8
|
+
type Procedure,
|
9
|
+
isProcedure,
|
10
|
+
} from './procedure'
|
11
|
+
import type { Context } from './types'
|
12
|
+
|
13
|
+
export interface Router<TContext extends Context> {
|
14
|
+
[k: string]: Procedure<TContext, any, any, any, any> | Router<TContext>
|
15
|
+
}
|
16
|
+
|
17
|
+
export type HandledRouter<TRouter extends Router<any>> = {
|
18
|
+
[K in keyof TRouter]: TRouter[K] extends Procedure<
|
19
|
+
infer UContext,
|
20
|
+
infer UExtraContext,
|
21
|
+
infer UInputSchema,
|
22
|
+
infer UOutputSchema,
|
23
|
+
infer UHandlerOutput
|
24
|
+
>
|
25
|
+
? DecoratedProcedure<
|
26
|
+
UContext,
|
27
|
+
UExtraContext,
|
28
|
+
UInputSchema,
|
29
|
+
UOutputSchema,
|
30
|
+
UHandlerOutput
|
31
|
+
>
|
32
|
+
: TRouter[K] extends Router<any>
|
33
|
+
? HandledRouter<TRouter[K]>
|
34
|
+
: never
|
35
|
+
}
|
36
|
+
|
37
|
+
export type RouterWithContract<
|
38
|
+
TContext extends Context,
|
39
|
+
TContract extends ContractRouter,
|
40
|
+
> = {
|
41
|
+
[K in keyof TContract]: TContract[K] extends ContractProcedure<
|
42
|
+
infer UInputSchema,
|
43
|
+
infer UOutputSchema
|
44
|
+
>
|
45
|
+
? Procedure<TContext, any, UInputSchema, UOutputSchema, any>
|
46
|
+
: TContract[K] extends ContractRouter
|
47
|
+
? RouterWithContract<TContext, TContract[K]>
|
48
|
+
: never
|
49
|
+
}
|
50
|
+
|
51
|
+
export function toContractRouter(
|
52
|
+
router: ContractRouter | Router<any>,
|
53
|
+
): ContractRouter {
|
54
|
+
const contract: ContractRouter = {}
|
55
|
+
|
56
|
+
for (const key in router) {
|
57
|
+
const item = router[key]
|
58
|
+
|
59
|
+
if (isContractProcedure(item)) {
|
60
|
+
contract[key] = item
|
61
|
+
} else if (isProcedure(item)) {
|
62
|
+
contract[key] = item.zz$p.contract
|
63
|
+
} else {
|
64
|
+
contract[key] = toContractRouter(item as any)
|
65
|
+
}
|
66
|
+
}
|
67
|
+
|
68
|
+
return contract
|
69
|
+
}
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import type { MergeContext } from './types'
|
2
|
+
|
3
|
+
test('MergeContext', () => {
|
4
|
+
expectTypeOf<MergeContext<undefined, undefined>>().toEqualTypeOf<undefined>()
|
5
|
+
expectTypeOf<MergeContext<undefined, { foo: string }>>().toEqualTypeOf<{
|
6
|
+
foo: string
|
7
|
+
}>()
|
8
|
+
expectTypeOf<MergeContext<{ foo: string }, undefined>>().toEqualTypeOf<{
|
9
|
+
foo: string
|
10
|
+
}>()
|
11
|
+
expectTypeOf<MergeContext<{ foo: string }, { foo: string }>>().toEqualTypeOf<{
|
12
|
+
foo: string
|
13
|
+
}>()
|
14
|
+
expectTypeOf<MergeContext<{ foo: string }, { bar: string }>>().toMatchTypeOf<{
|
15
|
+
foo: string
|
16
|
+
bar: string
|
17
|
+
}>()
|
18
|
+
})
|
package/src/types.ts
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
import type { WELL_DEFINED_PROCEDURE } from './procedure'
|
2
|
+
|
3
|
+
export type Context = Record<string, unknown> | undefined
|
4
|
+
|
5
|
+
export type MergeContext<
|
6
|
+
TA extends Context,
|
7
|
+
TB extends Context,
|
8
|
+
> = TA extends undefined ? TB : TB extends undefined ? TA : TA & TB
|
9
|
+
|
10
|
+
export interface Meta<T> extends Hooks<T> {
|
11
|
+
path: string[]
|
12
|
+
internal: boolean
|
13
|
+
procedure: WELL_DEFINED_PROCEDURE
|
14
|
+
}
|
15
|
+
|
16
|
+
export type Promisable<T> = T | Promise<T>
|
17
|
+
|
18
|
+
export interface UnsubscribeFn {
|
19
|
+
(): void
|
20
|
+
}
|
21
|
+
|
22
|
+
export interface Hooks<T> {
|
23
|
+
onSuccess: (fn: (output: T) => Promisable<void>) => UnsubscribeFn
|
24
|
+
onError: (fn: (error: unknown) => Promisable<void>) => UnsubscribeFn
|
25
|
+
onFinish: (
|
26
|
+
fn: (output: T | undefined, error: unknown | undefined) => Promisable<void>,
|
27
|
+
) => UnsubscribeFn
|
28
|
+
}
|
@@ -0,0 +1,243 @@
|
|
1
|
+
import { hook, mergeContext } from './utils'
|
2
|
+
|
3
|
+
test('mergeContext', () => {
|
4
|
+
expect(mergeContext(undefined, undefined)).toBe(undefined)
|
5
|
+
expect(mergeContext(undefined, { foo: 'bar' })).toEqual({ foo: 'bar' })
|
6
|
+
expect(mergeContext({ foo: 'bar' }, undefined)).toEqual({ foo: 'bar' })
|
7
|
+
expect(mergeContext({ foo: 'bar' }, { foo: 'bar' })).toEqual({ foo: 'bar' })
|
8
|
+
expect(mergeContext({ foo: 'bar' }, { bar: 'bar' })).toEqual({
|
9
|
+
foo: 'bar',
|
10
|
+
bar: 'bar',
|
11
|
+
})
|
12
|
+
expect(mergeContext({ foo: 'bar' }, { bar: 'bar', foo: 'bar1' })).toEqual({
|
13
|
+
foo: 'bar1',
|
14
|
+
bar: 'bar',
|
15
|
+
})
|
16
|
+
})
|
17
|
+
|
18
|
+
describe('hook', async () => {
|
19
|
+
it('on success', async () => {
|
20
|
+
const onSuccess = vi.fn()
|
21
|
+
const onError = vi.fn()
|
22
|
+
const onFinish = vi.fn()
|
23
|
+
|
24
|
+
await hook(async (hooks) => {
|
25
|
+
hooks.onSuccess(onSuccess)
|
26
|
+
hooks.onError(onError)
|
27
|
+
hooks.onFinish(onFinish)
|
28
|
+
|
29
|
+
return 'foo'
|
30
|
+
})
|
31
|
+
|
32
|
+
expect(onSuccess).toHaveBeenCalledWith('foo')
|
33
|
+
expect(onError).not.toHaveBeenCalled()
|
34
|
+
expect(onFinish).toHaveBeenCalledWith('foo', undefined)
|
35
|
+
})
|
36
|
+
|
37
|
+
it('on failed', async () => {
|
38
|
+
const onSuccess = vi.fn()
|
39
|
+
const onError = vi.fn()
|
40
|
+
const onFinish = vi.fn()
|
41
|
+
const error = new Error('foo')
|
42
|
+
|
43
|
+
try {
|
44
|
+
await hook(async (hooks) => {
|
45
|
+
hooks.onSuccess(onSuccess)
|
46
|
+
hooks.onError(onError)
|
47
|
+
hooks.onFinish(onFinish)
|
48
|
+
|
49
|
+
throw error
|
50
|
+
})
|
51
|
+
} catch (e) {
|
52
|
+
expect(e).toBe(error)
|
53
|
+
}
|
54
|
+
|
55
|
+
expect(onSuccess).not.toHaveBeenCalled()
|
56
|
+
expect(onError).toHaveBeenCalledWith(error)
|
57
|
+
expect(onFinish).toHaveBeenCalledWith(undefined, error)
|
58
|
+
})
|
59
|
+
|
60
|
+
it('throw the last error', async () => {
|
61
|
+
const error = new Error('foo')
|
62
|
+
const error1 = new Error('1')
|
63
|
+
const error2 = new Error('2')
|
64
|
+
|
65
|
+
try {
|
66
|
+
await hook(async (hooks) => {
|
67
|
+
hooks.onSuccess(() => {
|
68
|
+
throw error
|
69
|
+
})
|
70
|
+
})
|
71
|
+
} catch (e) {
|
72
|
+
expect(e).toBe(error)
|
73
|
+
}
|
74
|
+
|
75
|
+
try {
|
76
|
+
await hook(async (hooks) => {
|
77
|
+
hooks.onError(() => {
|
78
|
+
throw error1
|
79
|
+
})
|
80
|
+
})
|
81
|
+
} catch (e) {
|
82
|
+
expect(e).toBe(error1)
|
83
|
+
}
|
84
|
+
|
85
|
+
try {
|
86
|
+
await hook(async (hooks) => {
|
87
|
+
hooks.onFinish(() => {
|
88
|
+
throw error2
|
89
|
+
})
|
90
|
+
})
|
91
|
+
} catch (e) {
|
92
|
+
expect(e).toBe(error2)
|
93
|
+
}
|
94
|
+
|
95
|
+
try {
|
96
|
+
await hook(async (hooks) => {
|
97
|
+
hooks.onError((e) => {
|
98
|
+
expect(e).toBe(error)
|
99
|
+
throw error1
|
100
|
+
})
|
101
|
+
hooks.onFinish((_, error) => {
|
102
|
+
expect(error).toBe(error1)
|
103
|
+
throw error2
|
104
|
+
})
|
105
|
+
|
106
|
+
throw error
|
107
|
+
})
|
108
|
+
} catch (e) {
|
109
|
+
expect(e).toBe(error2)
|
110
|
+
}
|
111
|
+
})
|
112
|
+
|
113
|
+
it('Fist In Last Out', async () => {
|
114
|
+
const ref = { value: 0 }
|
115
|
+
const hooked = hook(async (hooks) => {
|
116
|
+
hooks.onSuccess(() => {
|
117
|
+
expect(ref).toEqual({ value: 2 })
|
118
|
+
ref.value++
|
119
|
+
})
|
120
|
+
hooks.onSuccess(() => {
|
121
|
+
expect(ref).toEqual({ value: 1 })
|
122
|
+
ref.value++
|
123
|
+
})
|
124
|
+
hooks.onSuccess(() => {
|
125
|
+
expect(ref).toEqual({ value: 0 })
|
126
|
+
ref.value++
|
127
|
+
})
|
128
|
+
|
129
|
+
hooks.onFinish(() => {
|
130
|
+
expect(ref).toEqual({ value: 5 })
|
131
|
+
ref.value++
|
132
|
+
})
|
133
|
+
hooks.onFinish(() => {
|
134
|
+
expect(ref).toEqual({ value: 4 })
|
135
|
+
ref.value++
|
136
|
+
})
|
137
|
+
hooks.onFinish(() => {
|
138
|
+
expect(ref).toEqual({ value: 3 })
|
139
|
+
ref.value++
|
140
|
+
})
|
141
|
+
})
|
142
|
+
|
143
|
+
await expect(hooked).resolves.toEqual(undefined)
|
144
|
+
expect(ref).toEqual({ value: 6 })
|
145
|
+
})
|
146
|
+
|
147
|
+
it('Fist In Last Out - onError', async () => {
|
148
|
+
const ref = { value: 0 }
|
149
|
+
const hooked = hook(async (hooks) => {
|
150
|
+
hooks.onError(() => {
|
151
|
+
expect(ref).toEqual({ value: 2 })
|
152
|
+
ref.value++
|
153
|
+
})
|
154
|
+
hooks.onError(() => {
|
155
|
+
expect(ref).toEqual({ value: 1 })
|
156
|
+
ref.value++
|
157
|
+
})
|
158
|
+
hooks.onError(() => {
|
159
|
+
expect(ref).toEqual({ value: 0 })
|
160
|
+
ref.value++
|
161
|
+
})
|
162
|
+
|
163
|
+
hooks.onFinish(() => {
|
164
|
+
expect(ref).toEqual({ value: 5 })
|
165
|
+
ref.value++
|
166
|
+
})
|
167
|
+
hooks.onFinish(() => {
|
168
|
+
expect(ref).toEqual({ value: 4 })
|
169
|
+
ref.value++
|
170
|
+
})
|
171
|
+
hooks.onFinish(() => {
|
172
|
+
expect(ref).toEqual({ value: 3 })
|
173
|
+
ref.value++
|
174
|
+
})
|
175
|
+
|
176
|
+
throw new Error('foo')
|
177
|
+
})
|
178
|
+
|
179
|
+
await expect(hooked).rejects.toThrow('foo')
|
180
|
+
expect(ref).toEqual({ value: 6 })
|
181
|
+
})
|
182
|
+
|
183
|
+
it('ensure run every onSuccess and onFinish even on throw many times', async () => {
|
184
|
+
const error = new Error('foo')
|
185
|
+
const error1 = new Error('1')
|
186
|
+
const error2 = new Error('2')
|
187
|
+
|
188
|
+
const onError = vi.fn(() => {
|
189
|
+
throw error
|
190
|
+
})
|
191
|
+
const onFinish = vi.fn(() => {
|
192
|
+
throw error1
|
193
|
+
})
|
194
|
+
|
195
|
+
try {
|
196
|
+
await hook(async (hooks) => {
|
197
|
+
hooks.onError(onError)
|
198
|
+
hooks.onError(onError)
|
199
|
+
hooks.onError(onError)
|
200
|
+
hooks.onFinish(onFinish)
|
201
|
+
hooks.onFinish(onFinish)
|
202
|
+
hooks.onFinish(onFinish)
|
203
|
+
|
204
|
+
throw error2
|
205
|
+
})
|
206
|
+
} catch (e) {
|
207
|
+
expect(e).toBe(error1)
|
208
|
+
}
|
209
|
+
|
210
|
+
expect(onError).toHaveBeenCalledTimes(3)
|
211
|
+
expect(onFinish).toHaveBeenCalledTimes(3)
|
212
|
+
})
|
213
|
+
|
214
|
+
it('can unsubscribe', async () => {
|
215
|
+
const onSuccess = vi.fn()
|
216
|
+
const onError = vi.fn()
|
217
|
+
const onFinish = vi.fn()
|
218
|
+
|
219
|
+
await hook(async (hooks) => {
|
220
|
+
hooks.onSuccess(onSuccess)()
|
221
|
+
hooks.onSuccess(onSuccess)()
|
222
|
+
hooks.onError(onError)()
|
223
|
+
hooks.onFinish(onFinish)()
|
224
|
+
|
225
|
+
return 'foo'
|
226
|
+
})
|
227
|
+
|
228
|
+
await expect(
|
229
|
+
hook(async (hooks) => {
|
230
|
+
hooks.onSuccess(onSuccess)()
|
231
|
+
hooks.onSuccess(onSuccess)()
|
232
|
+
hooks.onError(onError)()
|
233
|
+
hooks.onFinish(onFinish)()
|
234
|
+
|
235
|
+
throw new Error('foo')
|
236
|
+
}),
|
237
|
+
).rejects.toThrow('foo')
|
238
|
+
|
239
|
+
expect(onSuccess).not.toHaveBeenCalled()
|
240
|
+
expect(onError).not.toHaveBeenCalled()
|
241
|
+
expect(onFinish).not.toHaveBeenCalled()
|
242
|
+
})
|
243
|
+
})
|
package/src/utils.ts
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
import type { Context, Hooks, MergeContext, Promisable } from './types'
|
2
|
+
|
3
|
+
export function mergeContext<A extends Context, B extends Context>(
|
4
|
+
a: A,
|
5
|
+
b: B,
|
6
|
+
): MergeContext<A, B> {
|
7
|
+
if (!a) return b as any
|
8
|
+
if (!b) return a as any
|
9
|
+
|
10
|
+
return {
|
11
|
+
...a,
|
12
|
+
...b,
|
13
|
+
} as any
|
14
|
+
}
|
15
|
+
|
16
|
+
export async function hook<T>(
|
17
|
+
fn: (hooks: Hooks<T>) => Promisable<T>,
|
18
|
+
): Promise<T> {
|
19
|
+
const onSuccessFns: ((output: T) => Promisable<void>)[] = []
|
20
|
+
const onErrorFns: ((error: unknown) => Promisable<void>)[] = []
|
21
|
+
const onFinishFns: ((
|
22
|
+
output: T | undefined,
|
23
|
+
error: unknown | undefined,
|
24
|
+
) => Promisable<void>)[] = []
|
25
|
+
|
26
|
+
const hooks: Hooks<T> = {
|
27
|
+
onSuccess(fn) {
|
28
|
+
onSuccessFns.unshift(fn)
|
29
|
+
|
30
|
+
return () => {
|
31
|
+
const index = onSuccessFns.indexOf(fn)
|
32
|
+
if (index !== -1) onSuccessFns.splice(index, 1)
|
33
|
+
}
|
34
|
+
},
|
35
|
+
|
36
|
+
onError(fn) {
|
37
|
+
onErrorFns.unshift(fn)
|
38
|
+
|
39
|
+
return () => {
|
40
|
+
const index = onErrorFns.indexOf(fn)
|
41
|
+
if (index !== -1) onErrorFns.splice(index, 1)
|
42
|
+
}
|
43
|
+
},
|
44
|
+
|
45
|
+
onFinish(fn) {
|
46
|
+
onFinishFns.unshift(fn)
|
47
|
+
|
48
|
+
return () => {
|
49
|
+
const index = onFinishFns.indexOf(fn)
|
50
|
+
if (index !== -1) onFinishFns.splice(index, 1)
|
51
|
+
}
|
52
|
+
},
|
53
|
+
}
|
54
|
+
|
55
|
+
let error: unknown = undefined
|
56
|
+
let output: T | undefined = undefined
|
57
|
+
|
58
|
+
try {
|
59
|
+
output = await fn(hooks)
|
60
|
+
|
61
|
+
for (const onSuccessFn of onSuccessFns) {
|
62
|
+
await onSuccessFn(output)
|
63
|
+
}
|
64
|
+
|
65
|
+
return output
|
66
|
+
} catch (e) {
|
67
|
+
error = e
|
68
|
+
|
69
|
+
for (const onErrorFn of onErrorFns) {
|
70
|
+
try {
|
71
|
+
await onErrorFn(error)
|
72
|
+
} catch (e) {
|
73
|
+
error = e
|
74
|
+
}
|
75
|
+
}
|
76
|
+
|
77
|
+
throw error
|
78
|
+
} finally {
|
79
|
+
let hasNewError = false
|
80
|
+
|
81
|
+
for (const onFinishFn of onFinishFns) {
|
82
|
+
try {
|
83
|
+
await onFinishFn(output, error)
|
84
|
+
} catch (e) {
|
85
|
+
error = e
|
86
|
+
hasNewError = true
|
87
|
+
}
|
88
|
+
}
|
89
|
+
|
90
|
+
if (hasNewError) {
|
91
|
+
// biome-ignore lint/correctness/noUnsafeFinally: this behavior is expected
|
92
|
+
throw error
|
93
|
+
}
|
94
|
+
}
|
95
|
+
}
|