@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.
Files changed (63) hide show
  1. package/dist/chunk-ACLC6USM.js +262 -0
  2. package/dist/chunk-ACLC6USM.js.map +1 -0
  3. package/dist/fetch.js +648 -0
  4. package/dist/fetch.js.map +1 -0
  5. package/dist/index.js +403 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/src/adapters/fetch.d.ts +36 -0
  8. package/dist/src/adapters/fetch.d.ts.map +1 -0
  9. package/dist/src/builder.d.ts +49 -0
  10. package/dist/src/builder.d.ts.map +1 -0
  11. package/dist/src/index.d.ts +15 -0
  12. package/dist/src/index.d.ts.map +1 -0
  13. package/dist/src/middleware.d.ts +16 -0
  14. package/dist/src/middleware.d.ts.map +1 -0
  15. package/dist/src/procedure-builder.d.ts +31 -0
  16. package/dist/src/procedure-builder.d.ts.map +1 -0
  17. package/dist/src/procedure-caller.d.ts +33 -0
  18. package/dist/src/procedure-caller.d.ts.map +1 -0
  19. package/dist/src/procedure-implementer.d.ts +19 -0
  20. package/dist/src/procedure-implementer.d.ts.map +1 -0
  21. package/dist/src/procedure.d.ts +29 -0
  22. package/dist/src/procedure.d.ts.map +1 -0
  23. package/dist/src/router-builder.d.ts +22 -0
  24. package/dist/src/router-builder.d.ts.map +1 -0
  25. package/dist/src/router-caller.d.ts +36 -0
  26. package/dist/src/router-caller.d.ts.map +1 -0
  27. package/dist/src/router-implementer.d.ts +20 -0
  28. package/dist/src/router-implementer.d.ts.map +1 -0
  29. package/dist/src/router.d.ts +14 -0
  30. package/dist/src/router.d.ts.map +1 -0
  31. package/dist/src/types.d.ts +18 -0
  32. package/dist/src/types.d.ts.map +1 -0
  33. package/dist/src/utils.d.ts +4 -0
  34. package/dist/src/utils.d.ts.map +1 -0
  35. package/dist/tsconfig.tsbuildinfo +1 -0
  36. package/package.json +57 -0
  37. package/src/adapters/fetch.test.ts +399 -0
  38. package/src/adapters/fetch.ts +228 -0
  39. package/src/builder.test.ts +362 -0
  40. package/src/builder.ts +236 -0
  41. package/src/index.ts +16 -0
  42. package/src/middleware.test.ts +279 -0
  43. package/src/middleware.ts +119 -0
  44. package/src/procedure-builder.test.ts +219 -0
  45. package/src/procedure-builder.ts +158 -0
  46. package/src/procedure-caller.test.ts +210 -0
  47. package/src/procedure-caller.ts +165 -0
  48. package/src/procedure-implementer.test.ts +215 -0
  49. package/src/procedure-implementer.ts +103 -0
  50. package/src/procedure.test.ts +312 -0
  51. package/src/procedure.ts +251 -0
  52. package/src/router-builder.test.ts +106 -0
  53. package/src/router-builder.ts +120 -0
  54. package/src/router-caller.test.ts +173 -0
  55. package/src/router-caller.ts +95 -0
  56. package/src/router-implementer.test.ts +116 -0
  57. package/src/router-implementer.ts +110 -0
  58. package/src/router.test.ts +142 -0
  59. package/src/router.ts +69 -0
  60. package/src/types.test.ts +18 -0
  61. package/src/types.ts +28 -0
  62. package/src/utils.test.ts +243 -0
  63. 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
+ }