@orpc/experimental-lock 2.0.0-beta.35
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/LICENSE +21 -0
- package/README.md +184 -0
- package/dist/adapters/base-redis.d.mts +66 -0
- package/dist/adapters/base-redis.d.ts +66 -0
- package/dist/adapters/base-redis.mjs +45 -0
- package/dist/adapters/memory.d.mts +38 -0
- package/dist/adapters/memory.d.ts +38 -0
- package/dist/adapters/memory.mjs +70 -0
- package/dist/adapters/redis.d.mts +21 -0
- package/dist/adapters/redis.d.ts +21 -0
- package/dist/adapters/redis.mjs +29 -0
- package/dist/adapters/upstash.d.mts +20 -0
- package/dist/adapters/upstash.d.ts +20 -0
- package/dist/adapters/upstash.mjs +19 -0
- package/dist/index.d.mts +81 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.mjs +49 -0
- package/dist/shared/experimental-lock.CoYbYjCJ.mjs +13 -0
- package/dist/shared/experimental-lock.D48ZxIcJ.d.mts +46 -0
- package/dist/shared/experimental-lock.D48ZxIcJ.d.ts +46 -0
- package/package.json +83 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 oRPC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
<h1 align="center">oRPC - Typesafe APIs Made Simple 🪄</h1>
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
<a href="https://codecov.io/gh/middleapi/orpc">
|
|
5
|
+
<img alt="codecov" src="https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg">
|
|
6
|
+
</a>
|
|
7
|
+
<a href="https://www.npmjs.com/package/@orpc/experimental-lock">
|
|
8
|
+
<img alt="weekly downloads" src="https://img.shields.io/npm/dw/%40orpc%2Fexperimental-lock?logo=npm" />
|
|
9
|
+
</a>
|
|
10
|
+
<a href="https://app.codspeed.io/middleapi/orpc?utm_source=badge">
|
|
11
|
+
<img alt="CodSpeed" src="https://img.shields.io/endpoint?url=https://codspeed.io/badge.json" />
|
|
12
|
+
</a>
|
|
13
|
+
<a href="https://github.com/middleapi/orpc/blob/main/LICENSE">
|
|
14
|
+
<img alt="MIT License" src="https://img.shields.io/github/license/middleapi/orpc?logo=open-source-initiative" />
|
|
15
|
+
</a>
|
|
16
|
+
<a href="https://discord.gg/TXEbwRBvQn">
|
|
17
|
+
<img alt="Discord" src="https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff" />
|
|
18
|
+
</a>
|
|
19
|
+
<a href="https://deepwiki.com/middleapi/orpc">
|
|
20
|
+
<img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki">
|
|
21
|
+
</a>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
## Documentation
|
|
25
|
+
|
|
26
|
+
You can read the documentation [here](https://orpc.dev).
|
|
27
|
+
|
|
28
|
+
## Packages
|
|
29
|
+
|
|
30
|
+
**Core**
|
|
31
|
+
|
|
32
|
+
| Package | Purpose | Docs |
|
|
33
|
+
| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
34
|
+
| [@orpc/contract](https://www.npmjs.com/package/@orpc/contract) | Define API contract as the single source of truth. | [Procedure Contract](https://orpc.dev/docs/contract/procedure), [Contract Router](https://orpc.dev/docs/contract/router), [Implementation](https://orpc.dev/docs/contract/implementation), [Generate from OpenAPI](https://orpc.dev/docs/contract/generate-from-openapi) |
|
|
35
|
+
| [@orpc/server](https://www.npmjs.com/package/@orpc/server) | Build APIs or implement contracts. | [Procedure](https://orpc.dev/docs/procedure), [Router](https://orpc.dev/docs/router), [Middleware](https://orpc.dev/docs/middleware), [Context](https://orpc.dev/docs/context), [RPC Handler](https://orpc.dev/docs/rpc/handler) |
|
|
36
|
+
| [@orpc/client](https://www.npmjs.com/package/@orpc/client) | Consume APIs with end-to-end type safety. | [Client-Side Clients](https://orpc.dev/docs/client/client-side), [Server-Side Clients](https://orpc.dev/docs/client/server-side), [RPC Link](https://orpc.dev/docs/rpc/link), [Error Handling](https://orpc.dev/docs/client/error-handling) |
|
|
37
|
+
| [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi) | Add OpenAPI compatibility to APIs. | [OpenAPI Handler](https://orpc.dev/docs/openapi/handler), [OpenAPI Link](https://orpc.dev/docs/openapi/link), [Specification](https://orpc.dev/docs/openapi/specification), [Scalar](https://orpc.dev/docs/openapi/scalar) |
|
|
38
|
+
|
|
39
|
+
**Schema validation**
|
|
40
|
+
|
|
41
|
+
| Package | Purpose | Docs |
|
|
42
|
+
| ------------------------------------------------------------ | ----------------------------------------------- | ----------------------------------------------------- |
|
|
43
|
+
| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod) | Integrate with [Zod](https://zod.dev/). | [Zod](https://orpc.dev/docs/integrations/zod) |
|
|
44
|
+
| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | Integrate with [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) |
|
|
45
|
+
| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | Integrate with [ArkType](https://arktype.io/). | [ArkType](https://orpc.dev/docs/integrations/arktype) |
|
|
46
|
+
|
|
47
|
+
**Built-in features**
|
|
48
|
+
|
|
49
|
+
| Package | Purpose | Docs |
|
|
50
|
+
| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
|
|
51
|
+
| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) |
|
|
52
|
+
| [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit) | Rate limiting with memory, Redis, and Upstash adapters. | [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) |
|
|
53
|
+
| [@orpc/experimental-lock](https://www.npmjs.com/package/@orpc/experimental-lock) | Locking with memory, Redis, and Upstash adapters. | [Lock](https://orpc.dev/docs/helpers/lock) |
|
|
54
|
+
| [@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). | [Hibernation](https://orpc.dev/docs/integrations/hibernation) |
|
|
55
|
+
| [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema) | Smart coercion for OpenAPI requests. | [Smart Coercion](https://orpc.dev/docs/plugins/smart-coercion) |
|
|
56
|
+
|
|
57
|
+
**Framework & ecosystem integrations**
|
|
58
|
+
|
|
59
|
+
| Package | Purpose | Docs |
|
|
60
|
+
| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
61
|
+
| [@orpc/next](https://www.npmjs.com/package/@orpc/next) | Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). | [Next.js](https://orpc.dev/docs/integrations/next) |
|
|
62
|
+
| [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk) | Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. | [AI SDK](https://orpc.dev/docs/integrations/ai-sdk) |
|
|
63
|
+
| [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query) | Integrate with [TanStack Query](https://tanstack.com/query/latest). | [TanStack Query](https://orpc.dev/docs/integrations/tanstack-query) |
|
|
64
|
+
| [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada) | Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). | [Pinia Colada](https://orpc.dev/docs/integrations/pinia-colada) |
|
|
65
|
+
| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) |
|
|
66
|
+
| [@orpc/experimental-msw](https://www.npmjs.com/package/@orpc/experimental-msw) | Mock procedures with [Mock Service Worker](https://mswjs.io/). | [MSW](https://orpc.dev/docs/integrations/msw) |
|
|
67
|
+
| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) |
|
|
68
|
+
| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) |
|
|
69
|
+
| [@orpc/node](https://www.npmjs.com/package/@orpc/node) | [Node.js](https://nodejs.org/) plugins for static file serving and large uploads. | [Static File](https://orpc.dev/docs/plugins/static-file), [Tmp File Upload](https://orpc.dev/docs/plugins/tmp-file-upload), [Batch Response Compression](https://orpc.dev/docs/plugins/batch-response-compression) |
|
|
70
|
+
| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock) |
|
|
71
|
+
| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Lock](https://orpc.dev/docs/helpers/lock), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) |
|
|
72
|
+
| [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc) | Reuse existing [tRPC](https://trpc.io/) routers within oRPC. | [tRPC](https://orpc.dev/docs/integrations/trpc) |
|
|
73
|
+
|
|
74
|
+
**Observability**
|
|
75
|
+
|
|
76
|
+
| Package | Purpose | Docs |
|
|
77
|
+
| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
|
|
78
|
+
| [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry) | Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. | [OpenTelemetry](https://orpc.dev/docs/integrations/opentelemetry) |
|
|
79
|
+
| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino) | Integrate with [Pino](https://getpino.io/) for logging. | [Pino](https://orpc.dev/docs/integrations/pino) |
|
|
80
|
+
| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog) | Integrate with [Evlog](https://evlog.dev/) for logging. | [Evlog](https://orpc.dev/docs/integrations/evlog) |
|
|
81
|
+
|
|
82
|
+
## Sponsors
|
|
83
|
+
|
|
84
|
+
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! 🚀
|
|
85
|
+
|
|
86
|
+
<table>
|
|
87
|
+
<tr>
|
|
88
|
+
<td width="2000"><a href="https://screenshotone.com/?ref=middleapi&utm_source=middleapi&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>
|
|
89
|
+
</tr>
|
|
90
|
+
<tr>
|
|
91
|
+
<td width="2000"><a href="https://yuzu.health/careers?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="We're hiring NYC based engineers"><img src="https://avatars.githubusercontent.com/u/102488956?v=4" width="64" align="left" hspace="12" alt="Yuzu"/><b>Yuzu</b></a><br /><sub>We're hiring NYC based engineers</sub></td>
|
|
92
|
+
</tr>
|
|
93
|
+
<tr>
|
|
94
|
+
<td width="2000"><a href="https://misskey.io/?ref=middleapi&utm_source=middleapi&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>
|
|
95
|
+
</tr>
|
|
96
|
+
</table>
|
|
97
|
+
|
|
98
|
+
### Special Sponsors
|
|
99
|
+
|
|
100
|
+
<table>
|
|
101
|
+
<tr>
|
|
102
|
+
<td align="center"><a href="http://twitter.com/rauchg?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Guillermo Rauch"><img src="https://avatars.githubusercontent.com/u/13041?u=1ee8d111657cdd02ff6d253df00978d17ee6d722&v=4" width="279" alt="Guillermo Rauch"/><br />Guillermo Rauch</a></td>
|
|
103
|
+
</tr>
|
|
104
|
+
</table>
|
|
105
|
+
|
|
106
|
+
### Organization Sponsors
|
|
107
|
+
|
|
108
|
+
<table>
|
|
109
|
+
<tr>
|
|
110
|
+
<td align="center"><a href="https://lnmarkets.com/?ref=middleapi&utm_source=middleapi&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>
|
|
111
|
+
</tr>
|
|
112
|
+
</table>
|
|
113
|
+
|
|
114
|
+
### Sponsors
|
|
115
|
+
|
|
116
|
+
<table>
|
|
117
|
+
<tr>
|
|
118
|
+
<td align="center"><a href="https://github.com/hrmcdonald?ref=middleapi&utm_source=middleapi&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>
|
|
119
|
+
<td align="center"><a href="https://soymilk.party/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="あわわわとーにゅ"><img src="https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&v=4" width="139" alt="あわわわとーにゅ"/><br />あわわわとーにゅ</a></td>
|
|
120
|
+
<td align="center"><a href="https://github.com/nicognaW?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="nk"><img src="https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&v=4" width="139" alt="nk"/><br />nk</a></td>
|
|
121
|
+
<td align="center"><a href="https://supastarter.dev/?ref=middleapi&utm_source=middleapi&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>
|
|
122
|
+
<td align="center"><a href="https://github.com/divmgl?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Dexter Miguel"><img src="https://avatars.githubusercontent.com/u/5452298?u=645993204be8696c085ecf0d228c3062efe2ed65&v=4" width="139" alt="Dexter Miguel"/><br />Dexter Miguel</a></td>
|
|
123
|
+
<td align="center"><a href="https://github.com/herrfugbaum?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="herrfugbaum"><img src="https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&v=4" width="139" alt="herrfugbaum"/><br />herrfugbaum</a></td>
|
|
124
|
+
</tr>
|
|
125
|
+
<tr>
|
|
126
|
+
<td align="center"><a href="https://laststance.io/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Ryota Murakami"><img src="https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&v=4" width="139" alt="Ryota Murakami"/><br />Ryota Murakami</a></td>
|
|
127
|
+
<td align="center"><a href="https://cra.mr/?ref=middleapi&utm_source=middleapi&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>
|
|
128
|
+
<td align="center"><a href="https://valerii15298.github.io/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Valerii Petryniak"><img src="https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&v=4" width="139" alt="Valerii Petryniak"/><br />Valerii Petryniak</a></td>
|
|
129
|
+
<td align="center"><a href="https://letstri.dev/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Valerii Strilets"><img src="https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&v=4" width="139" alt="Valerii Strilets"/><br />Valerii Strilets</a></td>
|
|
130
|
+
<td align="center"><a href="https://blacklight.sh/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Kyle Mistele"><img src="https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&v=4" width="139" alt="Kyle Mistele"/><br />Kyle Mistele</a></td>
|
|
131
|
+
<td align="center"><a href="https://github.com/christ12938?ref=middleapi&utm_source=middleapi&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>
|
|
132
|
+
</tr>
|
|
133
|
+
<tr>
|
|
134
|
+
<td align="center"><a href="https://github.com/Ryanjso?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Ryan Soderberg"><img src="https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&v=4" width="139" alt="Ryan Soderberg"/><br />Ryan Soderberg</a></td>
|
|
135
|
+
<td align="center"><a href="https://github.com/itigoore01?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="shota"><img src="https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&v=4" width="139" alt="shota"/><br />shota</a></td>
|
|
136
|
+
<td align="center"><a href="https://github.com/ellis-driscoll?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Ellis Driscoll"><img src="https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&v=4" width="139" alt="Ellis Driscoll"/><br />Ellis Driscoll</a></td>
|
|
137
|
+
<td align="center"><a href="https://github.com/hoangbn?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Hoang Nguyen"><img src="https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&v=4" width="139" alt="Hoang Nguyen"/><br />Hoang Nguyen</a></td>
|
|
138
|
+
<td align="center"><a href="https://opencollective.com/guest-ac41de3b?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Orestis Ioannou"><img src="https://images.opencollective.com/guest-ac41de3b/avatar/460.png" width="139" alt="Orestis Ioannou"/><br />Orestis Ioannou</a></td>
|
|
139
|
+
<td align="center"><a href="https://automatio.ai/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Stefan Smiljkovic"><img src="https://avatars.githubusercontent.com/u/1984909?u=b7bf5bc40ed49df3c22f69d2da7b8d78709c49ed&v=4" width="139" alt="Stefan Smiljkovic"/><br />Stefan Smiljkovic</a></td>
|
|
140
|
+
</tr>
|
|
141
|
+
</table>
|
|
142
|
+
|
|
143
|
+
### Backers
|
|
144
|
+
|
|
145
|
+
<table>
|
|
146
|
+
<tr>
|
|
147
|
+
<td align="center"><a href="https://github.com/rhinodavid?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="David Walsh"><img src="https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&v=4" width="119" alt="David Walsh"/><br />David Walsh</a></td>
|
|
148
|
+
<td align="center"><a href="https://github.com/IPv4Addr?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="IPv4Addr"><img src="https://avatars.githubusercontent.com/u/100147665?u=59996b72f69bb53063cb7e9ff8b8f898616cd94d&v=4" width="119" alt="IPv4Addr"/><br />IPv4Addr</a></td>
|
|
149
|
+
<td align="center"><a href="https://robbevaes.be/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Robbe Vaes"><img src="https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&v=4" width="119" alt="Robbe Vaes"/><br />Robbe Vaes</a></td>
|
|
150
|
+
<td align="center"><a href="https://github.com/aidansunbury?ref=middleapi&utm_source=middleapi&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>
|
|
151
|
+
<td align="center"><a href="https://github.com/soonoo?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="soonoo"><img src="https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&v=4" width="119" alt="soonoo"/><br />soonoo</a></td>
|
|
152
|
+
<td align="center"><a href="https://kevinporten.dev/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Kevin Porten"><img src="https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&v=4" width="119" alt="Kevin Porten"/><br />Kevin Porten</a></td>
|
|
153
|
+
<td align="center"><a href="https://github.com/pumpkinlink?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Denis"><img src="https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&v=4" width="119" alt="Denis"/><br />Denis</a></td>
|
|
154
|
+
</tr>
|
|
155
|
+
<tr>
|
|
156
|
+
<td align="center"><a href="https://github.com/christopher-kapic?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Christopher Kapic"><img src="https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&v=4" width="119" alt="Christopher Kapic"/><br />Christopher Kapic</a></td>
|
|
157
|
+
<td align="center"><a href="http://ballingt.com/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Tom Ballinger"><img src="https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&v=4" width="119" alt="Tom Ballinger"/><br />Tom Ballinger</a></td>
|
|
158
|
+
<td align="center"><a href="https://lee-sam.com/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Sam"><img src="https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&v=4" width="119" alt="Sam"/><br />Sam</a></td>
|
|
159
|
+
<td align="center"><a href="https://github.com/Titoine?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Titoine"><img src="https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&v=4" width="119" alt="Titoine"/><br />Titoine</a></td>
|
|
160
|
+
<td align="center"><a href="https://rigtch.fm/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Igor Makowski"><img src="https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&v=4" width="119" alt="Igor Makowski"/><br />Igor Makowski</a></td>
|
|
161
|
+
<td align="center"><a href="https://blog.cwang.io/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="hanayashiki"><img src="https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&v=4" width="119" alt="hanayashiki"/><br />hanayashiki</a></td>
|
|
162
|
+
<td align="center"><a href="https://dubinets.io/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Lev Dubinets"><img src="https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&v=4" width="119" alt="Lev Dubinets"/><br />Lev Dubinets</a></td>
|
|
163
|
+
</tr>
|
|
164
|
+
<tr>
|
|
165
|
+
<td align="center"><a href="https://kellychan.im/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Kelly Peilin Chan"><img src="https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&v=4" width="119" alt="Kelly Peilin Chan"/><br />Kelly Peilin Chan</a></td>
|
|
166
|
+
<td align="center"><a href="https://guyariely.com/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Guy Ariely"><img src="https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&v=4" width="119" alt="Guy Ariely"/><br />Guy Ariely</a></td>
|
|
167
|
+
<td align="center"><a href="https://paulsenon.com/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="PaulSenon"><img src="https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&v=4" width="119" alt="PaulSenon"/><br />PaulSenon</a></td>
|
|
168
|
+
<td align="center"><a href="https://piscis.dev/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Alex"><img src="https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&v=4" width="119" alt="Alex"/><br />Alex</a></td>
|
|
169
|
+
<td align="center"><a href="https://opensource.gubanov.eu/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Andrey Gubanov"><img src="https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&v=4" width="119" alt="Andrey Gubanov"/><br />Andrey Gubanov</a></td>
|
|
170
|
+
</tr>
|
|
171
|
+
</table>
|
|
172
|
+
|
|
173
|
+
With thanks to [36 past sponsors](https://htmlpreview.github.io/?https://github.com/middleapi/static/blob/main/sponsors.svg) who helped get oRPC here.
|
|
174
|
+
|
|
175
|
+
## References
|
|
176
|
+
|
|
177
|
+
oRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to:
|
|
178
|
+
|
|
179
|
+
- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs.
|
|
180
|
+
- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set.
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
Distributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Promisable } from '@orpc/shared';
|
|
2
|
+
import { Locker, LockCallbackOptions, LockOptions } from '../index.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Options shared by every Redis-backed locker adapter.
|
|
6
|
+
*
|
|
7
|
+
* @see {@link https://orpc.dev/docs/helpers/lock#adapters | Lock Helpers - Adapters}
|
|
8
|
+
*/
|
|
9
|
+
interface BaseRedisLockerOptions {
|
|
10
|
+
/**
|
|
11
|
+
* The prefix to use for Redis keys.
|
|
12
|
+
*
|
|
13
|
+
* @default ''
|
|
14
|
+
*/
|
|
15
|
+
prefix?: string;
|
|
16
|
+
/**
|
|
17
|
+
* How long a lock is held before it expires automatically, in milliseconds.
|
|
18
|
+
* Guards against holders that never release the lock, such as a crashed process.
|
|
19
|
+
* Can be overridden per call.
|
|
20
|
+
*/
|
|
21
|
+
ttl: number;
|
|
22
|
+
/**
|
|
23
|
+
* How long to wait for a lock to become available, in milliseconds.
|
|
24
|
+
* Can be overridden per call.
|
|
25
|
+
*
|
|
26
|
+
* @default 10000
|
|
27
|
+
*/
|
|
28
|
+
timeout?: number;
|
|
29
|
+
/**
|
|
30
|
+
* How long to wait between acquisition attempts while the lock
|
|
31
|
+
* is held by someone else, in milliseconds.
|
|
32
|
+
*
|
|
33
|
+
* @default 100
|
|
34
|
+
*/
|
|
35
|
+
retryInterval?: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Base class for Redis-backed locker adapters. It owns the key naming, the lock
|
|
39
|
+
* token, the acquisition loop, and the atomic Lua release, so every adapter built
|
|
40
|
+
* on it can share locks with the others regardless of the Redis client in use.
|
|
41
|
+
*
|
|
42
|
+
* Extend it and implement the abstract methods to support another Redis client.
|
|
43
|
+
*
|
|
44
|
+
* @see {@link https://orpc.dev/docs/helpers/lock#adapters | Lock Helpers - Adapters}
|
|
45
|
+
*/
|
|
46
|
+
declare abstract class BaseRedisLocker implements Locker {
|
|
47
|
+
protected readonly prefix: string;
|
|
48
|
+
protected readonly ttl: number;
|
|
49
|
+
protected readonly timeout: number;
|
|
50
|
+
protected readonly retryInterval: number;
|
|
51
|
+
constructor(options: BaseRedisLockerOptions);
|
|
52
|
+
/**
|
|
53
|
+
* Sets `key` to `token` only when it does not exist yet, with an expiry
|
|
54
|
+
* of `ttl` milliseconds (`SET key token NX PX ttl`), and resolves with
|
|
55
|
+
* whether the key was set.
|
|
56
|
+
*/
|
|
57
|
+
protected abstract acquire(key: string, token: string, ttl: number): Promise<boolean>;
|
|
58
|
+
/**
|
|
59
|
+
* Runs a Lua script (`EVAL script numkeys key [key ...] arg [arg ...]`).
|
|
60
|
+
*/
|
|
61
|
+
protected abstract evalScript(script: string, keys: string[], args: string[]): Promise<unknown>;
|
|
62
|
+
lock<T>(key: string, fn: (options: LockCallbackOptions) => Promisable<T>, options?: LockOptions): Promise<T>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export { BaseRedisLocker };
|
|
66
|
+
export type { BaseRedisLockerOptions };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Promisable } from '@orpc/shared';
|
|
2
|
+
import { Locker, LockCallbackOptions, LockOptions } from '../index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Options shared by every Redis-backed locker adapter.
|
|
6
|
+
*
|
|
7
|
+
* @see {@link https://orpc.dev/docs/helpers/lock#adapters | Lock Helpers - Adapters}
|
|
8
|
+
*/
|
|
9
|
+
interface BaseRedisLockerOptions {
|
|
10
|
+
/**
|
|
11
|
+
* The prefix to use for Redis keys.
|
|
12
|
+
*
|
|
13
|
+
* @default ''
|
|
14
|
+
*/
|
|
15
|
+
prefix?: string;
|
|
16
|
+
/**
|
|
17
|
+
* How long a lock is held before it expires automatically, in milliseconds.
|
|
18
|
+
* Guards against holders that never release the lock, such as a crashed process.
|
|
19
|
+
* Can be overridden per call.
|
|
20
|
+
*/
|
|
21
|
+
ttl: number;
|
|
22
|
+
/**
|
|
23
|
+
* How long to wait for a lock to become available, in milliseconds.
|
|
24
|
+
* Can be overridden per call.
|
|
25
|
+
*
|
|
26
|
+
* @default 10000
|
|
27
|
+
*/
|
|
28
|
+
timeout?: number;
|
|
29
|
+
/**
|
|
30
|
+
* How long to wait between acquisition attempts while the lock
|
|
31
|
+
* is held by someone else, in milliseconds.
|
|
32
|
+
*
|
|
33
|
+
* @default 100
|
|
34
|
+
*/
|
|
35
|
+
retryInterval?: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Base class for Redis-backed locker adapters. It owns the key naming, the lock
|
|
39
|
+
* token, the acquisition loop, and the atomic Lua release, so every adapter built
|
|
40
|
+
* on it can share locks with the others regardless of the Redis client in use.
|
|
41
|
+
*
|
|
42
|
+
* Extend it and implement the abstract methods to support another Redis client.
|
|
43
|
+
*
|
|
44
|
+
* @see {@link https://orpc.dev/docs/helpers/lock#adapters | Lock Helpers - Adapters}
|
|
45
|
+
*/
|
|
46
|
+
declare abstract class BaseRedisLocker implements Locker {
|
|
47
|
+
protected readonly prefix: string;
|
|
48
|
+
protected readonly ttl: number;
|
|
49
|
+
protected readonly timeout: number;
|
|
50
|
+
protected readonly retryInterval: number;
|
|
51
|
+
constructor(options: BaseRedisLockerOptions);
|
|
52
|
+
/**
|
|
53
|
+
* Sets `key` to `token` only when it does not exist yet, with an expiry
|
|
54
|
+
* of `ttl` milliseconds (`SET key token NX PX ttl`), and resolves with
|
|
55
|
+
* whether the key was set.
|
|
56
|
+
*/
|
|
57
|
+
protected abstract acquire(key: string, token: string, ttl: number): Promise<boolean>;
|
|
58
|
+
/**
|
|
59
|
+
* Runs a Lua script (`EVAL script numkeys key [key ...] arg [arg ...]`).
|
|
60
|
+
*/
|
|
61
|
+
protected abstract evalScript(script: string, keys: string[], args: string[]): Promise<unknown>;
|
|
62
|
+
lock<T>(key: string, fn: (options: LockCallbackOptions) => Promisable<T>, options?: LockOptions): Promise<T>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export { BaseRedisLocker };
|
|
66
|
+
export type { BaseRedisLockerOptions };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { sleep } from '@orpc/shared';
|
|
2
|
+
import { L as LockTimeoutError } from '../shared/experimental-lock.CoYbYjCJ.mjs';
|
|
3
|
+
|
|
4
|
+
const RELEASE_LOCK_SCRIPT = `
|
|
5
|
+
if redis.call('GET', KEYS[1]) == ARGV[1] then
|
|
6
|
+
return redis.call('DEL', KEYS[1])
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
return 0
|
|
10
|
+
`;
|
|
11
|
+
class BaseRedisLocker {
|
|
12
|
+
prefix;
|
|
13
|
+
ttl;
|
|
14
|
+
timeout;
|
|
15
|
+
retryInterval;
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.prefix = options.prefix ?? "";
|
|
18
|
+
this.ttl = options.ttl;
|
|
19
|
+
this.timeout = options.timeout ?? 1e4;
|
|
20
|
+
this.retryInterval = options.retryInterval ?? 100;
|
|
21
|
+
}
|
|
22
|
+
async lock(key, fn, options = {}) {
|
|
23
|
+
const prefixedKey = `${this.prefix}${key}`;
|
|
24
|
+
const ttl = options.ttl ?? this.ttl;
|
|
25
|
+
const deadline = Date.now() + (options.timeout ?? this.timeout);
|
|
26
|
+
const token = crypto.randomUUID();
|
|
27
|
+
let waited = false;
|
|
28
|
+
options.signal?.throwIfAborted();
|
|
29
|
+
while (!await this.acquire(prefixedKey, token, ttl)) {
|
|
30
|
+
const remaining = deadline - Date.now();
|
|
31
|
+
if (remaining <= 0) {
|
|
32
|
+
throw new LockTimeoutError(key);
|
|
33
|
+
}
|
|
34
|
+
waited = true;
|
|
35
|
+
await sleep(Math.min(this.retryInterval, remaining), { signal: options.signal });
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
return await fn({ waited });
|
|
39
|
+
} finally {
|
|
40
|
+
await this.evalScript(RELEASE_LOCK_SCRIPT, [prefixedKey], [token]);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { BaseRedisLocker };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Promisable } from '@orpc/shared';
|
|
2
|
+
import { Locker, LockCallbackOptions, LockOptions } from '../index.mjs';
|
|
3
|
+
|
|
4
|
+
interface MemoryLockerOptions {
|
|
5
|
+
/**
|
|
6
|
+
* How long a lock is held before it expires automatically, in milliseconds.
|
|
7
|
+
* Guards against holders that never release the lock.
|
|
8
|
+
* Can be overridden per call.
|
|
9
|
+
*
|
|
10
|
+
* @default undefined (held until released)
|
|
11
|
+
*/
|
|
12
|
+
ttl?: number;
|
|
13
|
+
/**
|
|
14
|
+
* How long to wait for a lock to become available, in milliseconds.
|
|
15
|
+
* Can be overridden per call.
|
|
16
|
+
*
|
|
17
|
+
* @default 10000
|
|
18
|
+
*/
|
|
19
|
+
timeout?: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Locker adapter backed by in-memory storage, so locks are only shared within
|
|
23
|
+
* the current process. Waiters acquire the lock in order, without polling.
|
|
24
|
+
*
|
|
25
|
+
* @see {@link https://orpc.dev/docs/helpers/lock#adapters | Lock Helpers - Adapters}
|
|
26
|
+
*/
|
|
27
|
+
declare class MemoryLocker implements Locker {
|
|
28
|
+
private readonly ttl;
|
|
29
|
+
private readonly timeout;
|
|
30
|
+
private readonly entries;
|
|
31
|
+
constructor(options?: MemoryLockerOptions);
|
|
32
|
+
lock<T>(key: string, fn: (options: LockCallbackOptions) => Promisable<T>, options?: LockOptions): Promise<T>;
|
|
33
|
+
private wait;
|
|
34
|
+
private release;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { MemoryLocker };
|
|
38
|
+
export type { MemoryLockerOptions };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Promisable } from '@orpc/shared';
|
|
2
|
+
import { Locker, LockCallbackOptions, LockOptions } from '../index.js';
|
|
3
|
+
|
|
4
|
+
interface MemoryLockerOptions {
|
|
5
|
+
/**
|
|
6
|
+
* How long a lock is held before it expires automatically, in milliseconds.
|
|
7
|
+
* Guards against holders that never release the lock.
|
|
8
|
+
* Can be overridden per call.
|
|
9
|
+
*
|
|
10
|
+
* @default undefined (held until released)
|
|
11
|
+
*/
|
|
12
|
+
ttl?: number;
|
|
13
|
+
/**
|
|
14
|
+
* How long to wait for a lock to become available, in milliseconds.
|
|
15
|
+
* Can be overridden per call.
|
|
16
|
+
*
|
|
17
|
+
* @default 10000
|
|
18
|
+
*/
|
|
19
|
+
timeout?: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Locker adapter backed by in-memory storage, so locks are only shared within
|
|
23
|
+
* the current process. Waiters acquire the lock in order, without polling.
|
|
24
|
+
*
|
|
25
|
+
* @see {@link https://orpc.dev/docs/helpers/lock#adapters | Lock Helpers - Adapters}
|
|
26
|
+
*/
|
|
27
|
+
declare class MemoryLocker implements Locker {
|
|
28
|
+
private readonly ttl;
|
|
29
|
+
private readonly timeout;
|
|
30
|
+
private readonly entries;
|
|
31
|
+
constructor(options?: MemoryLockerOptions);
|
|
32
|
+
lock<T>(key: string, fn: (options: LockCallbackOptions) => Promisable<T>, options?: LockOptions): Promise<T>;
|
|
33
|
+
private wait;
|
|
34
|
+
private release;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { MemoryLocker };
|
|
38
|
+
export type { MemoryLockerOptions };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { promiseWithResolvers } from '@orpc/shared';
|
|
2
|
+
import { L as LockTimeoutError } from '../shared/experimental-lock.CoYbYjCJ.mjs';
|
|
3
|
+
|
|
4
|
+
class MemoryLocker {
|
|
5
|
+
ttl;
|
|
6
|
+
timeout;
|
|
7
|
+
entries = /* @__PURE__ */ new Map();
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.ttl = options.ttl;
|
|
10
|
+
this.timeout = options.timeout ?? 1e4;
|
|
11
|
+
}
|
|
12
|
+
async lock(key, fn, options = {}) {
|
|
13
|
+
const ttl = options.ttl ?? this.ttl;
|
|
14
|
+
const timeout = options.timeout ?? this.timeout;
|
|
15
|
+
const token = {};
|
|
16
|
+
options.signal?.throwIfAborted();
|
|
17
|
+
let entry = this.entries.get(key);
|
|
18
|
+
const waited = entry !== void 0;
|
|
19
|
+
if (entry) {
|
|
20
|
+
await this.wait(key, entry, token, timeout, options.signal);
|
|
21
|
+
} else {
|
|
22
|
+
entry = { holder: token, waiters: /* @__PURE__ */ new Set() };
|
|
23
|
+
this.entries.set(key, entry);
|
|
24
|
+
}
|
|
25
|
+
if (ttl !== void 0) {
|
|
26
|
+
entry.expiry = setTimeout(() => this.release(key, token), ttl);
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
return await fn({ waited });
|
|
30
|
+
} finally {
|
|
31
|
+
this.release(key, token);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
wait(key, entry, token, timeout, signal) {
|
|
35
|
+
if (timeout <= 0) {
|
|
36
|
+
return Promise.reject(new LockTimeoutError(key));
|
|
37
|
+
}
|
|
38
|
+
const { promise, resolve, reject } = promiseWithResolvers();
|
|
39
|
+
const waiter = { token, resolve };
|
|
40
|
+
entry.waiters.add(waiter);
|
|
41
|
+
const fail = (reason) => {
|
|
42
|
+
entry.waiters.delete(waiter);
|
|
43
|
+
reject(reason);
|
|
44
|
+
};
|
|
45
|
+
const timer = setTimeout(() => fail(new LockTimeoutError(key)), timeout);
|
|
46
|
+
const abortListener = () => fail(signal?.reason);
|
|
47
|
+
signal?.addEventListener("abort", abortListener, { once: true });
|
|
48
|
+
return promise.finally(() => {
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
signal?.removeEventListener("abort", abortListener);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
release(key, token) {
|
|
54
|
+
const entry = this.entries.get(key);
|
|
55
|
+
if (entry?.holder !== token) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
clearTimeout(entry.expiry);
|
|
59
|
+
const [next] = entry.waiters;
|
|
60
|
+
if (!next) {
|
|
61
|
+
this.entries.delete(key);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
entry.waiters.delete(next);
|
|
65
|
+
entry.holder = next.token;
|
|
66
|
+
next.resolve();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export { MemoryLocker };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { RedisClientType, RedisClusterType } from 'redis';
|
|
2
|
+
import { BaseRedisLocker, BaseRedisLockerOptions } from './base-redis.mjs';
|
|
3
|
+
import '@orpc/shared';
|
|
4
|
+
import '../shared/experimental-lock.D48ZxIcJ.mjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Locker adapter for Redis. Acquires locks with `SET NX PX` and releases them
|
|
8
|
+
* atomically with a Lua script, so every instance using the same server shares
|
|
9
|
+
* the same locks. Works with both standalone and cluster clients.
|
|
10
|
+
*
|
|
11
|
+
* @see {@link https://orpc.dev/docs/helpers/lock#adapters | Lock Helpers - Adapters}
|
|
12
|
+
*/
|
|
13
|
+
declare class RedisLocker extends BaseRedisLocker {
|
|
14
|
+
private readonly redis;
|
|
15
|
+
constructor(redis: RedisClientType<any, any, any, any, any> | RedisClusterType<any, any, any, any, any>, options: BaseRedisLockerOptions);
|
|
16
|
+
protected acquire(key: string, token: string, ttl: number): Promise<boolean>;
|
|
17
|
+
protected evalScript(script: string, keys: string[], args: string[]): Promise<unknown>;
|
|
18
|
+
private connectIfNeeded;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { RedisLocker };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { RedisClientType, RedisClusterType } from 'redis';
|
|
2
|
+
import { BaseRedisLocker, BaseRedisLockerOptions } from './base-redis.js';
|
|
3
|
+
import '@orpc/shared';
|
|
4
|
+
import '../shared/experimental-lock.D48ZxIcJ.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Locker adapter for Redis. Acquires locks with `SET NX PX` and releases them
|
|
8
|
+
* atomically with a Lua script, so every instance using the same server shares
|
|
9
|
+
* the same locks. Works with both standalone and cluster clients.
|
|
10
|
+
*
|
|
11
|
+
* @see {@link https://orpc.dev/docs/helpers/lock#adapters | Lock Helpers - Adapters}
|
|
12
|
+
*/
|
|
13
|
+
declare class RedisLocker extends BaseRedisLocker {
|
|
14
|
+
private readonly redis;
|
|
15
|
+
constructor(redis: RedisClientType<any, any, any, any, any> | RedisClusterType<any, any, any, any, any>, options: BaseRedisLockerOptions);
|
|
16
|
+
protected acquire(key: string, token: string, ttl: number): Promise<boolean>;
|
|
17
|
+
protected evalScript(script: string, keys: string[], args: string[]): Promise<unknown>;
|
|
18
|
+
private connectIfNeeded;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { RedisLocker };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { BaseRedisLocker } from './base-redis.mjs';
|
|
2
|
+
import '@orpc/shared';
|
|
3
|
+
import '../shared/experimental-lock.CoYbYjCJ.mjs';
|
|
4
|
+
|
|
5
|
+
class RedisLocker extends BaseRedisLocker {
|
|
6
|
+
constructor(redis, options) {
|
|
7
|
+
super(options);
|
|
8
|
+
this.redis = redis;
|
|
9
|
+
}
|
|
10
|
+
async acquire(key, token, ttl) {
|
|
11
|
+
await this.connectIfNeeded();
|
|
12
|
+
const result = await this.redis.set(key, token, {
|
|
13
|
+
condition: "NX",
|
|
14
|
+
expiration: { type: "PX", value: ttl }
|
|
15
|
+
});
|
|
16
|
+
return result === "OK";
|
|
17
|
+
}
|
|
18
|
+
async evalScript(script, keys, args) {
|
|
19
|
+
await this.connectIfNeeded();
|
|
20
|
+
return await this.redis.eval(script, { keys, arguments: args });
|
|
21
|
+
}
|
|
22
|
+
async connectIfNeeded() {
|
|
23
|
+
if (!this.redis.isOpen) {
|
|
24
|
+
await this.redis.connect();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { RedisLocker };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Redis } from '@upstash/redis';
|
|
2
|
+
import { BaseRedisLocker, BaseRedisLockerOptions } from './base-redis.mjs';
|
|
3
|
+
import '@orpc/shared';
|
|
4
|
+
import '../shared/experimental-lock.D48ZxIcJ.mjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Locker adapter for Upstash Redis. Acquires locks with `SET NX PX` and releases
|
|
8
|
+
* them atomically with a Lua script, so it shares locks with `RedisLocker`.
|
|
9
|
+
* A good fit for serverless and edge runtimes.
|
|
10
|
+
*
|
|
11
|
+
* @see {@link https://orpc.dev/docs/helpers/lock#adapters | Lock Helpers - Adapters}
|
|
12
|
+
*/
|
|
13
|
+
declare class UpstashLocker extends BaseRedisLocker {
|
|
14
|
+
private readonly redis;
|
|
15
|
+
constructor(redis: Redis, options: BaseRedisLockerOptions);
|
|
16
|
+
protected acquire(key: string, token: string, ttl: number): Promise<boolean>;
|
|
17
|
+
protected evalScript(script: string, keys: string[], args: string[]): Promise<unknown>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { UpstashLocker };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Redis } from '@upstash/redis';
|
|
2
|
+
import { BaseRedisLocker, BaseRedisLockerOptions } from './base-redis.js';
|
|
3
|
+
import '@orpc/shared';
|
|
4
|
+
import '../shared/experimental-lock.D48ZxIcJ.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Locker adapter for Upstash Redis. Acquires locks with `SET NX PX` and releases
|
|
8
|
+
* them atomically with a Lua script, so it shares locks with `RedisLocker`.
|
|
9
|
+
* A good fit for serverless and edge runtimes.
|
|
10
|
+
*
|
|
11
|
+
* @see {@link https://orpc.dev/docs/helpers/lock#adapters | Lock Helpers - Adapters}
|
|
12
|
+
*/
|
|
13
|
+
declare class UpstashLocker extends BaseRedisLocker {
|
|
14
|
+
private readonly redis;
|
|
15
|
+
constructor(redis: Redis, options: BaseRedisLockerOptions);
|
|
16
|
+
protected acquire(key: string, token: string, ttl: number): Promise<boolean>;
|
|
17
|
+
protected evalScript(script: string, keys: string[], args: string[]): Promise<unknown>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { UpstashLocker };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { BaseRedisLocker } from './base-redis.mjs';
|
|
2
|
+
import '@orpc/shared';
|
|
3
|
+
import '../shared/experimental-lock.CoYbYjCJ.mjs';
|
|
4
|
+
|
|
5
|
+
class UpstashLocker extends BaseRedisLocker {
|
|
6
|
+
constructor(redis, options) {
|
|
7
|
+
super(options);
|
|
8
|
+
this.redis = redis;
|
|
9
|
+
}
|
|
10
|
+
async acquire(key, token, ttl) {
|
|
11
|
+
const result = await this.redis.set(key, token, { nx: true, px: ttl });
|
|
12
|
+
return result === "OK";
|
|
13
|
+
}
|
|
14
|
+
async evalScript(script, keys, args) {
|
|
15
|
+
return await this.redis.eval(script, keys, args);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { UpstashLocker };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Context, MiddlewareOptions, Middleware } from '@orpc/server';
|
|
2
|
+
import { Value, Promisable } from '@orpc/shared';
|
|
3
|
+
import { L as Locker } from './shared/experimental-lock.D48ZxIcJ.mjs';
|
|
4
|
+
export { a as LockCallbackOptions, b as LockOptions } from './shared/experimental-lock.D48ZxIcJ.mjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Thrown when a lock cannot be acquired before the timeout elapses.
|
|
8
|
+
*
|
|
9
|
+
* @see {@link https://orpc.dev/docs/helpers/lock#options | Lock Helpers - Options}
|
|
10
|
+
*/
|
|
11
|
+
declare class LockTimeoutError extends Error {
|
|
12
|
+
/**
|
|
13
|
+
* The key of the lock that could not be acquired.
|
|
14
|
+
*/
|
|
15
|
+
readonly key: string;
|
|
16
|
+
constructor(key: string);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
declare const LOCK_MIDDLEWARE_CONTEXT_SYMBOL: unique symbol;
|
|
20
|
+
interface LockMiddlewareContext {
|
|
21
|
+
[LOCK_MIDDLEWARE_CONTEXT_SYMBOL]?: {
|
|
22
|
+
/**
|
|
23
|
+
* The locks held in this request, mainly for deduplication purposes
|
|
24
|
+
*/
|
|
25
|
+
held: {
|
|
26
|
+
locker: Locker;
|
|
27
|
+
key: string;
|
|
28
|
+
waited: boolean;
|
|
29
|
+
}[];
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
interface LockMiddlewareOutContext {
|
|
33
|
+
/**
|
|
34
|
+
* Whether the lock was acquired only after waiting for another holder to release it.
|
|
35
|
+
* `false` means the lock was acquired immediately.
|
|
36
|
+
*/
|
|
37
|
+
['lock/waited']: boolean;
|
|
38
|
+
}
|
|
39
|
+
interface LockMiddlewareOptions<TInContext extends Context, TInput> {
|
|
40
|
+
/**
|
|
41
|
+
* The locker to acquire the lock from
|
|
42
|
+
*/
|
|
43
|
+
locker: Value<Promisable<Locker>, [options: MiddlewareOptions<TInContext, unknown, Record<never, never>>, input: TInput]>;
|
|
44
|
+
/**
|
|
45
|
+
* The key to identify the work that must not run concurrently
|
|
46
|
+
*/
|
|
47
|
+
key: Value<Promisable<string>, [options: MiddlewareOptions<TInContext, unknown, Record<never, never>>, input: TInput]>;
|
|
48
|
+
/**
|
|
49
|
+
* How long the lock is held before it expires automatically, in milliseconds.
|
|
50
|
+
*
|
|
51
|
+
* @default the adapter default
|
|
52
|
+
*/
|
|
53
|
+
ttl?: Value<Promisable<number | undefined>, [options: MiddlewareOptions<TInContext, unknown, Record<never, never>>, input: TInput]>;
|
|
54
|
+
/**
|
|
55
|
+
* How long to wait for the lock to become available, in milliseconds.
|
|
56
|
+
*
|
|
57
|
+
* @default the adapter default
|
|
58
|
+
*/
|
|
59
|
+
timeout?: Value<Promisable<number | undefined>, [options: MiddlewareOptions<TInContext, unknown, Record<never, never>>, input: TInput]>;
|
|
60
|
+
/**
|
|
61
|
+
* If your lock middleware is used multiple times
|
|
62
|
+
* or you invoke a procedure inside another procedure (shared the same context) that also has
|
|
63
|
+
* lock middleware **with the same locker and key**, this option
|
|
64
|
+
* will ensure that the lock is only acquired once per request,
|
|
65
|
+
* instead of waiting for itself until the timeout elapses.
|
|
66
|
+
*
|
|
67
|
+
* @default true
|
|
68
|
+
*/
|
|
69
|
+
dedupe?: boolean;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Creates a middleware that runs oRPC procedures under a lock, so that calls
|
|
73
|
+
* sharing the same key never run concurrently. Rejects with a `CONFLICT` error
|
|
74
|
+
* when the lock cannot be acquired before the timeout elapses.
|
|
75
|
+
*
|
|
76
|
+
* @see {@link https://orpc.dev/docs/helpers/lock#lock-middleware | Lock Helpers - Lock Middleware}
|
|
77
|
+
*/
|
|
78
|
+
declare function lock<TInContext extends Context, TInput>({ dedupe, ...options }: LockMiddlewareOptions<TInContext, TInput>): Middleware<TInContext, LockMiddlewareOutContext, TInput, any, object>;
|
|
79
|
+
|
|
80
|
+
export { LOCK_MIDDLEWARE_CONTEXT_SYMBOL, LockTimeoutError, Locker, lock };
|
|
81
|
+
export type { LockMiddlewareContext, LockMiddlewareOptions, LockMiddlewareOutContext };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Context, MiddlewareOptions, Middleware } from '@orpc/server';
|
|
2
|
+
import { Value, Promisable } from '@orpc/shared';
|
|
3
|
+
import { L as Locker } from './shared/experimental-lock.D48ZxIcJ.js';
|
|
4
|
+
export { a as LockCallbackOptions, b as LockOptions } from './shared/experimental-lock.D48ZxIcJ.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Thrown when a lock cannot be acquired before the timeout elapses.
|
|
8
|
+
*
|
|
9
|
+
* @see {@link https://orpc.dev/docs/helpers/lock#options | Lock Helpers - Options}
|
|
10
|
+
*/
|
|
11
|
+
declare class LockTimeoutError extends Error {
|
|
12
|
+
/**
|
|
13
|
+
* The key of the lock that could not be acquired.
|
|
14
|
+
*/
|
|
15
|
+
readonly key: string;
|
|
16
|
+
constructor(key: string);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
declare const LOCK_MIDDLEWARE_CONTEXT_SYMBOL: unique symbol;
|
|
20
|
+
interface LockMiddlewareContext {
|
|
21
|
+
[LOCK_MIDDLEWARE_CONTEXT_SYMBOL]?: {
|
|
22
|
+
/**
|
|
23
|
+
* The locks held in this request, mainly for deduplication purposes
|
|
24
|
+
*/
|
|
25
|
+
held: {
|
|
26
|
+
locker: Locker;
|
|
27
|
+
key: string;
|
|
28
|
+
waited: boolean;
|
|
29
|
+
}[];
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
interface LockMiddlewareOutContext {
|
|
33
|
+
/**
|
|
34
|
+
* Whether the lock was acquired only after waiting for another holder to release it.
|
|
35
|
+
* `false` means the lock was acquired immediately.
|
|
36
|
+
*/
|
|
37
|
+
['lock/waited']: boolean;
|
|
38
|
+
}
|
|
39
|
+
interface LockMiddlewareOptions<TInContext extends Context, TInput> {
|
|
40
|
+
/**
|
|
41
|
+
* The locker to acquire the lock from
|
|
42
|
+
*/
|
|
43
|
+
locker: Value<Promisable<Locker>, [options: MiddlewareOptions<TInContext, unknown, Record<never, never>>, input: TInput]>;
|
|
44
|
+
/**
|
|
45
|
+
* The key to identify the work that must not run concurrently
|
|
46
|
+
*/
|
|
47
|
+
key: Value<Promisable<string>, [options: MiddlewareOptions<TInContext, unknown, Record<never, never>>, input: TInput]>;
|
|
48
|
+
/**
|
|
49
|
+
* How long the lock is held before it expires automatically, in milliseconds.
|
|
50
|
+
*
|
|
51
|
+
* @default the adapter default
|
|
52
|
+
*/
|
|
53
|
+
ttl?: Value<Promisable<number | undefined>, [options: MiddlewareOptions<TInContext, unknown, Record<never, never>>, input: TInput]>;
|
|
54
|
+
/**
|
|
55
|
+
* How long to wait for the lock to become available, in milliseconds.
|
|
56
|
+
*
|
|
57
|
+
* @default the adapter default
|
|
58
|
+
*/
|
|
59
|
+
timeout?: Value<Promisable<number | undefined>, [options: MiddlewareOptions<TInContext, unknown, Record<never, never>>, input: TInput]>;
|
|
60
|
+
/**
|
|
61
|
+
* If your lock middleware is used multiple times
|
|
62
|
+
* or you invoke a procedure inside another procedure (shared the same context) that also has
|
|
63
|
+
* lock middleware **with the same locker and key**, this option
|
|
64
|
+
* will ensure that the lock is only acquired once per request,
|
|
65
|
+
* instead of waiting for itself until the timeout elapses.
|
|
66
|
+
*
|
|
67
|
+
* @default true
|
|
68
|
+
*/
|
|
69
|
+
dedupe?: boolean;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Creates a middleware that runs oRPC procedures under a lock, so that calls
|
|
73
|
+
* sharing the same key never run concurrently. Rejects with a `CONFLICT` error
|
|
74
|
+
* when the lock cannot be acquired before the timeout elapses.
|
|
75
|
+
*
|
|
76
|
+
* @see {@link https://orpc.dev/docs/helpers/lock#lock-middleware | Lock Helpers - Lock Middleware}
|
|
77
|
+
*/
|
|
78
|
+
declare function lock<TInContext extends Context, TInput>({ dedupe, ...options }: LockMiddlewareOptions<TInContext, TInput>): Middleware<TInContext, LockMiddlewareOutContext, TInput, any, object>;
|
|
79
|
+
|
|
80
|
+
export { LOCK_MIDDLEWARE_CONTEXT_SYMBOL, LockTimeoutError, Locker, lock };
|
|
81
|
+
export type { LockMiddlewareContext, LockMiddlewareOptions, LockMiddlewareOutContext };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { L as LockTimeoutError } from './shared/experimental-lock.CoYbYjCJ.mjs';
|
|
2
|
+
import { ORPCError } from '@orpc/server';
|
|
3
|
+
import { value, toArray } from '@orpc/shared';
|
|
4
|
+
|
|
5
|
+
const LOCK_MIDDLEWARE_CONTEXT_SYMBOL = Symbol.for("ORPC_LOCK_MIDDLEWARE_CONTEXT");
|
|
6
|
+
function lock({ dedupe = true, ...options }) {
|
|
7
|
+
return async function lock2(middlewareOptions, input) {
|
|
8
|
+
const [locker, key, ttl, timeout] = await Promise.all([
|
|
9
|
+
value(options.locker, middlewareOptions, input),
|
|
10
|
+
value(options.key, middlewareOptions, input),
|
|
11
|
+
value(options.ttl, middlewareOptions, input),
|
|
12
|
+
value(options.timeout, middlewareOptions, input)
|
|
13
|
+
]);
|
|
14
|
+
const middlewareContext = middlewareOptions.context[LOCK_MIDDLEWARE_CONTEXT_SYMBOL];
|
|
15
|
+
const held = middlewareContext?.held.find((l) => l.key === key && l.locker === locker);
|
|
16
|
+
if (dedupe && held) {
|
|
17
|
+
return middlewareOptions.next({
|
|
18
|
+
context: {
|
|
19
|
+
"lock/waited": held.waited
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
let acquired = false;
|
|
24
|
+
try {
|
|
25
|
+
return await locker.lock(key, ({ waited }) => {
|
|
26
|
+
acquired = true;
|
|
27
|
+
return middlewareOptions.next({
|
|
28
|
+
context: {
|
|
29
|
+
"lock/waited": waited,
|
|
30
|
+
[LOCK_MIDDLEWARE_CONTEXT_SYMBOL]: {
|
|
31
|
+
...middlewareContext,
|
|
32
|
+
held: [
|
|
33
|
+
...toArray(middlewareContext?.held),
|
|
34
|
+
{ locker, key, waited }
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}, { ttl, timeout, signal: middlewareOptions.signal });
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (!acquired && error instanceof LockTimeoutError) {
|
|
42
|
+
throw new ORPCError("CONFLICT", { cause: error });
|
|
43
|
+
}
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { LOCK_MIDDLEWARE_CONTEXT_SYMBOL, LockTimeoutError, lock };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class LockTimeoutError extends Error {
|
|
2
|
+
/**
|
|
3
|
+
* The key of the lock that could not be acquired.
|
|
4
|
+
*/
|
|
5
|
+
key;
|
|
6
|
+
constructor(key) {
|
|
7
|
+
super(`Timed out waiting for the lock of key "${key}"`);
|
|
8
|
+
this.name = "LockTimeoutError";
|
|
9
|
+
this.key = key;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { LockTimeoutError as L };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Promisable } from '@orpc/shared';
|
|
2
|
+
|
|
3
|
+
interface LockOptions {
|
|
4
|
+
/**
|
|
5
|
+
* How long the lock is held before it expires automatically, in milliseconds.
|
|
6
|
+
* Guards against holders that never release the lock, such as a crashed process.
|
|
7
|
+
*
|
|
8
|
+
* @default the adapter default
|
|
9
|
+
*/
|
|
10
|
+
ttl?: number;
|
|
11
|
+
/**
|
|
12
|
+
* How long to wait for the lock to become available, in milliseconds.
|
|
13
|
+
*
|
|
14
|
+
* @default the adapter default
|
|
15
|
+
*/
|
|
16
|
+
timeout?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Aborts waiting for the lock. Has no effect once the lock is acquired.
|
|
19
|
+
*/
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
}
|
|
22
|
+
interface LockCallbackOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Whether the lock was acquired only after waiting for another holder to release it.
|
|
25
|
+
* `false` means the lock was acquired immediately.
|
|
26
|
+
*/
|
|
27
|
+
waited: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Standard interface for running work under a lock, so that work sharing
|
|
31
|
+
* the same key never runs concurrently.
|
|
32
|
+
* Implement it for custom strategies, or use one of the provided adapters.
|
|
33
|
+
*
|
|
34
|
+
* @see {@link https://orpc.dev/docs/helpers/lock | Lock Helpers}
|
|
35
|
+
*/
|
|
36
|
+
interface Locker {
|
|
37
|
+
/**
|
|
38
|
+
* Runs `fn` while holding the lock for `key`, and releases the lock afterwards,
|
|
39
|
+
* even when `fn` throws.
|
|
40
|
+
*
|
|
41
|
+
* @throws {LockTimeoutError} when the lock cannot be acquired before the timeout elapses
|
|
42
|
+
*/
|
|
43
|
+
lock<T>(key: string, fn: (options: LockCallbackOptions) => Promisable<T>, options?: LockOptions): Promise<T>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type { Locker as L, LockCallbackOptions as a, LockOptions as b };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Promisable } from '@orpc/shared';
|
|
2
|
+
|
|
3
|
+
interface LockOptions {
|
|
4
|
+
/**
|
|
5
|
+
* How long the lock is held before it expires automatically, in milliseconds.
|
|
6
|
+
* Guards against holders that never release the lock, such as a crashed process.
|
|
7
|
+
*
|
|
8
|
+
* @default the adapter default
|
|
9
|
+
*/
|
|
10
|
+
ttl?: number;
|
|
11
|
+
/**
|
|
12
|
+
* How long to wait for the lock to become available, in milliseconds.
|
|
13
|
+
*
|
|
14
|
+
* @default the adapter default
|
|
15
|
+
*/
|
|
16
|
+
timeout?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Aborts waiting for the lock. Has no effect once the lock is acquired.
|
|
19
|
+
*/
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
}
|
|
22
|
+
interface LockCallbackOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Whether the lock was acquired only after waiting for another holder to release it.
|
|
25
|
+
* `false` means the lock was acquired immediately.
|
|
26
|
+
*/
|
|
27
|
+
waited: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Standard interface for running work under a lock, so that work sharing
|
|
31
|
+
* the same key never runs concurrently.
|
|
32
|
+
* Implement it for custom strategies, or use one of the provided adapters.
|
|
33
|
+
*
|
|
34
|
+
* @see {@link https://orpc.dev/docs/helpers/lock | Lock Helpers}
|
|
35
|
+
*/
|
|
36
|
+
interface Locker {
|
|
37
|
+
/**
|
|
38
|
+
* Runs `fn` while holding the lock for `key`, and releases the lock afterwards,
|
|
39
|
+
* even when `fn` throws.
|
|
40
|
+
*
|
|
41
|
+
* @throws {LockTimeoutError} when the lock cannot be acquired before the timeout elapses
|
|
42
|
+
*/
|
|
43
|
+
lock<T>(key: string, fn: (options: LockCallbackOptions) => Promisable<T>, options?: LockOptions): Promise<T>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type { Locker as L, LockCallbackOptions as a, LockOptions as b };
|
package/package.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@orpc/experimental-lock",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "2.0.0-beta.35",
|
|
5
|
+
"description": "Locking for oRPC procedures, with memory, Redis, and Upstash adapters",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"funding": [
|
|
8
|
+
"https://github.com/sponsors/dinwwwh",
|
|
9
|
+
"https://opencollective.com/middleapi"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://orpc.dev",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/middleapi/orpc.git",
|
|
15
|
+
"directory": "packages/lock"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"orpc",
|
|
19
|
+
"lock",
|
|
20
|
+
"locking",
|
|
21
|
+
"mutex",
|
|
22
|
+
"redis",
|
|
23
|
+
"upstash",
|
|
24
|
+
"middleware",
|
|
25
|
+
"api",
|
|
26
|
+
"typescript"
|
|
27
|
+
],
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"exports": {
|
|
30
|
+
"./package.json": "./package.json",
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.mts",
|
|
33
|
+
"import": "./dist/index.mjs",
|
|
34
|
+
"default": "./dist/index.mjs"
|
|
35
|
+
},
|
|
36
|
+
"./base-redis": {
|
|
37
|
+
"types": "./dist/adapters/base-redis.d.mts",
|
|
38
|
+
"import": "./dist/adapters/base-redis.mjs",
|
|
39
|
+
"default": "./dist/adapters/base-redis.mjs"
|
|
40
|
+
},
|
|
41
|
+
"./memory": {
|
|
42
|
+
"types": "./dist/adapters/memory.d.mts",
|
|
43
|
+
"import": "./dist/adapters/memory.mjs",
|
|
44
|
+
"default": "./dist/adapters/memory.mjs"
|
|
45
|
+
},
|
|
46
|
+
"./redis": {
|
|
47
|
+
"types": "./dist/adapters/redis.d.mts",
|
|
48
|
+
"import": "./dist/adapters/redis.mjs",
|
|
49
|
+
"default": "./dist/adapters/redis.mjs"
|
|
50
|
+
},
|
|
51
|
+
"./upstash": {
|
|
52
|
+
"types": "./dist/adapters/upstash.d.mts",
|
|
53
|
+
"import": "./dist/adapters/upstash.mjs",
|
|
54
|
+
"default": "./dist/adapters/upstash.mjs"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"files": [
|
|
58
|
+
"dist"
|
|
59
|
+
],
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"@upstash/redis": ">=1.35.6",
|
|
62
|
+
"redis": ">=6.0.0"
|
|
63
|
+
},
|
|
64
|
+
"peerDependenciesMeta": {
|
|
65
|
+
"@upstash/redis": {
|
|
66
|
+
"optional": true
|
|
67
|
+
},
|
|
68
|
+
"redis": {
|
|
69
|
+
"optional": true
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"dependencies": {
|
|
73
|
+
"@orpc/server": "2.0.0-beta.36",
|
|
74
|
+
"@orpc/shared": "2.0.0-beta.36"
|
|
75
|
+
},
|
|
76
|
+
"devDependencies": {
|
|
77
|
+
"@upstash/redis": "^1.38.2",
|
|
78
|
+
"redis": "^6.2.1"
|
|
79
|
+
},
|
|
80
|
+
"scripts": {
|
|
81
|
+
"type:check": "tsc -b"
|
|
82
|
+
}
|
|
83
|
+
}
|