@orpc/node 2.0.0-beta.28 β†’ 2.0.0-beta.30

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/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  <a href="https://codecov.io/gh/middleapi/orpc">
5
5
  <img alt="codecov" src="https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg">
6
6
  </a>
7
- <a href="https://www.npmjs.com/package/@orpc/node">
7
+ <a href="https://npmx.dev/package/@orpc/node">
8
8
  <img alt="weekly downloads" src="https://img.shields.io/npm/dw/%40orpc%2Fnode?logo=npm" />
9
9
  </a>
10
10
  <a href="https://app.codspeed.io/middleapi/orpc?utm_source=badge">
@@ -29,69 +29,63 @@ You can read the documentation [here](https://orpc.dev).
29
29
 
30
30
  **Core**
31
31
 
32
- - [@orpc/contract](https://www.npmjs.com/package/@orpc/contract): Define API contract as the single source of truth.
33
- - [@orpc/server](https://www.npmjs.com/package/@orpc/server): Build APIs or implement contracts.
34
- - [@orpc/client](https://www.npmjs.com/package/@orpc/client): Consume APIs with end-to-end type safety.
35
- - [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi): Add OpenAPI compatibility to APIs.
32
+ - [@orpc/contract](https://npmx.dev/package/@orpc/contract): Define API contract as the single source of truth.
33
+ - [@orpc/server](https://npmx.dev/package/@orpc/server): Build APIs or implement contracts.
34
+ - [@orpc/client](https://npmx.dev/package/@orpc/client): Consume APIs with end-to-end type safety.
35
+ - [@orpc/openapi](https://npmx.dev/package/@orpc/openapi): Add OpenAPI compatibility to APIs.
36
36
 
37
37
  **Schema validation**
38
38
 
39
- - [@orpc/zod](https://www.npmjs.com/package/@orpc/zod): Integrate with [Zod](https://zod.dev/).
40
- - [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot): Integrate with [Valibot](https://valibot.dev/).
41
- - [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype): Integrate with [ArkType](https://arktype.io/).
39
+ - [@orpc/zod](https://npmx.dev/package/@orpc/zod): Integrate with [Zod](https://zod.dev/).
40
+ - [@orpc/valibot](https://npmx.dev/package/@orpc/valibot): Integrate with [Valibot](https://valibot.dev/).
41
+ - [@orpc/arktype](https://npmx.dev/package/@orpc/arktype): Integrate with [ArkType](https://arktype.io/).
42
42
 
43
43
  **Built-in features**
44
44
 
45
- - [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher): Pub/Sub with memory, Redis, and Upstash adapters.
46
- - [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit): Rate limiting with memory, Redis, and Upstash adapters.
47
- - [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation): Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api).
48
- - [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema): Smart coercion for OpenAPI requests.
45
+ - [@orpc/publisher](https://npmx.dev/package/@orpc/publisher): Pub/Sub with memory, Redis, and Upstash adapters.
46
+ - [@orpc/ratelimit](https://npmx.dev/package/@orpc/ratelimit): Rate limiting with memory, Redis, and Upstash adapters.
47
+ - [@orpc/hibernation](https://npmx.dev/package/@orpc/hibernation): Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api).
48
+ - [@orpc/json-schema](https://npmx.dev/package/@orpc/json-schema): Smart coercion for OpenAPI requests.
49
49
 
50
50
  **Framework & ecosystem integrations**
51
51
 
52
- - [@orpc/next](https://www.npmjs.com/package/@orpc/next): Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data).
53
- - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.
54
- - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest).
55
- - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).
56
- - [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/).
57
- - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/).
58
- - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/).
59
- - [@orpc/node](https://www.npmjs.com/package/@orpc/node): [Node.js](https://nodejs.org/) plugins, helpers, like serving static files.
60
- - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/).
61
- - [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare): Adapters for [Cloudflare's RateLimit and Durable Objects](https://developers.cloudflare.com/workers/).
62
- - [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc): Reuse existing [tRPC](https://trpc.io/) routers within oRPC.
52
+ - [@orpc/next](https://npmx.dev/package/@orpc/next): Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data).
53
+ - [@orpc/ai-sdk](https://npmx.dev/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.
54
+ - [@orpc/tanstack-query](https://npmx.dev/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest).
55
+ - [@orpc/pinia-colada](https://npmx.dev/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).
56
+ - [@orpc/swr](https://npmx.dev/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/).
57
+ - [@orpc/experimental-msw](https://npmx.dev/package/@orpc/experimental-msw): Mock procedures with [Mock Service Worker](https://mswjs.io/).
58
+ - [@orpc/experimental-effect](https://npmx.dev/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/).
59
+ - [@orpc/nest](https://npmx.dev/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/).
60
+ - [@orpc/node](https://npmx.dev/package/@orpc/node): [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.
61
+ - [@orpc/bun](https://npmx.dev/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/).
62
+ - [@orpc/cloudflare](https://npmx.dev/package/@orpc/cloudflare): Adapters for [Cloudflare's RateLimit and Durable Objects](https://developers.cloudflare.com/workers/).
63
+ - [@orpc/trpc](https://npmx.dev/package/@orpc/trpc): Reuse existing [tRPC](https://trpc.io/) routers within oRPC.
63
64
 
64
65
  **Observability**
65
66
 
66
- - [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry): Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing.
67
- - [@orpc/pino](https://www.npmjs.com/package/@orpc/pino): Integrate with [Pino](https://getpino.io/) for logging.
68
- - [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog): Integrate with [Evlog](https://evlog.dev/) for logging.
67
+ - [@orpc/opentelemetry](https://npmx.dev/package/@orpc/opentelemetry): Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing.
68
+ - [@orpc/pino](https://npmx.dev/package/@orpc/pino): Integrate with [Pino](https://getpino.io/) for logging.
69
+ - [@orpc/evlog](https://npmx.dev/package/@orpc/evlog): Integrate with [Evlog](https://evlog.dev/) for logging.
69
70
 
70
71
  ## Sponsors
71
72
 
72
- Like what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going here: [GitHub Sponsors](https://github.com/sponsors/dinwwwh). Every bit helps! πŸš€
73
-
74
- ### πŸ† Platinum Sponsor
73
+ Like what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going through [GitHub Sponsors](https://github.com/sponsors/dinwwwh) or [Open Collective](https://opencollective.com/middleapi). Every bit helps! πŸš€
75
74
 
76
75
  <table>
77
76
  <tr>
78
- <td align="center"><a href="https://screenshotone.com/?ref=orpc" target="_blank" rel="sponsored noopener" title="ScreenshotOne.com"><img src="https://avatars.githubusercontent.com/u/97035603?v=4" width="279" alt="ScreenshotOne.com"/><br />ScreenshotOne.com</a></td>
77
+ <td width="2000"><a href="https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener" title="The screenshot API for developers"><img src="https://avatars.githubusercontent.com/u/97035603?v=4" width="64" align="left" hspace="12" alt="ScreenshotOne.com"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>
79
78
  </tr>
80
- </table>
81
-
82
- ### πŸ₯ˆ Silver Sponsor
83
-
84
- <table>
85
79
  <tr>
86
- <td align="center"><a href="https://misskey.io/?ref=orpc" target="_blank" rel="sponsored noopener" title="ζ‘δΈŠγ•γ‚“"><img src="https://avatars.githubusercontent.com/u/37681609?u=0dd4c7e4ba937cbb52b068c55914b1d8164dc0c7&amp;v=4" width="209" alt="ζ‘δΈŠγ•γ‚“"/><br />ζ‘δΈŠγ•γ‚“</a></td>
80
+ <td width="2000"><a href="https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Decentralized microblogging SNS born on Earth"><img src="https://github.com/MisskeyIO.png" width="64" align="left" hspace="12" alt="MisskeyHQ"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>
87
81
  </tr>
88
82
  </table>
89
83
 
90
- ### Generous Sponsors
84
+ ### Organization Sponsors
91
85
 
92
86
  <table>
93
87
  <tr>
94
- <td align="center"><a href="https://github.com/ln-markets?ref=orpc" target="_blank" rel="sponsored noopener" title="LN Markets"><img src="https://avatars.githubusercontent.com/u/70597625?v=4" width="167" alt="LN Markets"/><br />LN Markets</a></td>
88
+ <td align="center"><a href="https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="LN Markets"><img src="https://avatars.githubusercontent.com/u/70597625?v=4" width="167" alt="LN Markets"/><br />LN Markets</a></td>
95
89
  </tr>
96
90
  </table>
97
91
 
@@ -99,26 +93,26 @@ Like what we build over at [middleapi](https://github.com/middleapi)? You can he
99
93
 
100
94
  <table>
101
95
  <tr>
102
- <td align="center"><a href="https://github.com/hrmcdonald?ref=orpc" target="_blank" rel="sponsored noopener" title="Reece McDonald"><img src="https://avatars.githubusercontent.com/u/39349270?v=4" width="139" alt="Reece McDonald"/><br />Reece McDonald</a></td>
103
- <td align="center"><a href="https://github.com/u1-liquid?ref=orpc" target="_blank" rel="sponsored noopener" title="あわわわとーにゅ"><img src="https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4" width="139" alt="あわわわとーにゅ"/><br />あわわわとーにゅ</a></td>
104
- <td align="center"><a href="https://github.com/nicognaW?ref=orpc" target="_blank" rel="sponsored noopener" title="nk"><img src="https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4" width="139" alt="nk"/><br />nk</a></td>
105
- <td align="center"><a href="https://github.com/supastarter?ref=orpc" target="_blank" rel="sponsored noopener" title="supastarter"><img src="https://avatars.githubusercontent.com/u/110960143?v=4" width="139" alt="supastarter"/><br />supastarter</a></td>
106
- <td align="center"><a href="https://github.com/divmgl?ref=orpc" target="_blank" rel="sponsored noopener" title="Dexter Miguel"><img src="https://avatars.githubusercontent.com/u/5452298?u=645993204be8696c085ecf0d228c3062efe2ed65&amp;v=4" width="139" alt="Dexter Miguel"/><br />Dexter Miguel</a></td>
107
- <td align="center"><a href="https://github.com/herrfugbaum?ref=orpc" target="_blank" rel="sponsored noopener" title="herrfugbaum"><img src="https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4" width="139" alt="herrfugbaum"/><br />herrfugbaum</a></td>
96
+ <td align="center"><a href="https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Reece McDonald"><img src="https://avatars.githubusercontent.com/u/39349270?v=4" width="139" alt="Reece McDonald"/><br />Reece McDonald</a></td>
97
+ <td align="center"><a href="https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="あわわわとーにゅ"><img src="https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4" width="139" alt="あわわわとーにゅ"/><br />あわわわとーにゅ</a></td>
98
+ <td align="center"><a href="https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="nk"><img src="https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4" width="139" alt="nk"/><br />nk</a></td>
99
+ <td align="center"><a href="https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="supastarter"><img src="https://avatars.githubusercontent.com/u/110960143?v=4" width="139" alt="supastarter"/><br />supastarter</a></td>
100
+ <td align="center"><a href="https://github.com/divmgl?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Dexter Miguel"><img src="https://avatars.githubusercontent.com/u/5452298?u=645993204be8696c085ecf0d228c3062efe2ed65&amp;v=4" width="139" alt="Dexter Miguel"/><br />Dexter Miguel</a></td>
101
+ <td align="center"><a href="https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="herrfugbaum"><img src="https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4" width="139" alt="herrfugbaum"/><br />herrfugbaum</a></td>
108
102
  </tr>
109
103
  <tr>
110
- <td align="center"><a href="https://github.com/ryota-murakami?ref=orpc" target="_blank" rel="sponsored noopener" title="Ryota Murakami"><img src="https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4" width="139" alt="Ryota Murakami"/><br />Ryota Murakami</a></td>
111
- <td align="center"><a href="https://github.com/dcramer?ref=orpc" target="_blank" rel="sponsored noopener" title="David Cramer"><img src="https://avatars.githubusercontent.com/u/23610?v=4" width="139" alt="David Cramer"/><br />David Cramer</a></td>
112
- <td align="center"><a href="https://github.com/valerii15298?ref=orpc" target="_blank" rel="sponsored noopener" title="Valerii Petryniak"><img src="https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4" width="139" alt="Valerii Petryniak"/><br />Valerii Petryniak</a></td>
113
- <td align="center"><a href="https://github.com/letstri?ref=orpc" target="_blank" rel="sponsored noopener" title="Valerii Strilets"><img src="https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4" width="139" alt="Valerii Strilets"/><br />Valerii Strilets</a></td>
114
- <td align="center"><a href="https://github.com/K-Mistele?ref=orpc" target="_blank" rel="sponsored noopener" title="Kyle Mistele"><img src="https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4" width="139" alt="Kyle Mistele"/><br />Kyle Mistele</a></td>
115
- <td align="center"><a href="https://github.com/christ12938?ref=orpc" target="_blank" rel="sponsored noopener" title="christ12938"><img src="https://avatars.githubusercontent.com/u/25758598?v=4" width="139" alt="christ12938"/><br />christ12938</a></td>
104
+ <td align="center"><a href="https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Ryota Murakami"><img src="https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4" width="139" alt="Ryota Murakami"/><br />Ryota Murakami</a></td>
105
+ <td align="center"><a href="https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="David Cramer"><img src="https://avatars.githubusercontent.com/u/23610?v=4" width="139" alt="David Cramer"/><br />David Cramer</a></td>
106
+ <td align="center"><a href="https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Valerii Petryniak"><img src="https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4" width="139" alt="Valerii Petryniak"/><br />Valerii Petryniak</a></td>
107
+ <td align="center"><a href="https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Valerii Strilets"><img src="https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4" width="139" alt="Valerii Strilets"/><br />Valerii Strilets</a></td>
108
+ <td align="center"><a href="https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Kyle Mistele"><img src="https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4" width="139" alt="Kyle Mistele"/><br />Kyle Mistele</a></td>
109
+ <td align="center"><a href="https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="christ12938"><img src="https://avatars.githubusercontent.com/u/25758598?v=4" width="139" alt="christ12938"/><br />christ12938</a></td>
116
110
  </tr>
117
111
  <tr>
118
- <td align="center"><a href="https://github.com/Ryanjso?ref=orpc" target="_blank" rel="sponsored noopener" title="Ryan Soderberg"><img src="https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4" width="139" alt="Ryan Soderberg"/><br />Ryan Soderberg</a></td>
119
- <td align="center"><a href="https://github.com/itigoore01?ref=orpc" target="_blank" rel="sponsored noopener" title="shota"><img src="https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4" width="139" alt="shota"/><br />shota</a></td>
120
- <td align="center"><a href="https://github.com/ellis-driscoll?ref=orpc" target="_blank" rel="sponsored noopener" title="Ellis Driscoll"><img src="https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4" width="139" alt="Ellis Driscoll"/><br />Ellis Driscoll</a></td>
121
- <td align="center"><a href="https://github.com/hoangbn?ref=orpc" target="_blank" rel="sponsored noopener" title="Hoang Nguyen"><img src="https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4" width="139" alt="Hoang Nguyen"/><br />Hoang Nguyen</a></td>
112
+ <td align="center"><a href="https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Ryan Soderberg"><img src="https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4" width="139" alt="Ryan Soderberg"/><br />Ryan Soderberg</a></td>
113
+ <td align="center"><a href="https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="shota"><img src="https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4" width="139" alt="shota"/><br />shota</a></td>
114
+ <td align="center"><a href="https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Ellis Driscoll"><img src="https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4" width="139" alt="Ellis Driscoll"/><br />Ellis Driscoll</a></td>
115
+ <td align="center"><a href="https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Hoang Nguyen"><img src="https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4" width="139" alt="Hoang Nguyen"/><br />Hoang Nguyen</a></td>
122
116
  </tr>
123
117
  </table>
124
118
 
@@ -126,31 +120,31 @@ Like what we build over at [middleapi](https://github.com/middleapi)? You can he
126
120
 
127
121
  <table>
128
122
  <tr>
129
- <td align="center"><a href="https://github.com/rhinodavid?ref=orpc" target="_blank" rel="sponsored noopener" title="David Walsh"><img src="https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4" width="119" alt="David Walsh"/><br />David Walsh</a></td>
130
- <td align="center"><a href="https://github.com/Robbe95?ref=orpc" target="_blank" rel="sponsored noopener" title="Robbe Vaes"><img src="https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4" width="119" alt="Robbe Vaes"/><br />Robbe Vaes</a></td>
131
- <td align="center"><a href="https://github.com/aidansunbury?ref=orpc" target="_blank" rel="sponsored noopener" title="Aidan Sunbury"><img src="https://avatars.githubusercontent.com/u/64103161?v=4" width="119" alt="Aidan Sunbury"/><br />Aidan Sunbury</a></td>
132
- <td align="center"><a href="https://github.com/soonoo?ref=orpc" target="_blank" rel="sponsored noopener" title="soonoo"><img src="https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4" width="119" alt="soonoo"/><br />soonoo</a></td>
133
- <td align="center"><a href="https://github.com/kporten?ref=orpc" target="_blank" rel="sponsored noopener" title="Kevin Porten"><img src="https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4" width="119" alt="Kevin Porten"/><br />Kevin Porten</a></td>
134
- <td align="center"><a href="https://github.com/pumpkinlink?ref=orpc" target="_blank" rel="sponsored noopener" title="Denis"><img src="https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4" width="119" alt="Denis"/><br />Denis</a></td>
135
- <td align="center"><a href="https://github.com/christopher-kapic?ref=orpc" target="_blank" rel="sponsored noopener" title="Christopher Kapic"><img src="https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4" width="119" alt="Christopher Kapic"/><br />Christopher Kapic</a></td>
123
+ <td align="center"><a href="https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="David Walsh"><img src="https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4" width="119" alt="David Walsh"/><br />David Walsh</a></td>
124
+ <td align="center"><a href="https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Robbe Vaes"><img src="https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4" width="119" alt="Robbe Vaes"/><br />Robbe Vaes</a></td>
125
+ <td align="center"><a href="https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Aidan Sunbury"><img src="https://avatars.githubusercontent.com/u/64103161?v=4" width="119" alt="Aidan Sunbury"/><br />Aidan Sunbury</a></td>
126
+ <td align="center"><a href="https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="soonoo"><img src="https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4" width="119" alt="soonoo"/><br />soonoo</a></td>
127
+ <td align="center"><a href="https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Kevin Porten"><img src="https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4" width="119" alt="Kevin Porten"/><br />Kevin Porten</a></td>
128
+ <td align="center"><a href="https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Denis"><img src="https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4" width="119" alt="Denis"/><br />Denis</a></td>
129
+ <td align="center"><a href="https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Christopher Kapic"><img src="https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4" width="119" alt="Christopher Kapic"/><br />Christopher Kapic</a></td>
136
130
  </tr>
137
131
  <tr>
138
- <td align="center"><a href="https://github.com/thomasballinger?ref=orpc" target="_blank" rel="sponsored noopener" title="Tom Ballinger"><img src="https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4" width="119" alt="Tom Ballinger"/><br />Tom Ballinger</a></td>
139
- <td align="center"><a href="https://github.com/SSam0419?ref=orpc" target="_blank" rel="sponsored noopener" title="Sam"><img src="https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4" width="119" alt="Sam"/><br />Sam</a></td>
140
- <td align="center"><a href="https://github.com/Titoine?ref=orpc" target="_blank" rel="sponsored noopener" title="Titoine"><img src="https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4" width="119" alt="Titoine"/><br />Titoine</a></td>
141
- <td align="center"><a href="https://github.com/Mnigos?ref=orpc" target="_blank" rel="sponsored noopener" title="Igor Makowski"><img src="https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4" width="119" alt="Igor Makowski"/><br />Igor Makowski</a></td>
142
- <td align="center"><a href="https://github.com/hanayashiki?ref=orpc" target="_blank" rel="sponsored noopener" title="hanayashiki"><img src="https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4" width="119" alt="hanayashiki"/><br />hanayashiki</a></td>
143
- <td align="center"><a href="https://github.com/ldub?ref=orpc" target="_blank" rel="sponsored noopener" title="Lev Dubinets"><img src="https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4" width="119" alt="Lev Dubinets"/><br />Lev Dubinets</a></td>
144
- <td align="center"><a href="https://github.com/mr-kelly?ref=orpc" target="_blank" rel="sponsored noopener" title="Kelly Peilin Chan"><img src="https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4" width="119" alt="Kelly Peilin Chan"/><br />Kelly Peilin Chan</a></td>
132
+ <td align="center"><a href="http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Tom Ballinger"><img src="https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4" width="119" alt="Tom Ballinger"/><br />Tom Ballinger</a></td>
133
+ <td align="center"><a href="https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Sam"><img src="https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4" width="119" alt="Sam"/><br />Sam</a></td>
134
+ <td align="center"><a href="https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Titoine"><img src="https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4" width="119" alt="Titoine"/><br />Titoine</a></td>
135
+ <td align="center"><a href="https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Igor Makowski"><img src="https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4" width="119" alt="Igor Makowski"/><br />Igor Makowski</a></td>
136
+ <td align="center"><a href="https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="hanayashiki"><img src="https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4" width="119" alt="hanayashiki"/><br />hanayashiki</a></td>
137
+ <td align="center"><a href="https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Lev Dubinets"><img src="https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4" width="119" alt="Lev Dubinets"/><br />Lev Dubinets</a></td>
138
+ <td align="center"><a href="https://bika.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Kelly Peilin Chan"><img src="https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4" width="119" alt="Kelly Peilin Chan"/><br />Kelly Peilin Chan</a></td>
145
139
  </tr>
146
140
  <tr>
147
- <td align="center"><a href="https://github.com/guyariely?ref=orpc" target="_blank" rel="sponsored noopener" title="Guy Ariely"><img src="https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4" width="119" alt="Guy Ariely"/><br />Guy Ariely</a></td>
148
- <td align="center"><a href="https://github.com/piscis?ref=orpc" target="_blank" rel="sponsored noopener" title="Alex"><img src="https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4" width="119" alt="Alex"/><br />Alex</a></td>
149
- <td align="center"><a href="https://github.com/finom?ref=orpc" target="_blank" rel="sponsored noopener" title="Andrey Gubanov"><img src="https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4" width="119" alt="Andrey Gubanov"/><br />Andrey Gubanov</a></td>
141
+ <td align="center"><a href="https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Guy Ariely"><img src="https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4" width="119" alt="Guy Ariely"/><br />Guy Ariely</a></td>
142
+ <td align="center"><a href="https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Alex"><img src="https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4" width="119" alt="Alex"/><br />Alex</a></td>
143
+ <td align="center"><a href="https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Andrey Gubanov"><img src="https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4" width="119" alt="Andrey Gubanov"/><br />Andrey Gubanov</a></td>
150
144
  </tr>
151
145
  </table>
152
146
 
153
- With thanks to 37 past sponsors who helped get oRPC here.
147
+ With thanks to [37 past sponsors](https://htmlpreview.github.io/?https://github.com/middleapi/static/blob/main/sponsors.svg) who helped get oRPC here.
154
148
 
155
149
  ## References
156
150
 
package/dist/index.d.mts CHANGED
@@ -1,7 +1,53 @@
1
1
  import { Context } from '@orpc/server';
2
- import { StandardHandlerPlugin, StandardHandlerOptions } from '@orpc/server/standard';
2
+ import { StandardHandlerPlugin, StandardHandlerOptions, StandardHandlerRoutingInterceptorOptions } from '@orpc/server/standard';
3
+ import { Value, Promisable } from '@orpc/shared';
3
4
  import { FilePropertyBag } from 'node:buffer';
4
5
 
6
+ interface BatchResponseCompressionHandlerPluginOptions {
7
+ /**
8
+ * The compression schemes to use for batch responses.
9
+ * Schemes are prioritized by their order in this array and
10
+ * only applied if the client supports them (via Accept-Encoding).
11
+ *
12
+ * @default ['gzip', 'deflate']
13
+ */
14
+ encodings?: readonly ('gzip' | 'deflate' | 'deflate-raw')[];
15
+ /**
16
+ * The minimum response size in bytes required to trigger compression.
17
+ * Responses smaller than this threshold will not be compressed to avoid overhead.
18
+ * A streaming batch response has no size until it ends, so it is always compressed.
19
+ *
20
+ * @default 1024 (1KB)
21
+ */
22
+ threshold?: number;
23
+ }
24
+ /**
25
+ * Compresses every successful [batch](https://orpc.dev/docs/plugins/batch) response, which the
26
+ * runtime-agnostic {@link https://orpc.dev/docs/plugins/response-compression | Response Compression Plugin}
27
+ * leaves untouched when framed: a batch frames subresponses of mixed content types, so its envelope
28
+ * has none of its own and nothing says whether the contents compress. Registering this plugin
29
+ * states that they do.
30
+ *
31
+ * @remarks
32
+ * Each message is flushed as soon as it is produced, so a streaming batch stays streaming.
33
+ * This is why the plugin lives in the Node.js package: `zlib` can flush mid-stream, while the
34
+ * web `CompressionStream` cannot, and would hold every early message until the batch ends.
35
+ *
36
+ * @see {@link https://orpc.dev/docs/plugins/batch-response-compression | Batch Response Compression Plugin}
37
+ */
38
+ declare class BatchResponseCompressionHandlerPlugin<T extends Context> implements StandardHandlerPlugin<T> {
39
+ name: string;
40
+ /**
41
+ * Compression should be done after batching, to compress the assembled batch response
42
+ * rather than each subresponse inside it.
43
+ */
44
+ after: string[];
45
+ private readonly encodings;
46
+ private readonly threshold;
47
+ constructor(options?: BatchResponseCompressionHandlerPluginOptions);
48
+ init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
49
+ }
50
+
5
51
  interface StaticFileHandlerPluginOptions {
6
52
  /**
7
53
  * The directory files are served from. Resolved against the working
@@ -149,7 +195,7 @@ interface TmpFileUploadHandlerPluginMaxBodySize {
149
195
  */
150
196
  stream: number;
151
197
  }
152
- interface TmpFileUploadHandlerPluginOptions {
198
+ interface TmpFileUploadHandlerPluginOptions<T extends Context> {
153
199
  /**
154
200
  * The directory temporary files are created under. Each request that spools
155
201
  * an upload gets its own subdirectory inside it, removed when the request
@@ -159,18 +205,32 @@ interface TmpFileUploadHandlerPluginOptions {
159
205
  */
160
206
  tmpDir?: string;
161
207
  /**
162
- * The size limit for each kind of request body. Every kind is required when
163
- * the option is given, so none is left unbounded by accident; set a kind to
164
- * `Number.POSITIVE_INFINITY` to deliberately leave it unlimited.
208
+ * The size limit for each kind of request body, either fixed or resolved
209
+ * per request, so limits can follow the request itself, such as a larger
210
+ * allowance for an authenticated user than for a guest. Every kind is
211
+ * required when the option is given, so none is left unbounded by accident;
212
+ * set a kind to `Number.POSITIVE_INFINITY` to deliberately leave it
213
+ * unlimited.
214
+ *
215
+ * A resolver runs only when a body is parsed, so a request without one never
216
+ * pays for it, and it receives the request as it arrived, before this plugin
217
+ * wraps its body resolution.
165
218
  *
166
219
  * @default unlimited for every kind
167
220
  */
168
- maxBodySize?: TmpFileUploadHandlerPluginMaxBodySize;
221
+ maxBodySize?: Value<Promisable<TmpFileUploadHandlerPluginMaxBodySize>, [options: StandardHandlerRoutingInterceptorOptions<T>]>;
169
222
  }
170
223
  /**
171
224
  * A `File` whose content lives in a temporary file on disk and is read lazily,
172
225
  * so it can represent an upload far larger than available memory.
173
226
  *
227
+ * @remarks
228
+ * Node's `fs.openAsBlob`, which backs this class, has known bugs with files
229
+ * over 2 GiB ({@link https://github.com/nodejs/node/issues/52585}): `size` is
230
+ * truncated above 4 GiB and `slice()` is unreliable above 2 GiB, while
231
+ * whole-file reads like `stream()` stay correct. When size or random access
232
+ * matters for such a file, stat or read {@link path} directly.
233
+ *
174
234
  * @see {@link https://orpc.dev/docs/plugins/tmp-file-upload | Tmp File Upload Plugin}
175
235
  */
176
236
  declare class TmpFile extends File {
@@ -198,8 +258,8 @@ declare class TmpFile extends File {
198
258
  * to the standard parser.
199
259
  *
200
260
  * Request body sizes are limited per content category: memory-parsed, spooled to
201
- * disk, and streamed. This subsumes the request limit plugin while sizing each
202
- * kind of body to what it actually costs.
261
+ * disk, and streamed, each limit fixed or resolved per request. This subsumes the
262
+ * request limit plugin while sizing each kind of body to what it actually costs.
203
263
  *
204
264
  * @remarks
205
265
  * Temporary files are removed when the request finishes. A streaming response body,
@@ -223,7 +283,7 @@ declare class TmpFileUploadHandlerPlugin<T extends Context> implements StandardH
223
283
  before: string[];
224
284
  private readonly tmpDir;
225
285
  private readonly maxBodySize;
226
- constructor(options?: TmpFileUploadHandlerPluginOptions);
286
+ constructor(options?: TmpFileUploadHandlerPluginOptions<T>);
227
287
  init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
228
288
  private resolveBody;
229
289
  /**
@@ -235,5 +295,5 @@ declare class TmpFileUploadHandlerPlugin<T extends Context> implements StandardH
235
295
  private parseMultipartBody;
236
296
  }
237
297
 
238
- export { StaticFileHandlerPlugin, TmpFile, TmpFileUploadHandlerPlugin };
239
- export type { StaticFileHandlerPluginOptions, TmpFileUploadHandlerPluginMaxBodySize, TmpFileUploadHandlerPluginOptions };
298
+ export { BatchResponseCompressionHandlerPlugin, StaticFileHandlerPlugin, TmpFile, TmpFileUploadHandlerPlugin };
299
+ export type { BatchResponseCompressionHandlerPluginOptions, StaticFileHandlerPluginOptions, TmpFileUploadHandlerPluginMaxBodySize, TmpFileUploadHandlerPluginOptions };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,53 @@
1
1
  import { Context } from '@orpc/server';
2
- import { StandardHandlerPlugin, StandardHandlerOptions } from '@orpc/server/standard';
2
+ import { StandardHandlerPlugin, StandardHandlerOptions, StandardHandlerRoutingInterceptorOptions } from '@orpc/server/standard';
3
+ import { Value, Promisable } from '@orpc/shared';
3
4
  import { FilePropertyBag } from 'node:buffer';
4
5
 
6
+ interface BatchResponseCompressionHandlerPluginOptions {
7
+ /**
8
+ * The compression schemes to use for batch responses.
9
+ * Schemes are prioritized by their order in this array and
10
+ * only applied if the client supports them (via Accept-Encoding).
11
+ *
12
+ * @default ['gzip', 'deflate']
13
+ */
14
+ encodings?: readonly ('gzip' | 'deflate' | 'deflate-raw')[];
15
+ /**
16
+ * The minimum response size in bytes required to trigger compression.
17
+ * Responses smaller than this threshold will not be compressed to avoid overhead.
18
+ * A streaming batch response has no size until it ends, so it is always compressed.
19
+ *
20
+ * @default 1024 (1KB)
21
+ */
22
+ threshold?: number;
23
+ }
24
+ /**
25
+ * Compresses every successful [batch](https://orpc.dev/docs/plugins/batch) response, which the
26
+ * runtime-agnostic {@link https://orpc.dev/docs/plugins/response-compression | Response Compression Plugin}
27
+ * leaves untouched when framed: a batch frames subresponses of mixed content types, so its envelope
28
+ * has none of its own and nothing says whether the contents compress. Registering this plugin
29
+ * states that they do.
30
+ *
31
+ * @remarks
32
+ * Each message is flushed as soon as it is produced, so a streaming batch stays streaming.
33
+ * This is why the plugin lives in the Node.js package: `zlib` can flush mid-stream, while the
34
+ * web `CompressionStream` cannot, and would hold every early message until the batch ends.
35
+ *
36
+ * @see {@link https://orpc.dev/docs/plugins/batch-response-compression | Batch Response Compression Plugin}
37
+ */
38
+ declare class BatchResponseCompressionHandlerPlugin<T extends Context> implements StandardHandlerPlugin<T> {
39
+ name: string;
40
+ /**
41
+ * Compression should be done after batching, to compress the assembled batch response
42
+ * rather than each subresponse inside it.
43
+ */
44
+ after: string[];
45
+ private readonly encodings;
46
+ private readonly threshold;
47
+ constructor(options?: BatchResponseCompressionHandlerPluginOptions);
48
+ init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
49
+ }
50
+
5
51
  interface StaticFileHandlerPluginOptions {
6
52
  /**
7
53
  * The directory files are served from. Resolved against the working
@@ -149,7 +195,7 @@ interface TmpFileUploadHandlerPluginMaxBodySize {
149
195
  */
150
196
  stream: number;
151
197
  }
152
- interface TmpFileUploadHandlerPluginOptions {
198
+ interface TmpFileUploadHandlerPluginOptions<T extends Context> {
153
199
  /**
154
200
  * The directory temporary files are created under. Each request that spools
155
201
  * an upload gets its own subdirectory inside it, removed when the request
@@ -159,18 +205,32 @@ interface TmpFileUploadHandlerPluginOptions {
159
205
  */
160
206
  tmpDir?: string;
161
207
  /**
162
- * The size limit for each kind of request body. Every kind is required when
163
- * the option is given, so none is left unbounded by accident; set a kind to
164
- * `Number.POSITIVE_INFINITY` to deliberately leave it unlimited.
208
+ * The size limit for each kind of request body, either fixed or resolved
209
+ * per request, so limits can follow the request itself, such as a larger
210
+ * allowance for an authenticated user than for a guest. Every kind is
211
+ * required when the option is given, so none is left unbounded by accident;
212
+ * set a kind to `Number.POSITIVE_INFINITY` to deliberately leave it
213
+ * unlimited.
214
+ *
215
+ * A resolver runs only when a body is parsed, so a request without one never
216
+ * pays for it, and it receives the request as it arrived, before this plugin
217
+ * wraps its body resolution.
165
218
  *
166
219
  * @default unlimited for every kind
167
220
  */
168
- maxBodySize?: TmpFileUploadHandlerPluginMaxBodySize;
221
+ maxBodySize?: Value<Promisable<TmpFileUploadHandlerPluginMaxBodySize>, [options: StandardHandlerRoutingInterceptorOptions<T>]>;
169
222
  }
170
223
  /**
171
224
  * A `File` whose content lives in a temporary file on disk and is read lazily,
172
225
  * so it can represent an upload far larger than available memory.
173
226
  *
227
+ * @remarks
228
+ * Node's `fs.openAsBlob`, which backs this class, has known bugs with files
229
+ * over 2 GiB ({@link https://github.com/nodejs/node/issues/52585}): `size` is
230
+ * truncated above 4 GiB and `slice()` is unreliable above 2 GiB, while
231
+ * whole-file reads like `stream()` stay correct. When size or random access
232
+ * matters for such a file, stat or read {@link path} directly.
233
+ *
174
234
  * @see {@link https://orpc.dev/docs/plugins/tmp-file-upload | Tmp File Upload Plugin}
175
235
  */
176
236
  declare class TmpFile extends File {
@@ -198,8 +258,8 @@ declare class TmpFile extends File {
198
258
  * to the standard parser.
199
259
  *
200
260
  * Request body sizes are limited per content category: memory-parsed, spooled to
201
- * disk, and streamed. This subsumes the request limit plugin while sizing each
202
- * kind of body to what it actually costs.
261
+ * disk, and streamed, each limit fixed or resolved per request. This subsumes the
262
+ * request limit plugin while sizing each kind of body to what it actually costs.
203
263
  *
204
264
  * @remarks
205
265
  * Temporary files are removed when the request finishes. A streaming response body,
@@ -223,7 +283,7 @@ declare class TmpFileUploadHandlerPlugin<T extends Context> implements StandardH
223
283
  before: string[];
224
284
  private readonly tmpDir;
225
285
  private readonly maxBodySize;
226
- constructor(options?: TmpFileUploadHandlerPluginOptions);
286
+ constructor(options?: TmpFileUploadHandlerPluginOptions<T>);
227
287
  init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
228
288
  private resolveBody;
229
289
  /**
@@ -235,5 +295,5 @@ declare class TmpFileUploadHandlerPlugin<T extends Context> implements StandardH
235
295
  private parseMultipartBody;
236
296
  }
237
297
 
238
- export { StaticFileHandlerPlugin, TmpFile, TmpFileUploadHandlerPlugin };
239
- export type { StaticFileHandlerPluginOptions, TmpFileUploadHandlerPluginMaxBodySize, TmpFileUploadHandlerPluginOptions };
298
+ export { BatchResponseCompressionHandlerPlugin, StaticFileHandlerPlugin, TmpFile, TmpFileUploadHandlerPlugin };
299
+ export type { BatchResponseCompressionHandlerPluginOptions, StaticFileHandlerPluginOptions, TmpFileUploadHandlerPluginMaxBodySize, TmpFileUploadHandlerPluginOptions };
package/dist/index.mjs CHANGED
@@ -1,15 +1,123 @@
1
+ import { Duplex, Readable } from 'node:stream';
2
+ import { constants, createGzip, createDeflate, createDeflateRaw } from 'node:zlib';
3
+ import { toArray, isNoTransformCacheControl, parseAcceptEncodingQualities, stringifyJSON, varyByAcceptEncoding, mergeHttpPath, tryDecodeURIComponent, matchesHttpPathPrefix, isCompressibleContentType, getOpenTelemetryConfig, value, isAsyncIteratorObject, override, wrapAsyncIterator, wrapReadableStream } from '@orpc/shared';
4
+ import { flattenStandardHeader, parseStandardUrl, resolveStandardBodyHint, getFilenameFromContentDisposition } from '@standardserver/core';
1
5
  import { createReadStream, openAsBlob } from 'node:fs';
2
6
  import { realpath, stat, mkdtemp, appendFile, rm } from 'node:fs/promises';
3
7
  import path from 'node:path';
4
- import { Readable } from 'node:stream';
5
- import { toArray, mergeHttpPath, tryDecodeURIComponent, matchesHttpPathPrefix, isCompressibleContentType, parseAcceptEncodingQualities, getOpenTelemetryConfig, isAsyncIteratorObject, override, wrapAsyncIterator, wrapReadableStream } from '@orpc/shared';
6
- import { parseStandardUrl, flattenStandardHeader, resolveStandardBodyHint, getFilenameFromContentDisposition } from '@standardserver/core';
7
8
  import mime from 'mime';
8
9
  import { Buffer } from 'node:buffer';
9
10
  import { tmpdir } from 'node:os';
10
11
  import { ORPCError } from '@orpc/server';
11
12
  import { toFetchHeaders, toStandardBody } from '@standardserver/fetch';
12
13
 
14
+ class BatchResponseCompressionHandlerPlugin {
15
+ name = "~batch-response-compression";
16
+ /**
17
+ * Compression should be done after batching, to compress the assembled batch response
18
+ * rather than each subresponse inside it.
19
+ */
20
+ after = ["~batch"];
21
+ encodings;
22
+ threshold;
23
+ constructor(options = {}) {
24
+ this.encodings = options.encodings ?? ["gzip", "deflate"];
25
+ this.threshold = options.threshold ?? 1024;
26
+ }
27
+ init(options) {
28
+ const routingInterceptor = async ({ next, ...interceptorOptions }) => {
29
+ const result = await next();
30
+ if (!result.matched) {
31
+ return result;
32
+ }
33
+ if (flattenStandardHeader(interceptorOptions.request.headers["orpc-batch"]) === void 0) {
34
+ return result;
35
+ }
36
+ const response = result.response;
37
+ const headers = response.headers;
38
+ const body = response.body;
39
+ if (response.status >= 400) {
40
+ return result;
41
+ }
42
+ if (flattenStandardHeader(headers["content-encoding"]) !== void 0) {
43
+ return result;
44
+ }
45
+ if (response.status === 206 || flattenStandardHeader(headers["content-range"]) !== void 0) {
46
+ return result;
47
+ }
48
+ if (isNoTransformCacheControl(flattenStandardHeader(headers["cache-control"]))) {
49
+ return result;
50
+ }
51
+ const acceptEncodings = parseAcceptEncodingQualities(
52
+ flattenStandardHeader(interceptorOptions.request.headers["accept-encoding"])
53
+ );
54
+ const encoding = this.encodings.find((enc) => (acceptEncodings.get(enc) ?? 0) > 0);
55
+ if (encoding === void 0) {
56
+ return result;
57
+ }
58
+ let stream;
59
+ let bodyHeaders;
60
+ if (Array.isArray(body)) {
61
+ const json = new Blob([stringifyJSON(body)]);
62
+ if (json.size < this.threshold) {
63
+ return result;
64
+ }
65
+ stream = json.stream();
66
+ bodyHeaders = {
67
+ // Serialized here rather than by the adapter, which no longer sees the array
68
+ "content-type": "application/json",
69
+ "standard-server": []
70
+ };
71
+ } else if (body instanceof Blob) {
72
+ if (Number.isFinite(body.size) && body.size < this.threshold) {
73
+ return result;
74
+ }
75
+ stream = body.stream();
76
+ bodyHeaders = {
77
+ /**
78
+ * The blob is gone once it becomes a stream, so the type it carried is stated here. The
79
+ * batch plugin puts it there rather than in the headers, because it builds the body
80
+ * itself instead of leaving it to the adapter.
81
+ */
82
+ "content-type": body.type,
83
+ "standard-server": "octet-stream"
84
+ };
85
+ } else if (body instanceof ReadableStream) {
86
+ stream = body;
87
+ bodyHeaders = { "standard-server": "octet-stream" };
88
+ } else {
89
+ return result;
90
+ }
91
+ return {
92
+ ...result,
93
+ response: {
94
+ ...response,
95
+ body: stream.pipeThrough(createFlushingCompressionStream(encoding)),
96
+ headers: {
97
+ ...headers,
98
+ ...bodyHeaders,
99
+ "content-length": [],
100
+ "content-encoding": encoding,
101
+ "vary": varyByAcceptEncoding(flattenStandardHeader(headers.vary))
102
+ }
103
+ }
104
+ };
105
+ };
106
+ return {
107
+ ...options,
108
+ routingInterceptors: [
109
+ routingInterceptor,
110
+ ...toArray(options.routingInterceptors)
111
+ ]
112
+ };
113
+ }
114
+ }
115
+ function createFlushingCompressionStream(encoding) {
116
+ const zlibOptions = { flush: constants.Z_SYNC_FLUSH };
117
+ const compressor = encoding === "gzip" ? createGzip(zlibOptions) : encoding === "deflate" ? createDeflate(zlibOptions) : createDeflateRaw(zlibOptions);
118
+ return Duplex.toWeb(compressor);
119
+ }
120
+
13
121
  const TEXT_CONTENT_TYPE_REGEX = /^text\//;
14
122
  const PRECOMPRESSED_ENCODINGS = [
15
123
  ["br", ".br"],
@@ -524,7 +632,7 @@ class TmpFileUploadHandlerPlugin {
524
632
  ...interceptorOptions,
525
633
  request: {
526
634
  ...interceptorOptions.request,
527
- resolveBody: (hint) => this.resolveBody(interceptorOptions.request, hint, tmpFiles)
635
+ resolveBody: (hint) => this.resolveBody(interceptorOptions, hint, tmpFiles)
528
636
  }
529
637
  });
530
638
  if (result.matched && tmpFiles.inUse) {
@@ -566,21 +674,23 @@ class TmpFileUploadHandlerPlugin {
566
674
  routingInterceptors: [routingInterceptor, ...toArray(options.routingInterceptors)]
567
675
  };
568
676
  }
569
- async resolveBody(request, hint, tmpFiles) {
677
+ async resolveBody(interceptorOptions, hint, tmpFiles) {
678
+ const { request } = interceptorOptions;
570
679
  const resolvedHint = hint ?? resolveStandardBodyHint(request.headers);
680
+ if (resolvedHint === "none") {
681
+ return request.resolveBody(hint);
682
+ }
683
+ const maxBodySize = await value(this.maxBodySize, interceptorOptions);
571
684
  if (resolvedHint === "form-data") {
572
- return this.parseMultipartBody(request, tmpFiles);
685
+ return this.parseMultipartBody(request, tmpFiles, maxBodySize);
573
686
  }
574
687
  if (resolvedHint === "file") {
575
- return this.spoolFileBody(request, tmpFiles);
688
+ return this.spoolFileBody(request, tmpFiles, maxBodySize.file);
576
689
  }
577
690
  if (resolvedHint === "json" || resolvedHint === "url-search-params") {
578
- return this.parseLimitedBody(request, hint, resolvedHint, this.maxBodySize.memory);
579
- }
580
- if (resolvedHint === "event-stream" || resolvedHint === "octet-stream") {
581
- return this.parseLimitedBody(request, hint, resolvedHint, this.maxBodySize.stream);
691
+ return this.parseLimitedBody(request, hint, resolvedHint, maxBodySize.memory);
582
692
  }
583
- return request.resolveBody(hint);
693
+ return this.parseLimitedBody(request, hint, resolvedHint, maxBodySize.stream);
584
694
  }
585
695
  /**
586
696
  * Enforces a size limit on a body the standard parser handles, counting the
@@ -600,8 +710,8 @@ class TmpFileUploadHandlerPlugin {
600
710
  });
601
711
  return toStandardBody(response, { hint: resolvedHint });
602
712
  }
603
- async spoolFileBody(request, tmpFiles) {
604
- assertContentLengthWithin(request.headers, this.maxBodySize.file);
713
+ async spoolFileBody(request, tmpFiles, fileLimit) {
714
+ assertContentLengthWithin(request.headers, fileLimit);
605
715
  const stream = await request.resolveBody("octet-stream");
606
716
  if (!(stream instanceof ReadableStream)) {
607
717
  return stream;
@@ -609,20 +719,20 @@ class TmpFileUploadHandlerPlugin {
609
719
  const contentDisposition = flattenStandardHeader(request.headers["content-disposition"]);
610
720
  const fileName = contentDisposition !== void 0 ? getFilenameFromContentDisposition(contentDisposition) : void 0;
611
721
  const contentType = flattenStandardHeader(request.headers["content-type"]);
612
- const limited = this.maxBodySize.file === Number.POSITIVE_INFINITY ? stream : limitStream(stream, this.maxBodySize.file);
722
+ const limited = fileLimit === Number.POSITIVE_INFINITY ? stream : limitStream(stream, fileLimit);
613
723
  const tmpPath = await tmpFiles.allocate();
614
724
  for await (const chunk of limited) {
615
725
  await tmpFiles.append(tmpPath, chunk);
616
726
  }
617
727
  return tmpFiles.seal(tmpPath, fileName ?? "blob", contentType ?? "");
618
728
  }
619
- async parseMultipartBody(request, tmpFiles) {
729
+ async parseMultipartBody(request, tmpFiles, maxBodySize) {
620
730
  const contentType = flattenStandardHeader(request.headers["content-type"]);
621
731
  const boundary = contentType === void 0 ? void 0 : parseHeaderParameters(contentType).get("boundary");
622
732
  if (boundary === void 0 || boundary === "") {
623
733
  throw new TypeError("Invalid multipart body: content-type header is missing the boundary parameter");
624
734
  }
625
- const totalLimit = this.maxBodySize.memory + this.maxBodySize.file;
735
+ const totalLimit = maxBodySize.memory + maxBodySize.file;
626
736
  assertContentLengthWithin(request.headers, totalLimit);
627
737
  const stream = await request.resolveBody("octet-stream");
628
738
  if (!(stream instanceof ReadableStream)) {
@@ -639,7 +749,7 @@ class TmpFileUploadHandlerPlugin {
639
749
  return {
640
750
  write: (chunk) => {
641
751
  memoryUsed += chunk.length;
642
- if (memoryUsed > this.maxBodySize.memory) {
752
+ if (memoryUsed > maxBodySize.memory) {
643
753
  throw new ORPCError("PAYLOAD_TOO_LARGE");
644
754
  }
645
755
  chunks.push(Buffer.from(chunk));
@@ -655,7 +765,7 @@ class TmpFileUploadHandlerPlugin {
655
765
  return {
656
766
  write: async (chunk) => {
657
767
  fileUsed += chunk.length;
658
- if (fileUsed > this.maxBodySize.file) {
768
+ if (fileUsed > maxBodySize.file) {
659
769
  throw new ORPCError("PAYLOAD_TOO_LARGE");
660
770
  }
661
771
  tmpPath ??= await tmpFiles.allocate();
@@ -732,4 +842,4 @@ class RequestTmpFiles {
732
842
  }
733
843
  }
734
844
 
735
- export { StaticFileHandlerPlugin, TmpFile, TmpFileUploadHandlerPlugin };
845
+ export { BatchResponseCompressionHandlerPlugin, StaticFileHandlerPlugin, TmpFile, TmpFileUploadHandlerPlugin };
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@orpc/node",
3
3
  "type": "module",
4
- "version": "2.0.0-beta.28",
4
+ "version": "2.0.0-beta.30",
5
+ "description": "Node.js plugins for oRPC: static file serving, temporary file uploads, and batch response compression",
5
6
  "license": "MIT",
6
- "funding": "https://github.com/sponsors/dinwwwh",
7
+ "funding": [
8
+ "https://github.com/sponsors/dinwwwh",
9
+ "https://opencollective.com/middleapi"
10
+ ],
7
11
  "homepage": "https://orpc.dev",
8
12
  "repository": {
9
13
  "type": "git",
@@ -12,7 +16,12 @@
12
16
  },
13
17
  "keywords": [
14
18
  "orpc",
15
- "node"
19
+ "nodejs",
20
+ "node",
21
+ "static-files",
22
+ "file-upload",
23
+ "compression",
24
+ "typescript"
16
25
  ],
17
26
  "sideEffects": false,
18
27
  "exports": {
@@ -30,12 +39,12 @@
30
39
  "@standardserver/core": "^0.8.0",
31
40
  "@standardserver/fetch": "^0.8.0",
32
41
  "mime": "^4.1.0",
33
- "@orpc/server": "2.0.0-beta.28",
34
- "@orpc/shared": "2.0.0-beta.28"
42
+ "@orpc/shared": "2.0.0-beta.30",
43
+ "@orpc/server": "2.0.0-beta.30"
35
44
  },
36
45
  "devDependencies": {
37
46
  "supertest": "^7.2.2",
38
- "@orpc/client": "2.0.0-beta.28"
47
+ "@orpc/client": "2.0.0-beta.30"
39
48
  },
40
49
  "scripts": {
41
50
  "build": "unbuild",