@orpc/server 0.0.0-unsafe-pr-2-20241118033608
Sign up to get free protection for your applications and to get access to all the features.
- 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: 'unnoq' }
|
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: 'unnoq' }
|
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 'unnoq'
|
125
|
+
}),
|
126
|
+
|
127
|
+
nested: osw.nested.router({
|
128
|
+
p2: osw.nested.p2.handler(() => {
|
129
|
+
return 'unnoq'
|
130
|
+
}),
|
131
|
+
}),
|
132
|
+
|
133
|
+
nested2: {
|
134
|
+
p3: osw.nested2.p3.handler(() => {
|
135
|
+
return 'unnoq'
|
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
|
+
}
|