@orpc/server 2.0.0-beta.48 → 2.0.0-beta.49

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 (269) hide show
  1. package/README.md +4 -4
  2. package/dist/adapters/aws-lambda/handler.d.ts +25 -0
  3. package/dist/adapters/aws-lambda/handler.js +24 -0
  4. package/dist/adapters/aws-lambda/index.d.ts +16 -0
  5. package/dist/adapters/aws-lambda/index.js +2 -0
  6. package/dist/adapters/aws-lambda/rpc-handler.d.ts +19 -0
  7. package/dist/adapters/aws-lambda/rpc-handler.js +18 -0
  8. package/dist/adapters/crossws/handler.d.ts +45 -0
  9. package/dist/adapters/crossws/handler.js +60 -0
  10. package/dist/adapters/crossws/index.d.ts +2 -0
  11. package/dist/adapters/crossws/index.js +2 -0
  12. package/dist/adapters/crossws/rpc-handler.d.ts +16 -0
  13. package/dist/adapters/crossws/rpc-handler.js +15 -0
  14. package/dist/adapters/fastify/handler.d.ts +21 -0
  15. package/dist/adapters/fastify/handler.js +20 -0
  16. package/dist/adapters/fastify/index.d.ts +2 -0
  17. package/dist/adapters/fastify/index.js +2 -0
  18. package/dist/adapters/fastify/rpc-handler.d.ts +15 -0
  19. package/dist/adapters/fastify/rpc-handler.js +14 -0
  20. package/dist/adapters/fetch/handler.d.ts +23 -0
  21. package/dist/adapters/fetch/handler.js +22 -0
  22. package/dist/adapters/fetch/index.d.ts +7 -0
  23. package/dist/adapters/fetch/index.js +7 -0
  24. package/dist/adapters/fetch/rpc-handler.d.ts +17 -0
  25. package/dist/adapters/fetch/rpc-handler.js +16 -0
  26. package/dist/adapters/message-port/handler.d.ts +64 -0
  27. package/dist/adapters/message-port/handler.js +86 -0
  28. package/dist/adapters/message-port/index.d.ts +2 -0
  29. package/dist/adapters/message-port/index.js +2 -0
  30. package/dist/adapters/message-port/rpc-handler.d.ts +16 -0
  31. package/dist/adapters/message-port/rpc-handler.js +15 -0
  32. package/dist/adapters/node/handler.d.ts +21 -0
  33. package/dist/adapters/node/handler.js +20 -0
  34. package/dist/adapters/node/index.d.ts +7 -0
  35. package/dist/adapters/node/index.js +7 -0
  36. package/dist/adapters/node/rpc-handler.d.ts +16 -0
  37. package/dist/adapters/node/rpc-handler.js +15 -0
  38. package/dist/adapters/standard/codec.d.ts +16 -0
  39. package/dist/adapters/standard/handler.d.ts +68 -0
  40. package/dist/adapters/standard/handler.js +205 -0
  41. package/dist/adapters/standard/index.d.ts +6 -0
  42. package/dist/adapters/standard/index.js +5 -0
  43. package/dist/adapters/standard/plugin.d.ts +32 -0
  44. package/dist/adapters/standard/plugin.js +16 -0
  45. package/dist/adapters/standard/rpc-handler-codec.d.ts +46 -0
  46. package/dist/adapters/standard/rpc-handler-codec.js +51 -0
  47. package/dist/adapters/standard/rpc-matcher.d.ts +53 -0
  48. package/dist/adapters/standard/rpc-matcher.js +152 -0
  49. package/dist/adapters/standard/utils.d.ts +10 -0
  50. package/dist/adapters/standard/utils.js +6 -0
  51. package/dist/adapters/standard-peer/index.d.ts +1 -0
  52. package/dist/adapters/standard-peer/index.js +1 -0
  53. package/dist/adapters/standard-peer/{utils.d.mts → utils.d.ts} +3 -3
  54. package/dist/adapters/standard-peer/utils.js +7 -0
  55. package/dist/adapters/websocket/handler.d.ts +59 -0
  56. package/dist/adapters/websocket/handler.js +85 -0
  57. package/dist/adapters/websocket/index.d.ts +2 -0
  58. package/dist/adapters/websocket/index.js +2 -0
  59. package/dist/adapters/websocket/rpc-handler.d.ts +16 -0
  60. package/dist/adapters/websocket/rpc-handler.js +15 -0
  61. package/dist/builder-variants.d.ts +226 -0
  62. package/dist/builder.d.ts +127 -0
  63. package/dist/builder.js +226 -0
  64. package/dist/context.d.ts +6 -0
  65. package/dist/extensions/callable.d.ts +11 -0
  66. package/dist/extensions/callable.js +7 -0
  67. package/dist/helpers/base64url.d.ts +31 -0
  68. package/dist/helpers/base64url.js +53 -0
  69. package/dist/helpers/cookie.d.ts +51 -0
  70. package/dist/helpers/cookie.js +69 -0
  71. package/dist/helpers/encryption.d.ts +28 -0
  72. package/dist/helpers/encryption.js +80 -0
  73. package/dist/helpers/index.d.ts +4 -0
  74. package/dist/helpers/index.js +4 -0
  75. package/dist/helpers/signing.d.ts +50 -0
  76. package/dist/helpers/signing.js +88 -0
  77. package/dist/implementer-procedure.d.ts +16 -0
  78. package/dist/implementer-procedure.js +44 -0
  79. package/dist/implementer-router.d.ts +40 -0
  80. package/dist/implementer-router.js +117 -0
  81. package/dist/implementer.d.ts +16 -0
  82. package/dist/implementer.js +37 -0
  83. package/dist/index.d.ts +61 -0
  84. package/dist/index.js +48 -0
  85. package/dist/lazy.d.ts +20 -0
  86. package/dist/lazy.js +31 -0
  87. package/dist/middleware-decorated.d.ts +10 -0
  88. package/dist/middleware-decorated.js +71 -0
  89. package/dist/middleware.d.ts +54 -0
  90. package/dist/plugins/batch.d.ts +102 -0
  91. package/dist/plugins/batch.js +405 -0
  92. package/dist/plugins/cors.d.ts +73 -0
  93. package/dist/plugins/cors.js +112 -0
  94. package/dist/plugins/get-method-csrf-protection.d.ts +23 -0
  95. package/dist/plugins/get-method-csrf-protection.js +64 -0
  96. package/dist/plugins/head-method.d.ts +23 -0
  97. package/dist/plugins/head-method.js +104 -0
  98. package/dist/plugins/index.d.ts +39 -0
  99. package/dist/plugins/index.js +29 -0
  100. package/dist/plugins/method-override.d.ts +38 -0
  101. package/dist/plugins/method-override.js +50 -0
  102. package/dist/plugins/request-compression.d.ts +16 -0
  103. package/dist/plugins/request-compression.js +82 -0
  104. package/dist/plugins/request-headers.d.ts +22 -0
  105. package/dist/plugins/request-headers.js +30 -0
  106. package/dist/plugins/request-limit.d.ts +30 -0
  107. package/dist/plugins/request-limit.js +68 -0
  108. package/dist/plugins/response-compression.d.ts +59 -0
  109. package/dist/plugins/response-compression.js +209 -0
  110. package/dist/plugins/response-headers.d.ts +30 -0
  111. package/dist/plugins/response-headers.js +47 -0
  112. package/dist/plugins/rethrow.d.ts +33 -0
  113. package/dist/plugins/rethrow.js +59 -0
  114. package/dist/plugins/signal-sync.d.ts +13 -0
  115. package/dist/plugins/signal-sync.js +91 -0
  116. package/dist/plugins/timeout.d.ts +37 -0
  117. package/dist/plugins/timeout.js +59 -0
  118. package/dist/procedure-client.d.ts +19 -0
  119. package/dist/procedure-client.js +175 -0
  120. package/dist/procedure-decorated.d.ts +10 -0
  121. package/dist/procedure-decorated.js +44 -0
  122. package/dist/procedure-utils.d.ts +41 -0
  123. package/dist/procedure-utils.js +49 -0
  124. package/dist/procedure.d.ts +72 -0
  125. package/dist/procedure.js +24 -0
  126. package/dist/router-client.d.ts +23 -0
  127. package/dist/router-client.js +43 -0
  128. package/dist/{router-hidden.d.mts → router-hidden.d.ts} +3 -3
  129. package/dist/router-hidden.js +14 -0
  130. package/dist/router-utils.d.ts +48 -0
  131. package/dist/router-utils.js +202 -0
  132. package/dist/router.d.ts +72 -0
  133. package/package.json +84 -86
  134. package/dist/adapters/aws-lambda/handler.d.mts +0 -25
  135. package/dist/adapters/aws-lambda/handler.mjs +0 -21
  136. package/dist/adapters/aws-lambda/index.d.mts +0 -16
  137. package/dist/adapters/aws-lambda/index.mjs +0 -2
  138. package/dist/adapters/aws-lambda/rpc-handler.d.mts +0 -18
  139. package/dist/adapters/aws-lambda/rpc-handler.mjs +0 -10
  140. package/dist/adapters/crossws/handler.d.mts +0 -41
  141. package/dist/adapters/crossws/handler.mjs +0 -38
  142. package/dist/adapters/crossws/index.d.mts +0 -2
  143. package/dist/adapters/crossws/index.mjs +0 -2
  144. package/dist/adapters/crossws/rpc-handler.d.mts +0 -15
  145. package/dist/adapters/crossws/rpc-handler.mjs +0 -10
  146. package/dist/adapters/fastify/handler.d.mts +0 -21
  147. package/dist/adapters/fastify/handler.mjs +0 -20
  148. package/dist/adapters/fastify/index.d.mts +0 -2
  149. package/dist/adapters/fastify/index.mjs +0 -2
  150. package/dist/adapters/fastify/rpc-handler.d.mts +0 -14
  151. package/dist/adapters/fastify/rpc-handler.mjs +0 -10
  152. package/dist/adapters/fetch/handler.d.mts +0 -23
  153. package/dist/adapters/fetch/handler.mjs +0 -22
  154. package/dist/adapters/fetch/index.d.mts +0 -7
  155. package/dist/adapters/fetch/index.mjs +0 -5
  156. package/dist/adapters/fetch/rpc-handler.d.mts +0 -16
  157. package/dist/adapters/fetch/rpc-handler.mjs +0 -10
  158. package/dist/adapters/message-port/handler.d.mts +0 -57
  159. package/dist/adapters/message-port/handler.mjs +0 -62
  160. package/dist/adapters/message-port/index.d.mts +0 -2
  161. package/dist/adapters/message-port/index.mjs +0 -2
  162. package/dist/adapters/message-port/rpc-handler.d.mts +0 -15
  163. package/dist/adapters/message-port/rpc-handler.mjs +0 -10
  164. package/dist/adapters/node/handler.d.mts +0 -21
  165. package/dist/adapters/node/handler.mjs +0 -20
  166. package/dist/adapters/node/index.d.mts +0 -7
  167. package/dist/adapters/node/index.mjs +0 -5
  168. package/dist/adapters/node/rpc-handler.d.mts +0 -15
  169. package/dist/adapters/node/rpc-handler.mjs +0 -10
  170. package/dist/adapters/standard/codec.d.mts +0 -16
  171. package/dist/adapters/standard/handler.d.mts +0 -68
  172. package/dist/adapters/standard/handler.mjs +0 -185
  173. package/dist/adapters/standard/index.d.mts +0 -6
  174. package/dist/adapters/standard/index.mjs +0 -5
  175. package/dist/adapters/standard/plugin.d.mts +0 -32
  176. package/dist/adapters/standard/plugin.mjs +0 -16
  177. package/dist/adapters/standard/rpc-handler-codec.d.mts +0 -42
  178. package/dist/adapters/standard/rpc-handler-codec.mjs +0 -51
  179. package/dist/adapters/standard/rpc-matcher.d.mts +0 -53
  180. package/dist/adapters/standard/rpc-matcher.mjs +0 -140
  181. package/dist/adapters/standard/utils.d.mts +0 -10
  182. package/dist/adapters/standard/utils.mjs +0 -6
  183. package/dist/adapters/standard-peer/index.d.mts +0 -1
  184. package/dist/adapters/standard-peer/index.mjs +0 -1
  185. package/dist/adapters/standard-peer/utils.mjs +0 -11
  186. package/dist/adapters/websocket/handler.d.mts +0 -55
  187. package/dist/adapters/websocket/handler.mjs +0 -55
  188. package/dist/adapters/websocket/index.d.mts +0 -2
  189. package/dist/adapters/websocket/index.mjs +0 -2
  190. package/dist/adapters/websocket/rpc-handler.d.mts +0 -15
  191. package/dist/adapters/websocket/rpc-handler.mjs +0 -10
  192. package/dist/builder-variants.d.mts +0 -267
  193. package/dist/builder.d.mts +0 -141
  194. package/dist/builder.mjs +0 -145
  195. package/dist/context.d.mts +0 -13
  196. package/dist/extensions/callable.d.mts +0 -17
  197. package/dist/extensions/callable.mjs +0 -7
  198. package/dist/helpers/base64url.d.mts +0 -31
  199. package/dist/helpers/base64url.mjs +0 -25
  200. package/dist/helpers/cookie.d.mts +0 -50
  201. package/dist/helpers/cookie.mjs +0 -35
  202. package/dist/helpers/encryption.d.mts +0 -28
  203. package/dist/helpers/encryption.mjs +0 -62
  204. package/dist/helpers/index.d.mts +0 -4
  205. package/dist/helpers/index.mjs +0 -4
  206. package/dist/helpers/signing.d.mts +0 -50
  207. package/dist/helpers/signing.mjs +0 -43
  208. package/dist/implementer-procedure.d.mts +0 -36
  209. package/dist/implementer-procedure.mjs +0 -38
  210. package/dist/implementer-router.d.mts +0 -78
  211. package/dist/implementer-router.mjs +0 -77
  212. package/dist/implementer.d.mts +0 -22
  213. package/dist/implementer.mjs +0 -30
  214. package/dist/index.d.mts +0 -57
  215. package/dist/index.mjs +0 -30
  216. package/dist/lazy.d.mts +0 -20
  217. package/dist/lazy.mjs +0 -22
  218. package/dist/middleware-decorated.d.mts +0 -27
  219. package/dist/middleware-decorated.mjs +0 -65
  220. package/dist/middleware.d.mts +0 -70
  221. package/dist/plugins/batch.d.mts +0 -104
  222. package/dist/plugins/batch.mjs +0 -373
  223. package/dist/plugins/cors.d.mts +0 -67
  224. package/dist/plugins/cors.mjs +0 -134
  225. package/dist/plugins/get-method-csrf-protection.d.mts +0 -23
  226. package/dist/plugins/get-method-csrf-protection.mjs +0 -45
  227. package/dist/plugins/head-method.d.mts +0 -23
  228. package/dist/plugins/head-method.mjs +0 -95
  229. package/dist/plugins/index.d.mts +0 -39
  230. package/dist/plugins/index.mjs +0 -23
  231. package/dist/plugins/method-override.d.mts +0 -38
  232. package/dist/plugins/method-override.mjs +0 -50
  233. package/dist/plugins/request-compression.d.mts +0 -16
  234. package/dist/plugins/request-compression.mjs +0 -67
  235. package/dist/plugins/request-headers.d.mts +0 -22
  236. package/dist/plugins/request-headers.mjs +0 -21
  237. package/dist/plugins/request-limit.d.mts +0 -30
  238. package/dist/plugins/request-limit.mjs +0 -53
  239. package/dist/plugins/response-compression.d.mts +0 -59
  240. package/dist/plugins/response-compression.mjs +0 -172
  241. package/dist/plugins/response-headers.d.mts +0 -30
  242. package/dist/plugins/response-headers.mjs +0 -34
  243. package/dist/plugins/rethrow.d.mts +0 -33
  244. package/dist/plugins/rethrow.mjs +0 -46
  245. package/dist/plugins/signal-sync.d.mts +0 -13
  246. package/dist/plugins/signal-sync.mjs +0 -84
  247. package/dist/plugins/timeout.d.mts +0 -37
  248. package/dist/plugins/timeout.mjs +0 -56
  249. package/dist/procedure-client.d.mts +0 -41
  250. package/dist/procedure-client.mjs +0 -157
  251. package/dist/procedure-decorated.d.mts +0 -20
  252. package/dist/procedure-decorated.mjs +0 -41
  253. package/dist/procedure-utils.d.mts +0 -52
  254. package/dist/procedure-utils.mjs +0 -33
  255. package/dist/procedure.d.mts +0 -93
  256. package/dist/procedure.mjs +0 -20
  257. package/dist/router-client.d.mts +0 -27
  258. package/dist/router-client.mjs +0 -35
  259. package/dist/router-hidden.mjs +0 -12
  260. package/dist/router-utils.d.mts +0 -59
  261. package/dist/router-utils.mjs +0 -191
  262. package/dist/router.d.mts +0 -61
  263. /package/dist/adapters/standard/{codec.mjs → codec.js} +0 -0
  264. /package/dist/{builder-variants.mjs → builder-variants.js} +0 -0
  265. /package/dist/{constants.d.mts → constants.d.ts} +0 -0
  266. /package/dist/{constants.mjs → constants.js} +0 -0
  267. /package/dist/{context.mjs → context.js} +0 -0
  268. /package/dist/{middleware.mjs → middleware.js} +0 -0
  269. /package/dist/{router.mjs → router.js} +0 -0
package/dist/lazy.js ADDED
@@ -0,0 +1,31 @@
1
+ import { getConstructor, isNonPrimitive } from '@orpc/shared';
2
+ export class Lazy {
3
+ '~orpc';
4
+ constructor(def) {
5
+ this['~orpc'] = def;
6
+ }
7
+ /**
8
+ * Checks if the given instance satisfies the {@link Lazy} class/interface.
9
+ */
10
+ static [Symbol.hasInstance](instance) {
11
+ if (this !== Lazy) {
12
+ // fallback to default instanceof check if this is extended class
13
+ return Function.prototype[Symbol.hasInstance].call(this, instance);
14
+ }
15
+ const constructor = getConstructor(instance);
16
+ if (constructor === Lazy) {
17
+ return true;
18
+ }
19
+ return (isNonPrimitive(instance) &&
20
+ isNonPrimitive(instance['~orpc']) &&
21
+ isNonPrimitive(instance['~orpc'].meta) &&
22
+ (instance['~orpc'].metaPlugins === undefined ||
23
+ Array.isArray(instance['~orpc'].metaPlugins)) &&
24
+ typeof instance['~orpc'].loader === 'function');
25
+ }
26
+ }
27
+ export function unlazy(maybeLazy) {
28
+ return maybeLazy instanceof Lazy
29
+ ? maybeLazy['~orpc'].loader()
30
+ : Promise.resolve({ default: maybeLazy });
31
+ }
@@ -0,0 +1,10 @@
1
+ import type { ErrorMap, MergedErrorMap } from '@orpc/contract';
2
+ import type { IntersectPick } from '@orpc/shared';
3
+ import type { Context, MergedContext, MergedInitialContext } from './context.ts';
4
+ import type { Middleware } from './middleware.ts';
5
+ export interface DecoratedMiddleware<TInContext extends Context, TOutContext extends Context, TInput, TOutput, TErrorMap extends ErrorMap> extends Middleware<TInContext, TOutContext, TInput, TOutput, TErrorMap> {
6
+ adaptInput<T>(adapt: (input: T) => TInput): DecoratedMiddleware<TInContext, TOutContext, T, TOutput, TErrorMap>;
7
+ errors<T extends ErrorMap>(errors: T): DecoratedMiddleware<TInContext, TOutContext, TInput, TOutput, MergedErrorMap<TErrorMap, T>>;
8
+ use<$OutContext extends IntersectPick<MergedContext<TInContext, TOutContext>, $OutContext>, $Input extends TInput, $InContext extends Context = MergedContext<TInContext, TOutContext>, $ErrorMap extends ErrorMap = TErrorMap>(middleware: Middleware<$InContext | MergedContext<TInContext, TOutContext>, $OutContext, $Input, TOutput, $ErrorMap>): DecoratedMiddleware<MergedInitialContext<TInContext, TOutContext, $InContext>, MergedContext<TOutContext, $OutContext>, $Input, TOutput, MergedErrorMap<$ErrorMap, TErrorMap>>;
9
+ }
10
+ export declare function decorateMiddleware<TInContext extends Context, TOutContext extends Context, TInput, TOutput, TErrorMap extends ErrorMap>(middleware: Middleware<TInContext, TOutContext, TInput, TOutput, TErrorMap>): DecoratedMiddleware<TInContext, TOutContext, TInput, TOutput, TErrorMap>;
@@ -0,0 +1,71 @@
1
+ import { mergeErrorMap } from '@orpc/contract';
2
+ import { toArray } from '@orpc/shared';
3
+ export function decorateMiddleware(middleware) {
4
+ const decorated = ((...args) => middleware(...args));
5
+ decorated['~orpc'] = middleware['~orpc'];
6
+ Object.defineProperty(decorated, 'name', {
7
+ value: middleware.name,
8
+ });
9
+ decorated.adaptInput = (adapt) => {
10
+ const mapped = decorateMiddleware((opts, input, ...args) => {
11
+ return middleware(opts, adapt(input), ...args);
12
+ });
13
+ mapped['~orpc'] = decorated['~orpc'];
14
+ Object.defineProperty(mapped, 'name', {
15
+ value: decorated.name,
16
+ });
17
+ return mapped;
18
+ };
19
+ decorated.errors = (errors) => {
20
+ const newMiddleware = decorateMiddleware(decorated);
21
+ newMiddleware['~orpc'] = {
22
+ ...decorated['~orpc'],
23
+ errorMap: mergeErrorMap(decorated['~orpc']?.errorMap, errors),
24
+ };
25
+ return newMiddleware;
26
+ };
27
+ decorated.use = (usedMiddleware) => {
28
+ const merged = decorateMiddleware((opts, ...args) => {
29
+ return middleware({
30
+ ...opts,
31
+ async next(nextOpts1) {
32
+ const signal = nextOpts1?.signal ?? opts.signal;
33
+ const result = await usedMiddleware({
34
+ ...opts,
35
+ context: { ...opts.context, ...nextOpts1?.context },
36
+ signal,
37
+ next(nextOpts2, ...args) {
38
+ return opts.next({
39
+ context: {
40
+ ...nextOpts1?.context,
41
+ ...nextOpts2?.context,
42
+ },
43
+ signal: nextOpts2?.signal ?? signal,
44
+ }, ...args);
45
+ },
46
+ }, ...args);
47
+ return {
48
+ ...result,
49
+ context: {
50
+ ...nextOpts1?.context,
51
+ ...result.context,
52
+ },
53
+ };
54
+ },
55
+ }, ...args);
56
+ });
57
+ merged['~orpc'] = {
58
+ ...decorated['~orpc'],
59
+ errorMap: mergeErrorMap(usedMiddleware['~orpc']?.errorMap, decorated['~orpc']?.errorMap),
60
+ metaPlugins: [
61
+ ...toArray(decorated['~orpc']?.metaPlugins),
62
+ ...toArray(usedMiddleware['~orpc']?.metaPlugins),
63
+ ],
64
+ };
65
+ Object.defineProperty(merged, 'name', {
66
+ value: `${decorated.name} + ${usedMiddleware.name}`,
67
+ });
68
+ return merged;
69
+ };
70
+ return decorated;
71
+ }
@@ -0,0 +1,54 @@
1
+ import type { AnyMetaPlugin, ErrorMap, ORPCErrorConstructorMap } from '@orpc/contract';
2
+ import type { MaybeOptionalOptions, Promisable } from '@orpc/shared';
3
+ import type { Context } from './context.ts';
4
+ import type { AnyProcedure } from './procedure.ts';
5
+ export type MiddlewareResult<TOutContext extends Context, TOutput> = Promisable<{
6
+ output: TOutput;
7
+ context: TOutContext;
8
+ }>;
9
+ export type MiddlewareNextOptions<TOutContext extends Context> = (object extends TOutContext ? {
10
+ context?: TOutContext | undefined;
11
+ } : {
12
+ context: TOutContext;
13
+ }) & {
14
+ /** Overrides the signal for the rest of the chain. */
15
+ signal?: AbortSignal | undefined;
16
+ };
17
+ export interface MiddlewareNext<TOutput> {
18
+ <T extends Context = object>(...rest: MaybeOptionalOptions<MiddlewareNextOptions<T>>): MiddlewareResult<T, TOutput>;
19
+ }
20
+ export type MiddlewareDoneOptions<TOutContext, TOutput> = (object extends TOutContext ? {
21
+ context?: TOutContext | undefined;
22
+ } : {
23
+ context: TOutContext;
24
+ }) & {
25
+ output: TOutput;
26
+ };
27
+ export interface MiddlewareDone<TOutput> {
28
+ /**
29
+ * Create a successful result and terminate the middleware chain early.
30
+ */
31
+ <TOutContext extends Context = object>(...rest: MaybeOptionalOptions<MiddlewareDoneOptions<TOutContext, TOutput>>): MiddlewareResult<TOutContext, TOutput>;
32
+ }
33
+ export interface MiddlewareOptions<TInContext extends Context, TOutput, TErrorConstructorMap extends ORPCErrorConstructorMap<any>> {
34
+ context: TInContext;
35
+ path: string[];
36
+ procedure: AnyProcedure;
37
+ signal?: AbortSignal | undefined;
38
+ lastEventId: string | undefined;
39
+ /**
40
+ * Invoke to continue the middleware chain.
41
+ */
42
+ next: MiddlewareNext<TOutput>;
43
+ errors: TErrorConstructorMap;
44
+ }
45
+ export interface MiddlewareDefinition<TErrorMap extends ErrorMap> {
46
+ errorMap?: TErrorMap | undefined;
47
+ metaPlugins?: AnyMetaPlugin[] | undefined;
48
+ }
49
+ export interface Middleware<TInContext extends Context, TOutContext extends Context, TInput, TOutput, TErrorMap extends ErrorMap> {
50
+ /** this property should be optional to support inline middleware */
51
+ '~orpc'?: MiddlewareDefinition<TErrorMap> | undefined;
52
+ (opts: MiddlewareOptions<TInContext, TOutput, ORPCErrorConstructorMap<TErrorMap>>, input: TInput, done: MiddlewareDone<TOutput>): Promisable<MiddlewareResult<TOutContext, TOutput>>;
53
+ }
54
+ export type AnyMiddleware = Middleware<any, any, any, any, any>;
@@ -0,0 +1,102 @@
1
+ import type { Promisable, Value } from '@orpc/shared';
2
+ import type { StandardHeaders, StandardLazyRequest, StandardResponse } from '@standard-server/core';
3
+ import type { StandardHandlerOptions, StandardHandlerPlugin, StandardHandlerRoutingInterceptorOptions } from '../adapters/standard/index.ts';
4
+ import type { Context } from '../context.ts';
5
+ /**
6
+ * Content type for batch responses that use the length-prefixed binary framing,
7
+ * which they do in `streaming` mode, or when any sub-response contains binary or is a stream.
8
+ *
9
+ * Decoding is driven by the `standard-server` body hint, not this header,
10
+ * so it only serves to describe the payload to logs, proxies, and dev tools.
11
+ */
12
+ export declare const BATCH_CONTENT_TYPE = "application/vnd.orpc.batch";
13
+ export interface BatchHandlerPluginOptions<T extends Context> {
14
+ /**
15
+ * The max size of the batch allowed. Larger batches are rejected with a `413`,
16
+ * so keep the client Batch Link Plugin's `maxSize` at or below it.
17
+ *
18
+ * @default DEFAULT_BATCH_MAX_SIZE (10)
19
+ * @see {@link https://orpc.dev/docs/plugins/batch#max-size | Batch Plugin - Max Size}
20
+ */
21
+ maxSize?: Value<Promisable<number>, [options: StandardHandlerRoutingInterceptorOptions<T>]> | undefined;
22
+ /**
23
+ * Map each subrequest in the batch before it is processed.
24
+ *
25
+ * @default merges the batch request headers into the subrequest, giving them priority over
26
+ * the subrequest headers, and removes the `orpc-batch` header to prevent nested batching
27
+ */
28
+ mapSubrequest?: ((subrequest: StandardLazyRequest, batchOptions: StandardHandlerRoutingInterceptorOptions<T>) => StandardLazyRequest) | undefined;
29
+ /**
30
+ * Map each sub-response in the batch before it is sent.
31
+ *
32
+ * @default removes the headers the batch response already has with the same value, which the client
33
+ * merges back, and `set-cookie`, which the browser ignores there and JavaScript could read
34
+ */
35
+ mapSubresponse?: ((subresponse: StandardResponse, partialBatchResponse: Pick<StandardResponse, 'status' | 'headers'>, batchOptions: StandardHandlerRoutingInterceptorOptions<T>) => StandardResponse) | undefined;
36
+ /**
37
+ * Success batch response status code.
38
+ *
39
+ * @default 207
40
+ */
41
+ successStatus?: Value<Promisable<number>, [batchOptions: StandardHandlerRoutingInterceptorOptions<T>]> | undefined;
42
+ /**
43
+ * Success batch response headers.
44
+ *
45
+ * In `buffered` mode, the batch response also gets the cookies its sub-responses set, and the headers they all share.
46
+ * In `streaming` mode, it gets only the cookies of the first sub-response to be ready.
47
+ * Headers set here win, and setting one to `undefined` keeps it off, except `set-cookie` and `vary`, which merge.
48
+ *
49
+ * @default {}
50
+ */
51
+ headers?: Value<Promisable<StandardHeaders>, [
52
+ batchOptions: StandardHandlerRoutingInterceptorOptions<T>
53
+ ]> | undefined;
54
+ /**
55
+ * Keep-alive frames for batch responses that stream.
56
+ *
57
+ * A zero-length length-prefixed frame is sent periodically while the stream is idle
58
+ * (no message sent for `interval` ms). Clients ignore these frames.
59
+ *
60
+ * `false` disables it, an object customizes it.
61
+ *
62
+ * @default true
63
+ */
64
+ keepAlive?: boolean | {
65
+ /**
66
+ * Interval (in milliseconds) between keep-alive frames after the last message.
67
+ *
68
+ * @default 15000
69
+ */
70
+ interval?: number | undefined;
71
+ } | undefined;
72
+ }
73
+ /**
74
+ * Handles batch requests sent by the client Batch Link Plugin, splitting each
75
+ * batch into sub-requests and sending their responses back together.
76
+ *
77
+ * @remarks
78
+ * The client Batch Link Plugin's `mode` picks how each batch responds: `buffered` (default) waits for every
79
+ * sub-response and carries their cookies and shared headers; `streaming` starts once the first is ready and
80
+ * carries only its cookies.
81
+ *
82
+ * **Note**: HTTP/2 and later already multiplex requests over a single connection, which often makes this plugin unnecessary.
83
+ *
84
+ * @see {@link https://orpc.dev/docs/plugins/batch | Batch Plugin}
85
+ */
86
+ export declare class BatchHandlerPlugin<T extends Context> implements StandardHandlerPlugin<T> {
87
+ name: string;
88
+ /**
89
+ * Run batch interceptors inside the tracing interceptors
90
+ * so each subrequest gets its own span instead of sharing one batch-level span.
91
+ */
92
+ after: string[];
93
+ private readonly maxSize;
94
+ private readonly mapSubrequest;
95
+ private readonly mapSubresponse;
96
+ private readonly successStatus;
97
+ private readonly headers;
98
+ private readonly keepAliveEnabled;
99
+ private readonly keepAliveInterval;
100
+ constructor(options?: BatchHandlerPluginOptions<T>);
101
+ init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
102
+ }
@@ -0,0 +1,405 @@
1
+ import { ORPCError } from '@orpc/client';
2
+ import { BATCH_HEADER, DEFAULT_BATCH_MAX_SIZE } from '@orpc/client/plugins';
3
+ import { isAsyncIteratorObject, promiseWithResolvers, toArray, value } from '@orpc/shared';
4
+ import { flattenStandardHeader, parseStandardUrl } from '@standard-server/core';
5
+ import { encodePeerMessage, isClientPeerSendMessage, isPeerMessage, ServerPeer, } from '@standard-server/peer';
6
+ /**
7
+ * Content type for batch responses that use the length-prefixed binary framing,
8
+ * which they do in `streaming` mode, or when any sub-response contains binary or is a stream.
9
+ *
10
+ * Decoding is driven by the `standard-server` body hint, not this header,
11
+ * so it only serves to describe the payload to logs, proxies, and dev tools.
12
+ */
13
+ export const BATCH_CONTENT_TYPE = 'application/vnd.orpc.batch';
14
+ const textEncoder = /* @__PURE__ */ new TextEncoder();
15
+ function encodeLengthPrefix(length) {
16
+ const prefix = new Uint8Array(4);
17
+ new DataView(prefix.buffer).setUint32(0, length, false);
18
+ return prefix;
19
+ }
20
+ /**
21
+ * Encodes a message as a length-prefixed frame: its 4-byte big-endian length, then its bytes.
22
+ */
23
+ async function encodeFrame(message) {
24
+ const encoded = await encodePeerMessage(message);
25
+ const bytes = typeof encoded === 'string' ? textEncoder.encode(encoded) : encoded;
26
+ return [encodeLengthPrefix(bytes.byteLength), bytes];
27
+ }
28
+ /**
29
+ * Whether a header describes a subresponse's own body, which the batch response does not share.
30
+ */
31
+ function isBodyHeader(name) {
32
+ return name.startsWith('content-') || name === 'standard-server';
33
+ }
34
+ /**
35
+ * Whether untrusted batch JSON is a list of client peer messages.
36
+ * `isPeerMessage` must run first, since `isClientPeerSendMessage` does not check `id` or `binary`.
37
+ */
38
+ function isClientPeerSendMessages(maybe) {
39
+ return (Array.isArray(maybe) &&
40
+ maybe.every((m) => isPeerMessage(m) && isClientPeerSendMessage(m)));
41
+ }
42
+ /**
43
+ * Errors should not occur at the routing interceptor level.
44
+ * Reject the promise so it can be handled by the unhandledRejection handler
45
+ * for global logging or error handling.
46
+ */
47
+ function internalError(err) {
48
+ void Promise.reject(err);
49
+ return { status: 500, headers: {}, body: 'Internal server error' };
50
+ }
51
+ /**
52
+ * Resolves with the first subresponse head, or `undefined` if none has one.
53
+ */
54
+ function firstSubresponseHead(heads) {
55
+ return new Promise((resolve) => {
56
+ for (const promise of heads) {
57
+ void promise.then((head) => head && resolve(head));
58
+ }
59
+ void Promise.all(heads).then(() => resolve(undefined));
60
+ });
61
+ }
62
+ /**
63
+ * Sets the distinct cookies of every source on the headers.
64
+ */
65
+ function liftCookies(headers, sources) {
66
+ const cookies = new Set(sources.flatMap((source) => toArray(source['set-cookie'])));
67
+ return cookies.size > 0 ? { ...headers, 'set-cookie': [...cookies] } : headers;
68
+ }
69
+ /**
70
+ * Builds the batch response headers: the headers the subresponses all have with the same value, as
71
+ * the client does with the headers every subrequest shares, then the batch headers, which win, and
72
+ * the distinct cookies and `vary` fields of both. A cache must vary on whatever any subresponse varies on.
73
+ */
74
+ function liftSubresponseHeaders(batchHeaders, subresponseHeaders) {
75
+ const [first = {}, ...rest] = subresponseHeaders;
76
+ const shared = {};
77
+ for (const [name, value] of Object.entries(first)) {
78
+ if (value === undefined || isBodyHeader(name)) {
79
+ continue;
80
+ }
81
+ const flattened = flattenStandardHeader(value);
82
+ if (rest.every((other) => flattenStandardHeader(other[name]) === flattened)) {
83
+ shared[name] = value;
84
+ }
85
+ }
86
+ const headers = liftCookies({ ...shared, ...batchHeaders }, [batchHeaders, ...subresponseHeaders]);
87
+ const varies = new Map();
88
+ for (const value of [batchHeaders, ...subresponseHeaders].flatMap((other) => toArray(other.vary))) {
89
+ for (const field of value.split(',')) {
90
+ const name = field.trim();
91
+ const key = name.toLowerCase();
92
+ if (name && !varies.has(key)) {
93
+ varies.set(key, name);
94
+ }
95
+ }
96
+ }
97
+ if (varies.size > 0) {
98
+ headers.vary = varies.has('*') ? '*' : [...varies.values()].join(', ');
99
+ }
100
+ return headers;
101
+ }
102
+ /**
103
+ * Handles batch requests sent by the client Batch Link Plugin, splitting each
104
+ * batch into sub-requests and sending their responses back together.
105
+ *
106
+ * @remarks
107
+ * The client Batch Link Plugin's `mode` picks how each batch responds: `buffered` (default) waits for every
108
+ * sub-response and carries their cookies and shared headers; `streaming` starts once the first is ready and
109
+ * carries only its cookies.
110
+ *
111
+ * **Note**: HTTP/2 and later already multiplex requests over a single connection, which often makes this plugin unnecessary.
112
+ *
113
+ * @see {@link https://orpc.dev/docs/plugins/batch | Batch Plugin}
114
+ */
115
+ export class BatchHandlerPlugin {
116
+ name = '~batch';
117
+ /**
118
+ * Run batch interceptors inside the tracing interceptors
119
+ * so each subrequest gets its own span instead of sharing one batch-level span.
120
+ */
121
+ after = ['~tracing'];
122
+ maxSize;
123
+ mapSubrequest;
124
+ mapSubresponse;
125
+ successStatus;
126
+ headers;
127
+ keepAliveEnabled;
128
+ keepAliveInterval;
129
+ constructor(options = {}) {
130
+ this.maxSize = options.maxSize ?? DEFAULT_BATCH_MAX_SIZE;
131
+ this.mapSubrequest =
132
+ options.mapSubrequest ??
133
+ ((subRequest, { request: batchRequest }) => ({
134
+ ...subRequest,
135
+ headers: {
136
+ ...subRequest.headers,
137
+ /**
138
+ * The batch request headers win over the subrequest ones. They are the only ones the
139
+ * transport actually saw, so headers the browser injects on its own, such as `cookie`
140
+ * or `origin`, cannot be overridden by a subrequest, which is just request payload.
141
+ */
142
+ ...batchRequest.headers,
143
+ [BATCH_HEADER]: undefined, // useful in case batch plugin is used multiple times
144
+ },
145
+ }));
146
+ this.mapSubresponse =
147
+ options.mapSubresponse ??
148
+ ((subresponse, { headers: batchHeaders }) => {
149
+ const headers = {};
150
+ for (const [name, value] of Object.entries(subresponse.headers)) {
151
+ if (name !== 'set-cookie' &&
152
+ flattenStandardHeader(value) !== flattenStandardHeader(batchHeaders[name])) {
153
+ headers[name] = value;
154
+ }
155
+ }
156
+ return { ...subresponse, headers };
157
+ });
158
+ this.successStatus = options.successStatus ?? 207;
159
+ this.headers = options.headers ?? {};
160
+ this.keepAliveEnabled = options.keepAlive !== false;
161
+ this.keepAliveInterval =
162
+ (typeof options.keepAlive === 'object' ? options.keepAlive.interval : undefined) ?? 15_000;
163
+ }
164
+ init(options) {
165
+ const routingInterceptor = async (interceptorOptions) => {
166
+ const batchHeader = flattenStandardHeader(interceptorOptions.request.headers[BATCH_HEADER]);
167
+ if (batchHeader === undefined) {
168
+ return interceptorOptions.next();
169
+ }
170
+ const streaming = batchHeader === 'streaming';
171
+ let messages;
172
+ try {
173
+ if (interceptorOptions.request.method === 'GET') {
174
+ const [, search] = parseStandardUrl(interceptorOptions.request.url);
175
+ const params = new URLSearchParams(search);
176
+ const data = params.getAll('data').at(-1);
177
+ if (!data) {
178
+ return {
179
+ matched: true,
180
+ response: {
181
+ status: 400,
182
+ headers: {},
183
+ body: 'Missing data parameter for batch request',
184
+ },
185
+ };
186
+ }
187
+ const mightBeMessages = JSON.parse(data);
188
+ if (!isClientPeerSendMessages(mightBeMessages)) {
189
+ return {
190
+ matched: true,
191
+ response: { status: 400, headers: {}, body: 'Invalid batch request data parameter' },
192
+ };
193
+ }
194
+ messages = mightBeMessages;
195
+ }
196
+ else {
197
+ const mightBeMessages = await interceptorOptions.request.resolveBody();
198
+ if (!isClientPeerSendMessages(mightBeMessages)) {
199
+ return {
200
+ matched: true,
201
+ response: { status: 400, headers: {}, body: 'Invalid batch request body' },
202
+ };
203
+ }
204
+ messages = mightBeMessages;
205
+ }
206
+ }
207
+ catch (error) {
208
+ return {
209
+ matched: true,
210
+ response: {
211
+ status: 400,
212
+ headers: {},
213
+ body: error instanceof ORPCError ? error.message : 'Invalid batch request',
214
+ },
215
+ };
216
+ }
217
+ const outerMethod = interceptorOptions.request.method;
218
+ if ((outerMethod === 'GET' || outerMethod === 'QUERY') &&
219
+ messages.some((message) => message.kind === 'request' && message.json.method !== outerMethod)) {
220
+ return {
221
+ matched: true,
222
+ response: {
223
+ status: 400,
224
+ headers: {},
225
+ body: `${outerMethod} batch requests only accept ${outerMethod} sub-requests`,
226
+ },
227
+ };
228
+ }
229
+ const maxSize = await value(this.maxSize, interceptorOptions);
230
+ if (messages.length > maxSize) {
231
+ return {
232
+ matched: true,
233
+ response: {
234
+ status: 413,
235
+ headers: {},
236
+ body: 'Batch request size exceeds the maximum allowed size',
237
+ },
238
+ };
239
+ }
240
+ const status = await value(this.successStatus, interceptorOptions);
241
+ const headers = await value(this.headers, interceptorOptions);
242
+ // Messages the peer sends before the response streams, or the whole body when it does not
243
+ const bufferedMessages = [];
244
+ let streamController;
245
+ let keepAliveTimer;
246
+ // Subresponses wait here for the batch headers: lifted from all of them, or only the first in streaming mode
247
+ const ready = messages.map(() => promiseWithResolvers());
248
+ const heads = ready.map(({ promise }) => promise);
249
+ const lifting = streaming
250
+ ? firstSubresponseHead(heads).then((head) => ({
251
+ batchHeaders: liftCookies(headers, [headers, head?.headers ?? {}]),
252
+ streams: true,
253
+ }))
254
+ : Promise.all(heads).then((all) => {
255
+ const present = all.filter((head) => head !== undefined);
256
+ return {
257
+ batchHeaders: liftSubresponseHeaders(headers, present.map((head) => head.headers)),
258
+ streams: present.some((head) => head.stream),
259
+ };
260
+ });
261
+ const clearKeepAlive = () => {
262
+ if (keepAliveTimer !== undefined) {
263
+ clearInterval(keepAliveTimer);
264
+ keepAliveTimer = undefined;
265
+ }
266
+ };
267
+ const scheduleKeepAlive = () => {
268
+ if (!this.keepAliveEnabled) {
269
+ return;
270
+ }
271
+ clearKeepAlive();
272
+ keepAliveTimer = setInterval(() => {
273
+ try {
274
+ // Zero-length length-prefixed frame = keep-alive (ignored by clients)
275
+ streamController.enqueue(encodeLengthPrefix(0));
276
+ }
277
+ catch {
278
+ // Stream may already be closed or errored.
279
+ clearKeepAlive();
280
+ }
281
+ }, this.keepAliveInterval);
282
+ };
283
+ const enqueueMessage = async (message) => {
284
+ for (const chunk of await encodeFrame(message)) {
285
+ streamController.enqueue(chunk);
286
+ }
287
+ scheduleKeepAlive(); // reset idle timer
288
+ };
289
+ const peer = new ServerPeer(async (message) => {
290
+ if (streamController === undefined) {
291
+ bufferedMessages.push(message);
292
+ }
293
+ else {
294
+ await enqueueMessage(message);
295
+ }
296
+ });
297
+ const handleIndividualRequest = async (request, markReady) => {
298
+ let response;
299
+ try {
300
+ request = this.mapSubrequest(request, interceptorOptions);
301
+ const result = await interceptorOptions.next({ ...interceptorOptions, request });
302
+ response = result.matched
303
+ ? result.response
304
+ : { status: 404, headers: {}, body: 'No procedure matched' };
305
+ }
306
+ catch (err) {
307
+ response = internalError(err);
308
+ }
309
+ markReady({
310
+ headers: response.headers,
311
+ stream: response.body instanceof ReadableStream || isAsyncIteratorObject(response.body),
312
+ });
313
+ const { batchHeaders } = await lifting;
314
+ try {
315
+ return this.mapSubresponse(response, { status, headers: batchHeaders }, interceptorOptions);
316
+ }
317
+ catch (err) {
318
+ return internalError(err);
319
+ }
320
+ };
321
+ const runSubrequests = async () => {
322
+ const promise = Promise.all(messages.map((msg, i) => {
323
+ const { resolve } = ready[i];
324
+ // Not every message reaches the handler, such as a cancel or a repeated id
325
+ return peer
326
+ .message(msg, (request) => handleIndividualRequest(request, resolve))
327
+ .finally(() => resolve(undefined));
328
+ }));
329
+ const signal = interceptorOptions.request.signal;
330
+ const closePeer = () => peer.close(signal?.reason);
331
+ if (signal?.aborted) {
332
+ void closePeer();
333
+ }
334
+ signal?.addEventListener('abort', closePeer);
335
+ try {
336
+ await promise;
337
+ }
338
+ finally {
339
+ signal?.removeEventListener('abort', closePeer);
340
+ }
341
+ };
342
+ const subrequests = runSubrequests();
343
+ const { batchHeaders, streams } = await lifting;
344
+ const framedHeaders = {
345
+ ...batchHeaders,
346
+ 'content-type': batchHeaders['content-type'] ?? BATCH_CONTENT_TYPE,
347
+ };
348
+ if (!streams) {
349
+ await subrequests;
350
+ await peer.close();
351
+ if (bufferedMessages.some((msg) => msg.binary !== undefined)) {
352
+ const chunks = (await Promise.all(bufferedMessages.map(encodeFrame))).flat();
353
+ return {
354
+ matched: true,
355
+ response: {
356
+ status,
357
+ headers: framedHeaders,
358
+ body: new Blob(chunks),
359
+ },
360
+ };
361
+ }
362
+ return {
363
+ matched: true,
364
+ response: { status, headers: batchHeaders, body: bufferedMessages },
365
+ };
366
+ }
367
+ // Answer with a length-prefixed stream of every message, starting with those buffered so far
368
+ const body = new ReadableStream({
369
+ start(controller) {
370
+ streamController = controller;
371
+ scheduleKeepAlive();
372
+ },
373
+ async cancel(reason) {
374
+ clearKeepAlive();
375
+ await peer.close(reason);
376
+ },
377
+ });
378
+ const flushed = Promise.all(bufferedMessages.splice(0).map(enqueueMessage));
379
+ // DO NOT await here to block streaming response
380
+ Promise.all([flushed, subrequests])
381
+ .then(async () => {
382
+ clearKeepAlive();
383
+ streamController.close();
384
+ await peer.close();
385
+ })
386
+ .catch(async (error) => {
387
+ clearKeepAlive();
388
+ streamController.error(error);
389
+ await peer.close(error);
390
+ });
391
+ return {
392
+ matched: true,
393
+ response: {
394
+ status,
395
+ headers: framedHeaders,
396
+ body,
397
+ },
398
+ };
399
+ };
400
+ return {
401
+ ...options,
402
+ routingInterceptors: [routingInterceptor, ...toArray(options.routingInterceptors)],
403
+ };
404
+ }
405
+ }