diff --git a/README.md b/README.md index 86cb6d543..9897311b4 100644 --- a/README.md +++ b/README.md @@ -46,29 +46,30 @@ You can read the documentation [here](https://orpc.dev). **Built-in features** -| Package | Purpose | Docs | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | -| [@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) | -| [@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) | -| [@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) | +| Package | Purpose | Docs | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | +| [@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) | +| [@orpc/experimental-cache](https://www.npmjs.com/package/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](https://orpc.dev/docs/helpers/cache) | +| [@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) | +| [@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) | **Framework & ecosystem integrations** -| Package | Purpose | Docs | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [@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) | -| [@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) | -| [@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) | -| [@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) | -| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | -| [@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) | -| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | -| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | -| [@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) | -| [@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) | -| [@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), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | -| [@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) | +| Package | Purpose | Docs | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [@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) | +| [@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) | +| [@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) | +| [@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) | +| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | +| [@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) | +| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | +| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | +| [@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) | +| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | +| [@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) | **Observability** diff --git a/apps/content/docs/api-reference.mdx b/apps/content/docs/api-reference.mdx index c121ae601..96601b18f 100644 --- a/apps/content/docs/api-reference.mdx +++ b/apps/content/docs/api-reference.mdx @@ -32,6 +32,7 @@ For questions the reference does not answer, [oRPC on DeepWiki](https://deepwiki | Package | Purpose | Related Guides | | ------- | ------- | -------------- | +| [@orpc/experimental-cache](https://npmx.dev/package-docs/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](/docs/helpers/cache) | | [@orpc/publisher](https://npmx.dev/package-docs/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](/docs/helpers/publisher) | | [@orpc/ratelimit](https://npmx.dev/package-docs/@orpc/ratelimit) | Rate limiting with memory, Redis, and Upstash adapters. | [Rate Limit](/docs/helpers/ratelimit) | | [@orpc/hibernation](https://npmx.dev/package-docs/@orpc/hibernation) | Leverage Hibernation APIs like Cloudflare's WebSocket Hibernation. | [Hibernation](/docs/integrations/hibernation) | @@ -50,8 +51,8 @@ For questions the reference does not answer, [oRPC on DeepWiki](https://deepwiki | [@orpc/experimental-msw](https://npmx.dev/package-docs/@orpc/experimental-msw) | Mock procedures at the network level with typed MSW request handlers. | [MSW](/docs/integrations/msw) | | [@orpc/nest](https://npmx.dev/package-docs/@orpc/nest) | Implement your contract with NestJS. | [NestJS](/docs/integrations/nest) | | [@orpc/node](https://npmx.dev/package-docs/@orpc/node) | Node.js plugins for static file serving and large uploads. | [Static File](/docs/plugins/static-file), [Tmp File Upload](/docs/plugins/tmp-file-upload), [Batch Response Compression](/docs/plugins/batch-response-compression) | -| [@orpc/bun](https://npmx.dev/package-docs/@orpc/bun) | Bun Redis adapters for Publisher and Rate Limit. | [Publisher](/docs/helpers/publisher), [Rate Limit](/docs/helpers/ratelimit) | -| [@orpc/cloudflare](https://npmx.dev/package-docs/@orpc/cloudflare) | Adapters for Cloudflare Workers. | [Publisher](/docs/helpers/publisher), [Rate Limit](/docs/helpers/ratelimit), [Traces](/docs/integrations/cloudflare-traces) | +| [@orpc/bun](https://npmx.dev/package-docs/@orpc/bun) | Bun Redis adapters for Cache, Publisher, and Rate Limit. | [Cache](/docs/helpers/cache), [Publisher](/docs/helpers/publisher), [Rate Limit](/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://npmx.dev/package-docs/@orpc/cloudflare) | Adapters for Cloudflare Workers. | [Cache](/docs/helpers/cache), [Publisher](/docs/helpers/publisher), [Rate Limit](/docs/helpers/ratelimit), [Traces](/docs/integrations/cloudflare-traces) | | [@orpc/trpc](https://npmx.dev/package-docs/@orpc/trpc) | Reuse existing tRPC routers within oRPC. | [tRPC](/docs/integrations/trpc) | ## Observability diff --git a/apps/content/docs/helpers/cache.mdx b/apps/content/docs/helpers/cache.mdx new file mode 100644 index 000000000..7bf93261a --- /dev/null +++ b/apps/content/docs/helpers/cache.mdx @@ -0,0 +1,327 @@ +--- +title: "Cache Helpers" +description: "Cache oRPC procedure output with tag-based revalidation, stale-while-revalidate, storage adapters, and a handler plugin that reflects cache tags in HTTP headers." +sidebar: + label: "Cache" +--- + +## Installation + +```package-install +npm install @orpc/experimental-cache@beta +``` + +## Basic Usage + +The core concept is the `CacheStore` interface: `fetch` resolves the entry under a key, filling it once when there is none even for concurrent callers, and `revalidate` invalidates entries by tag. You can create your own custom store or use one of the provided adapters. A router shares a single store, provided through the request context under the `cache/store` key, as defined by the `CacheContext` interface. + +```ts twoslash +import { MemoryCacheStore } from '@orpc/experimental-cache/memory' +// ---cut--- +const store = new MemoryCacheStore() + +const entry = await store.fetch('planet:1', async () => ({ id: 1, name: 'Earth' }), { + tags: ['planets', 'planet:1'], + ttl: 60, +}) + +await store.revalidate({ tags: ['planets'] }) // the next `fetch` fills again +``` + +An entry stays fresh for `ttl` seconds and is retained for an extra `swr` window afterward, during which `fetch` still returns it with a past `expiresAt` while one caller refreshes it in the background. Revalidating a tag invalidates every entry associated with it, fresh or stale. + +## Adapters + +| Name | Adapter for | +| -------------------------------- | ------------------------------------------------------------------------------------------ | +| `MemoryCacheStore` | In-memory storage | +| `RedisCacheStore` | [Redis](https://github.com/redis/redis) | +| `UpstashCacheStore` | [Upstash Redis](https://github.com/upstash/redis-js) | +| `BunRedisCacheStore` | [Bun's Redis](https://bun.com/docs/runtime/redis) | +| `VercelCacheStore` | [Vercel Runtime Cache](https://vercel.com/docs/caching/runtime-cache) | +| `experimental_WorkersCacheStore` | [Cloudflare Workers Caching](https://developers.cloudflare.com/workers/cache/), purge only | + +Every duration is in seconds, matching what the underlying caches accept. + +Keys may be any serializable value. Strings are used verbatim, while anything else is encoded with `encodeCacheKey`: serialized first, so complex values like Date, Map, or Set become plain JSON, then canonicalized, so structurally equal keys resolve the same entry regardless of property order. Reuse it when implementing your own store. + +Every adapter coalesces concurrent callers of one key. The Redis, Upstash, and Bun stores take a lock in Redis within the same script that reads the entry, so a miss costs one round trip to read and lock and one to store and release, and the lock spans processes; it is released when the fill finishes, or after `lockTtl` if it never does. They share `BaseRedisCacheStore` from `@orpc/experimental-cache/redis`, which holds the scripts and the flow, so a store for another Redis-compatible client only has to run a script. The memory and Vercel stores coalesce within the process through `MemoryLock` from `@orpc/shared`, which suits stores of your own too. + + + +```ts memory +import { MemoryCacheStore } from '@orpc/experimental-cache/memory' + +const store = new MemoryCacheStore({ + /** + * Serializer used to encode non-string keys. + * + * @default RPCJsonSerializer + */ + serializer: undefined, +}) +``` + +```ts redis +import { RedisCacheStore } from '@orpc/experimental-cache/redis' +import { createClient } from 'redis' + +const client = createClient({ url: 'redis://localhost:6379' }) + +// RedisCacheStore lazily connects to Redis when needed. +// You can still call `client.connect()` manually, but it is optional. +await client.connect() + +const store = new RedisCacheStore(client, { + /** + * The prefix to use for Redis keys. + * + * @default undefined + */ + prefix: undefined, + + /** + * Serializer for cached outputs. + * + * @default RPCSerializer + */ + serializer: undefined, + + /** + * How long a lock may be held, in seconds, so a crashed holder frees its waiters. + * + * @default 10 + */ + lockTtl: 10, +}) +``` + +```ts upstash +import { UpstashCacheStore } from '@orpc/experimental-cache/upstash' +import { Redis } from '@upstash/redis' + +const redis = Redis.fromEnv() + +// Shares its key and envelope format with RedisCacheStore, +// so both can serve the same database. +const store = new UpstashCacheStore(redis, { + /** + * The prefix to use for Redis keys. + * + * @default undefined + */ + prefix: undefined, + + /** + * Serializer for cached outputs. + * + * @default RPCSerializer + */ + serializer: undefined, + + /** + * How long a lock may be held, in seconds, so a crashed holder frees its waiters. + * + * @default 10 + */ + lockTtl: 10, +}) +``` + +```ts bun +import { BunRedisCacheStore } from '@orpc/bun' +import { redis } from 'bun' + +// Shares its key and envelope format with RedisCacheStore, +// so both can serve the same database. +const store = new BunRedisCacheStore(redis, { + /** + * The prefix to use for Redis keys. + * + * @default undefined + */ + prefix: undefined, + + /** + * Serializer for cached outputs. + * + * @default RPCSerializer + */ + serializer: undefined, + + /** + * How long a lock may be held, in seconds, so a crashed holder frees its waiters. + * + * @default 10 + */ + lockTtl: 10, +}) +``` + +```ts vercel +import { VercelCacheStore } from '@orpc/experimental-cache/vercel' +import { getCache } from '@vercel/functions' + +const store = new VercelCacheStore({ + /** + * The Vercel Runtime Cache to use. Outside Vercel, + * it falls back to an in-memory cache. + * + * @default getCache() + */ + cache: getCache(), + + /** + * Serializer for cached outputs. + * + * @default RPCSerializer + */ + serializer: undefined, +}) +``` + +```ts cloudflare-workers-caching +import { experimental_WorkersCacheStore as WorkersCacheStore } from '@orpc/cloudflare' + +// Workers Caching caches whole responses in front of the Worker via the +// `cache-control` and `cache-tag` plugin headers; this store only purges +// tags on revalidation. Requires `"cache": { "enabled": true }` in your +// wrangler configuration. Purges are scoped to the calling entrypoint, +// tags match case-insensitively, and purge calls always use the Free +// tier rate limits regardless of your plan. +// +// Purges through `cache` from `cloudflare:workers` by default; pass +// a purger such as `ctx.cache` to use another one. +const store = new WorkersCacheStore() +``` + + +## Cache Middleware + +The `cache` helper creates middleware that caches the output of [procedures](/docs/procedure). On a hit it returns the cached output without executing the handler, and on a miss it executes the handler and stores the result. Concurrent misses for one key run the handler once and the rest are served from the entry it stores. The `key`, `tags`, `ttl`, `swr`, and `enabled` options accept static values or functions of the middleware options and input. + +The `key` is optional: by default it is derived from the procedure path and input. When provided, it is used as given, so procedures sharing a key also share an entry. + +```ts +import { cache, CacheContext } from '@orpc/experimental-cache' +import { MemoryCacheStore } from '@orpc/experimental-cache/memory' + +const findPlanet = os + .$context() + .input(z.object({ id: z.number() })) + .use( + cache({ + key: (_, input) => `planet:${input.id}`, + tags: (_, input) => ['planets', `planet:${input.id}`], + ttl: 60, // Optional fresh lifetime in seconds, default is no expiry + swr: 300, // Optional stale-while-revalidate window in seconds, default is 0 + }), + ) + .handler(({ input }) => { + return { id: input.id, name: `Planet ${input.id}` } + }) + +const result = await call( + findPlanet, + { id: 1 }, + { context: { 'cache/store': new MemoryCacheStore() } }, +) +``` + +:::warning +Entries are stored only when the handler succeeds, and stores pass output straight to their serializer. Values it cannot represent, such as [AsyncIteratorObject](/docs/async-iterator-object), readable streams, Blob, and File, will not survive the round trip, so do not cache procedures returning them. +::: + +:::warning +A cached entry is shared by everyone using the same key. If output depends on the requester, include the distinguishing part in `key`, or resolve `enabled` to `false` to bypass caching for that request. +::: + +### Stale While Revalidate + +When an entry is past `ttl` but within the `swr` window, the middleware returns the stale output immediately and re-executes the procedure in the background to refresh the entry. Concurrent stale hits refresh once; the cache never serves anything older than `ttl + swr`. + +On runtimes that stop pending work once the response is sent, such as Cloudflare Workers, provide `cache/waitUntil` through the context so background refreshes can finish: + +```ts +export default { + async fetch(request, env, ctx) { + const { response } = await handler.handle(request, { + context: { + 'cache/store': store, + 'cache/waitUntil': ctx.waitUntil.bind(ctx), + }, + }) + + return response ?? new Response('Not Found', { status: 404 }) + }, +} +``` + +The promise it receives rejects when a refresh fails, so `cache/waitUntil` is also where those failures are handled. Without it they surface as unhandled rejections, so on other runtimes provide one that reports them, for example `promise => promise.catch(console.error)`. + +## Revalidate Middleware + +The `revalidate` helper creates middleware that revalidates tags after the procedure succeeds, typically on mutations. The required `tags` option accepts a non-empty list of tags or a function of the middleware options and input. If the procedure throws, or `tags` resolves to `null` or `undefined`, the revalidation is skipped. + +```ts +import { revalidate } from '@orpc/experimental-cache' + +const updatePlanet = os + .$context() + .input(z.object({ id: z.number(), name: z.string() })) + .use( + revalidate({ tags: (_, input) => ['planets', `planet:${input.id}`] }), + ) + .handler(({ input }) => { + return input + }) +``` + +## Handler Plugin + +The `CacheHandlerPlugin` reflects the cache activity of [Cache Middleware](#cache-middleware) and [Revalidate Middleware](#revalidate-middleware) into response headers. It does nothing by default; only the headers you list are set: + +- `orpc-cache-tag` carries the tags the response depends on. +- `orpc-cache-tag-invalidation` carries the tags revalidated by the request, useful for invalidating tagged data in client caches. +- `cache-control` and `cache-tag` are the standard HTTP counterparts for response caches in front, such as CDNs or Cloudflare Workers Caching. + +The plugin sets these over anything already on the response. To override them, set your own afterwards with [ResponseHeadersPlugin](/docs/plugins/response-headers). + +Tags are joined with commas. Only `%`, `,`, uppercase letters, and characters that cannot appear in a header value are percent-encoded, so typical tags stay readable. Uppercase letters are encoded because caches like Cloudflare Workers Caching match tags case-insensitively; the encoded form stays unambiguous under case folding. Use `decodeCacheTagHeader` from `@orpc/shared` to parse a header back into tags. + +```ts +import { CacheHandlerPlugin } from '@orpc/experimental-cache' + +const handler = new RPCHandler(router, { + plugins: [ + new CacheHandlerPlugin({ + headers: ['orpc-cache-tag', 'orpc-cache-tag-invalidation'], + }), + ], +}) +``` + +:::info[Response Caches in Front] +With `cache-control` and `cache-tag` configured, a response cache in front serves cached responses without invoking your server at all. Pair it with a purge-capable store, such as `experimental_WorkersCacheStore`, so revalidations also purge the front cache. The plugin sets these whenever the called procedure ran the cache middleware, but standard HTTP caches only store GET and HEAD responses, so this mainly benefits [OpenAPIHandler](/docs/openapi/handler) routes; RPC requests use POST. + +`cache-control` uses `max-age`, not `s-maxage`, because [`s-maxage` carries `proxy-revalidate` semantics](https://www.rfc-editor.org/rfc/rfc9111#section-5.2.2.10) that forbid the stale reuse `swr` asks for. It therefore reaches browser caches too, which no tag purge can invalidate. Set your own `cache-control` when you need responses kept out of them. +::: + +:::info +When a procedure calls other procedures, only the first cache check and the first revalidation of the procedure the client called are reflected. Nested procedures never leak their tags into the response. Headers appear only on successful responses. +::: + +:::tip[Cross-Origin Clients] +The headers use oRPC-specific names on purpose: CDN-facing conventions like `Cache-Tag` can be consumed and stripped by intermediaries before reaching the browser, while these always arrive intact for client-side revalidation. For cross-origin browser clients, list them in [CORSPlugin](/docs/plugins/cors)'s `exposeHeaders` so client code can read them: + +```ts +new CORSPlugin({ + exposeHeaders: ['orpc-cache-tag', 'orpc-cache-tag-invalidation'], +}) +``` + +::: + +:::info +The `handler` can be any supported oRPC handler, such as [RPCHandler](/docs/rpc/handler), [OpenAPIHandler](/docs/openapi/handler), or a custom one. +::: diff --git a/apps/content/package.json b/apps/content/package.json index 7420e2eaa..7613d48d7 100644 --- a/apps/content/package.json +++ b/apps/content/package.json @@ -17,6 +17,7 @@ "@orpc/client": "workspace:*", "@orpc/contract": "workspace:*", "@orpc/evlog": "workspace:*", + "@orpc/experimental-cache": "workspace:*", "@orpc/openapi": "workspace:*", "@orpc/opentelemetry": "workspace:*", "@orpc/pino": "workspace:*", diff --git a/eslint.config.js b/eslint.config.js index e508e8159..96843c640 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -17,6 +17,7 @@ export default antfu({ rules: { 'ts/consistent-type-definitions': 'off', 'ts/method-signature-style': ['off'], + 'new-cap': ['error', { capIsNew: false, newIsCapExceptionPattern: '^experimental_', properties: true }], 'ban/ban': [ 'error', { diff --git a/package.json b/package.json index 301599d4b..93414d8a1 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@orpc/client": "workspace:*", "@orpc/contract": "workspace:*", "@orpc/evlog": "workspace:*", + "@orpc/experimental-cache": "workspace:*", "@orpc/experimental-effect": "workspace:*", "@orpc/experimental-msw": "workspace:*", "@orpc/hibernation": "workspace:*", diff --git a/packages/ai-sdk/README.md b/packages/ai-sdk/README.md index 7298e862f..b0328ca6f 100644 --- a/packages/ai-sdk/README.md +++ b/packages/ai-sdk/README.md @@ -46,29 +46,30 @@ You can read the documentation [here](https://orpc.dev). **Built-in features** -| Package | Purpose | Docs | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | -| [@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) | -| [@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) | -| [@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) | +| Package | Purpose | Docs | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | +| [@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) | +| [@orpc/experimental-cache](https://www.npmjs.com/package/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](https://orpc.dev/docs/helpers/cache) | +| [@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) | +| [@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) | **Framework & ecosystem integrations** -| Package | Purpose | Docs | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [@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) | -| [@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) | -| [@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) | -| [@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) | -| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | -| [@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) | -| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | -| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | -| [@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) | -| [@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) | -| [@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), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | -| [@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) | +| Package | Purpose | Docs | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [@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) | +| [@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) | +| [@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) | +| [@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) | +| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | +| [@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) | +| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | +| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | +| [@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) | +| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | +| [@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) | **Observability** diff --git a/packages/arktype/README.md b/packages/arktype/README.md index b1826d3c0..0463696aa 100644 --- a/packages/arktype/README.md +++ b/packages/arktype/README.md @@ -46,29 +46,30 @@ You can read the documentation [here](https://orpc.dev). **Built-in features** -| Package | Purpose | Docs | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | -| [@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) | -| [@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) | -| [@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) | +| Package | Purpose | Docs | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | +| [@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) | +| [@orpc/experimental-cache](https://www.npmjs.com/package/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](https://orpc.dev/docs/helpers/cache) | +| [@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) | +| [@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) | **Framework & ecosystem integrations** -| Package | Purpose | Docs | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [@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) | -| [@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) | -| [@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) | -| [@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) | -| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | -| [@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) | -| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | -| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | -| [@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) | -| [@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) | -| [@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), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | -| [@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) | +| Package | Purpose | Docs | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [@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) | +| [@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) | +| [@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) | +| [@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) | +| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | +| [@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) | +| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | +| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | +| [@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) | +| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | +| [@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) | **Observability** diff --git a/packages/bun/README.md b/packages/bun/README.md index 0c725cfcf..2d855876e 100644 --- a/packages/bun/README.md +++ b/packages/bun/README.md @@ -43,29 +43,30 @@ You can read the documentation [here](https://orpc.dev). **Built-in features** -| Package | Purpose | Docs | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | -| [@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) | -| [@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) | -| [@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) | +| Package | Purpose | Docs | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | +| [@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) | +| [@orpc/experimental-cache](https://www.npmjs.com/package/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](https://orpc.dev/docs/helpers/cache) | +| [@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) | +| [@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) | **Framework & ecosystem integrations** -| Package | Purpose | Docs | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [@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) | -| [@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) | -| [@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) | -| [@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) | -| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | -| [@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) | -| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | -| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | -| [@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) | -| [@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) | -| [@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), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | -| [@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) | +| Package | Purpose | Docs | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [@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) | +| [@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) | +| [@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) | +| [@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) | +| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | +| [@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) | +| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | +| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | +| [@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) | +| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | +| [@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) | **Observability** diff --git a/packages/bun/package.json b/packages/bun/package.json index add01e1fc..0a2b72c4d 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -2,7 +2,7 @@ "name": "@orpc/bun", "type": "module", "version": "2.0.0-beta.34", - "description": "Bun integration for oRPC: Redis-backed pub/sub and rate limiting using Bun's built-in Redis client", + "description": "Bun integration for oRPC: Redis-backed pub/sub, rate limiting, and caching using Bun's built-in Redis client", "license": "MIT", "funding": [ "https://github.com/sponsors/dinwwwh", @@ -20,6 +20,7 @@ "redis", "pubsub", "ratelimit", + "cache", "typescript" ], "sideEffects": false, @@ -48,6 +49,7 @@ }, "dependencies": { "@orpc/client": "workspace:*", + "@orpc/experimental-cache": "workspace:*", "@orpc/publisher": "workspace:*", "@orpc/ratelimit": "workspace:*", "@orpc/server": "workspace:*", diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index b5bc5396e..4b5a162d0 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -1,2 +1,3 @@ +export * from './redis-cache' export * from './redis-publisher' export * from './redis-ratelimit' diff --git a/packages/bun/src/redis-cache.test.ts b/packages/bun/src/redis-cache.test.ts new file mode 100644 index 000000000..129f10861 --- /dev/null +++ b/packages/bun/src/redis-cache.test.ts @@ -0,0 +1,214 @@ +import { RPCSerializer } from '@orpc/client' +import { nowInSeconds, sleep } from '@orpc/shared' +import { RedisClient } from 'bun' +import { beforeAll, describe, expect, it, mock, spyOn } from 'bun:test' +import { waitFor } from '../tests/__shared__/utils' +import { BunRedisCacheStore } from './redis-cache' + +const REDIS_URL = Bun.env.REDIS_URL + +describe.skipIf(!REDIS_URL)('bun redis cache store integration', () => { + const redis = new RedisClient(REDIS_URL) + + // Closing the client here breaks the next file's client on Bun 1.4; the process exit closes it. + beforeAll(async () => { + await redis.connect() + }) + + function createTestingStore(options: ConstructorParameters[1] = {}) { + const prefix = `orpc-bun-redis-cache-store-${crypto.randomUUID()}:` + return { store: new BunRedisCacheStore(redis, { prefix, ...options }), prefix } + } + + it('fills a miss once, then serves the entry with its tags and expiresAt', async () => { + const { store } = createTestingStore() + const fill = mock(async () => ({ nested: [1, 2] })) + + const first = await store.fetch('k', fill, { tags: ['t'], ttl: 120 }) + expect(first.output).toEqual({ nested: [1, 2] }) + expect(first.tags).toEqual(['t']) + expect(first.expiresAt).toBeGreaterThan(nowInSeconds()) + + await expect(store.fetch('k', fill, { tags: ['t'], ttl: 120 })).resolves.toEqual(first) + expect(fill).toHaveBeenCalledTimes(1) + + await store.fetch('u', async () => undefined) + await expect(store.fetch('u', async () => 'refilled')).resolves.toEqual({ output: undefined, tags: undefined, expiresAt: undefined }) + }, { timeout: 20_000 }) + + it('preserves Date, Map, Set, and BigInt outputs', async () => { + const { store } = createTestingStore() + const output = { + date: new Date('2026-01-02T03:04:05.678Z'), + map: new Map([['a', 1]]), + set: new Set([1, 2]), + big: 123n, + } + + await store.fetch('k', async () => output) + await expect(store.fetch('k', async () => 'refilled')).resolves.toMatchObject({ output }) + }, { timeout: 20_000 }) + + it('invalidates entries by any of their tags, and keeps ones filled afterwards', async () => { + const { store } = createTestingStore() + + await store.fetch('multi', async () => 'v', { tags: ['a', 'b'] }) + await store.fetch('other', async () => 'v', { tags: ['c'] }) + + await store.revalidate({ tags: ['a', 'b'] }) + + await expect(store.fetch('multi', async () => 'new', { tags: ['a'] })).resolves.toMatchObject({ output: 'new' }) + await expect(store.fetch('other', async () => 'refilled', { tags: ['c'] })).resolves.toMatchObject({ output: 'v' }) + await expect(store.fetch('multi', async () => 'newer', { tags: ['a'] })).resolves.toMatchObject({ output: 'new' }) + }, { timeout: 20_000 }) + + it('supports a custom serializer', async () => { + const serializer = new RPCSerializer() + const serializeSpy = spyOn(serializer, 'serialize') + const deserializeSpy = spyOn(serializer, 'deserialize') + const { store } = createTestingStore({ serializer }) + + await store.fetch('k', async () => ({ a: 1 })) + + await expect(store.fetch('k', async () => 'other')).resolves.toMatchObject({ output: { a: 1 } }) + expect(serializeSpy).toHaveBeenCalled() + expect(deserializeSpy).toHaveBeenCalled() + }, { timeout: 20_000 }) + + it('fills again at ttl without swr, and serves stale within the swr window while refreshing', async () => { + const { store } = createTestingStore() + + await store.fetch('no-swr', async () => 'v', { ttl: 1 }) + await store.fetch('swr', async () => 'v', { ttl: 1, swr: 10 }) + + await sleep(1500) + + await expect(store.fetch('no-swr', async () => 'refilled', { ttl: 1 })).resolves.toMatchObject({ output: 'refilled' }) + + const waitUntil = mock((_promise: Promise) => {}) + const stale = await store.fetch('swr', async () => 'fresh', { ttl: 1, swr: 10, waitUntil }) + expect(stale.output).toBe('v') + expect(stale.expiresAt).toBeLessThanOrEqual(nowInSeconds()) + + expect(waitUntil).toHaveBeenCalledTimes(1) + await waitUntil.mock.calls[0]![0] + + const fresh = await store.fetch('swr', async () => 'other', { ttl: 1, swr: 10 }) + expect(fresh.output).toBe('fresh') + expect(fresh.expiresAt).toBeGreaterThan(stale.expiresAt!) + }, { timeout: 20_000 }) + + it('stores entries as hashes and tag counters under the prefixed key families, locking while filling', async () => { + const { store, prefix } = createTestingStore() + + await store.fetch('k', async () => { + await expect(redis.exists(`${prefix}l:k`)).resolves.toBe(true) + return 'v' + }, { tags: ['t'] }) + await store.revalidate({ tags: ['t'] }) + + await expect(redis.send('TYPE', [`${prefix}e:k`])).resolves.toBe('hash') + await expect(redis.exists(`${prefix}t:t`)).resolves.toBe(true) + await expect(redis.exists(`${prefix}l:k`)).resolves.toBe(false) + + const unprefixed = new BunRedisCacheStore(redis) + const key = crypto.randomUUID() + await unprefixed.fetch(key, async () => 'v') + await expect(redis.exists(`e:${key}`)).resolves.toBe(true) + }, { timeout: 20_000 }) + + it('treats tags missing from the snapshot as version zero', async () => { + const { store, prefix } = createTestingStore() + + await redis.send('HSET', [`${prefix}e:k`, 'output', JSON.stringify({ body: { json: 'v' } }), 'tags', '["t"]', 'tagVersions', '{}']) + + await expect(store.fetch('k', async () => 'other')).resolves.toMatchObject({ output: 'v' }) + }, { timeout: 20_000 }) + + it('reloads scripts the server dropped, and rethrows other script errors', async () => { + const { store, prefix } = createTestingStore() + + await store.fetch('k', async () => 'v') + await redis.send('SCRIPT', ['FLUSH']) + await expect(store.fetch('k', async () => 'other')).resolves.toMatchObject({ output: 'v' }) + + await redis.send('HSET', [`${prefix}e:broken`, 'output', '{}', 'tags', 'not json', 'tagVersions', '{}']) + await expect(store.fetch('broken', async () => 'v')).rejects.toThrow() + }, { timeout: 20_000 }) + + it('encodes non-string keys stably', async () => { + const { store } = createTestingStore() + + await store.fetch([['planet', 'find'], { b: 2, a: 1 }], async () => 'v') + + await expect(store.fetch([['planet', 'find'], { a: 1, b: 2 }], async () => 'other')).resolves.toMatchObject({ output: 'v' }) + await expect(store.fetch([['planet', 'find'], { a: 1, b: 3 }], async () => 'other')).resolves.toMatchObject({ output: 'other' }) + }, { timeout: 20_000 }) + + it('fills once for concurrent callers of one key, and lets a waiter fill when the holder failed', async () => { + const { store } = createTestingStore() + let finish!: (output: string) => void + const fill = mock(() => new Promise((resolve) => { + finish = resolve + })) + + const fetches = Promise.all([store.fetch('k', fill), store.fetch('k', fill), store.fetch('k', fill)]) + await waitFor(() => expect(fill).toHaveBeenCalledTimes(1), { timeout: 5000 }) + finish('v') + + const entries = await fetches + expect(entries.map(entry => entry.output)).toEqual(['v', 'v', 'v']) + expect(fill).toHaveBeenCalledTimes(1) + + let fail!: (error: Error) => void + let started!: () => void + const holding = new Promise((resolve) => { + started = resolve + }) + const first = store.fetch('failing', () => { + started() + return new Promise((_, reject) => { + fail = reject + }) + }) + await holding + const second = store.fetch('failing', async () => 'fresh') + fail(new Error('handler down')) + + await expect(first).rejects.toThrow('handler down') + await expect(second).resolves.toMatchObject({ output: 'fresh' }) + }, { timeout: 20_000 }) + + it('frees waiters after lockTtl and leaves a lock taken over that way alone', async () => { + const { store: holderStore, prefix } = createTestingStore({ lockTtl: 1 }) + const waiterStore = new BunRedisCacheStore(redis, { prefix }) + let release!: () => void + const held = new Promise((resolve) => { + release = resolve + }) + let takenOver!: () => void + const takeover = new Promise((resolve) => { + takenOver = resolve + }) + + const holder = holderStore.fetch('k', async () => { + await takeover + return 'holder' + }) + await waitFor(async () => expect(await redis.exists(`${prefix}l:k`)).toBe(true), { timeout: 5000 }) + + const waiter = waiterStore.fetch('k', async () => { + takenOver() + await held + return 'waiter' + }) + + await expect(holder).resolves.toMatchObject({ output: 'holder' }) + await expect(redis.exists(`${prefix}l:k`)).resolves.toBe(true) + + release() + await expect(waiter).resolves.toMatchObject({ output: 'waiter' }) + await expect(redis.exists(`${prefix}l:k`)).resolves.toBe(false) + await expect(holderStore.fetch('k', async () => 'other')).resolves.toMatchObject({ output: 'waiter' }) + }, { timeout: 20_000 }) +}) diff --git a/packages/bun/src/redis-cache.ts b/packages/bun/src/redis-cache.ts new file mode 100644 index 000000000..fc6f7e0b3 --- /dev/null +++ b/packages/bun/src/redis-cache.ts @@ -0,0 +1,45 @@ +import type { BaseRedisCacheStoreOptions } from '@orpc/experimental-cache/redis' +import type { RedisClient } from 'bun' +import { BaseRedisCacheStore } from '@orpc/experimental-cache/redis' + +export type BunRedisCacheStoreOptions = BaseRedisCacheStoreOptions + +/** + * Cache store adapter for Bun's built-in Redis client. Shares its key and + * entry format with `RedisCacheStore`, so both can serve the same database, + * and runs the scripts by sha, loading each once per client and again if the + * server dropped it. + * + * @see {@link https://orpc.dev/docs/helpers/cache#adapters | Cache Helpers - Adapters} + */ +export class BunRedisCacheStore extends BaseRedisCacheStore { + private readonly scriptShas = new Map() + + constructor( + private readonly redis: RedisClient, + options: BunRedisCacheStoreOptions = {}, + ) { + super(options) + } + + protected async run(script: string, keys: string[], args: string[], reloaded = false): Promise { + let sha = this.scriptShas.get(script) + + if (sha === undefined) { + sha = await this.redis.send('SCRIPT', ['LOAD', script]) as string + this.scriptShas.set(script, sha) + } + + try { + return await this.redis.send('EVALSHA', [sha, String(keys.length), ...keys, ...args]) + } + catch (error) { + if (!reloaded && error instanceof Error && error.message.startsWith('NOSCRIPT')) { + this.scriptShas.delete(script) + return await this.run(script, keys, args, true) + } + + throw error + } + } +} diff --git a/packages/bun/tests/cache-redis-adapters-compatibility.test.ts b/packages/bun/tests/cache-redis-adapters-compatibility.test.ts new file mode 100644 index 000000000..12ef4c5b4 --- /dev/null +++ b/packages/bun/tests/cache-redis-adapters-compatibility.test.ts @@ -0,0 +1,124 @@ +import type { CacheStore } from '@orpc/experimental-cache' +import { RedisCacheStore } from '@orpc/experimental-cache/redis' +import { nowInSeconds, sleep } from '@orpc/shared' +import { RedisClient } from 'bun' +import { afterAll, describe, expect, it } from 'bun:test' +import { createClient } from 'redis' +import { BunRedisCacheStore } from '../src/redis-cache' + +const REDIS_URL = Bun.env.REDIS_URL + +/** + * These tests require a real Redis server. Set `REDIS_URL` before running them. + * + * When adding new tests, always use unique keys to avoid conflicts with other cases. + * + * All adapters must connect to the same server. + */ +const stores: Array<{ name: string, store: CacheStore }> = [] +const prefix = `redis-adapters:${crypto.randomUUID()}:` + +if (REDIS_URL) { + const redis = createClient({ url: REDIS_URL }) + const bunRedis = new RedisClient(REDIS_URL) + + afterAll(() => { + redis.close() + bunRedis.close() + }) + + stores.push({ name: 'redis', store: new RedisCacheStore(redis, { prefix }) }) + stores.push({ name: 'bun redis', store: new BunRedisCacheStore(bunRedis, { prefix }) }) +} + +describe.concurrent('cache redis adapters compatibility', () => { + describe.skipIf(stores.length < 2)('cross-adapter compatibility', () => { + for (const source of stores) { + for (const target of stores) { + if (source === target) { + continue + } + + it(`shares entries and revalidations: ${source.name} → ${target.name}`, async () => { + const id = crypto.randomUUID() + const tag = `tag:${crypto.randomUUID()}` + const output = { date: new Date('2026-01-02T03:04:05.678Z'), big: 123n } + + await source.store.fetch([['planet', 'find'], { b: 2, id }], async () => output, { tags: [tag], ttl: 60 }) + + const entry = await target.store.fetch([['planet', 'find'], { id, b: 2 }], async () => 'refilled', { tags: [tag], ttl: 60 }) + expect(entry.output).toEqual(output) + expect(entry.tags).toEqual([tag]) + expect(entry.expiresAt).toBeGreaterThan(nowInSeconds()) + + await target.store.revalidate({ tags: [tag] }) + + await expect(source.store.fetch([['planet', 'find'], { b: 2, id }], async () => 'refilled', { tags: [tag] })).resolves.toMatchObject({ output: 'refilled' }) + }, { timeout: 20_000 }) + + it(`shares tag counters: ${source.name} → ${target.name}`, async () => { + const key = `counter:${crypto.randomUUID()}` + const tag = `tag:${crypto.randomUUID()}` + + await source.store.fetch(key, async () => 'v1', { tags: [tag] }) + await target.store.revalidate({ tags: [tag] }) + + await expect(target.store.fetch(key, async () => 'v2', { tags: [tag] })).resolves.toMatchObject({ output: 'v2' }) + await expect(source.store.fetch(key, async () => 'v3', { tags: [tag] })).resolves.toMatchObject({ output: 'v2' }) + + await source.store.revalidate({ tags: [tag] }) + await expect(target.store.fetch(key, async () => 'v4', { tags: [tag] })).resolves.toMatchObject({ output: 'v4' }) + }, { timeout: 20_000 }) + + it(`shares retention: ${source.name} → ${target.name}`, async () => { + const noSwr = `no-swr:${crypto.randomUUID()}` + const swr = `swr:${crypto.randomUUID()}` + + await source.store.fetch(noSwr, async () => 'v', { ttl: 1 }) + await source.store.fetch(swr, async () => 'v', { ttl: 1, swr: 10 }) + + await sleep(1500) + + await expect(target.store.fetch(noSwr, async () => 'refilled', { ttl: 1 })).resolves.toMatchObject({ output: 'refilled' }) + + const waitUntil = (_promise: Promise) => {} + const stale = await target.store.fetch(swr, async () => 'refilled', { ttl: 1, swr: 10, waitUntil }) + expect(stale.output).toBe('v') + expect(stale.expiresAt).toBeLessThanOrEqual(nowInSeconds()) + }, { timeout: 20_000 }) + + it(`shares locks: ${source.name} → ${target.name}`, async () => { + const key = `lock:${crypto.randomUUID()}` + let release!: () => void + const held = new Promise((resolve) => { + release = resolve + }) + let acquired!: () => void + const holding = new Promise((resolve) => { + acquired = resolve + }) + + const holder = source.store.fetch(key, async () => { + acquired() + await held + return 'held' + }) + await holding + + let settled = false + const waiter = target.store.fetch(key, async () => 'refilled').then((entry) => { + settled = true + return entry + }) + + await sleep(300) + expect(settled).toBe(false) + + release() + await holder + await expect(waiter).resolves.toMatchObject({ output: 'held' }) + }, { timeout: 20_000 }) + } + } + }) +}) diff --git a/packages/cache/README.md b/packages/cache/README.md new file mode 100644 index 000000000..f685538c8 --- /dev/null +++ b/packages/cache/README.md @@ -0,0 +1,170 @@ +

oRPC - Typesafe APIs Made Simple 🪄

+ + + +## Documentation + +You can read the documentation [here](https://orpc.dev). + +## Packages + +**Core** + +| Package | Purpose | Docs | +| -------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [@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) | +| [@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) | +| [@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) | +| [@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) | + +**Schema validation** + +| Package | Purpose | Docs | +| ------------------------------------------------------------ | ----------------------------------------------- | ----------------------------------------------------- | +| [@orpc/zod](https://www.npmjs.com/package/@orpc/zod) | Integrate with [Zod](https://zod.dev/). | [Zod](https://orpc.dev/docs/integrations/zod) | +| [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot) | Integrate with [Valibot](https://valibot.dev/). | [Valibot](https://orpc.dev/docs/integrations/valibot) | +| [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype) | Integrate with [ArkType](https://arktype.io/). | [ArkType](https://orpc.dev/docs/integrations/arktype) | + +**Built-in features** + +| Package | Purpose | Docs | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | +| [@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) | +| [@orpc/experimental-cache](https://www.npmjs.com/package/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](https://orpc.dev/docs/helpers/cache) | +| [@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) | +| [@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) | + +**Framework & ecosystem integrations** + +| Package | Purpose | Docs | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [@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) | +| [@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) | +| [@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) | +| [@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) | +| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | +| [@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) | +| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | +| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | +| [@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) | +| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@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) | + +**Observability** + +| Package | Purpose | Docs | +| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- | +| [@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) | +| [@orpc/pino](https://www.npmjs.com/package/@orpc/pino) | Integrate with [Pino](https://getpino.io/) for logging. | [Pino](https://orpc.dev/docs/integrations/pino) | +| [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog) | Integrate with [Evlog](https://evlog.dev/) for logging. | [Evlog](https://orpc.dev/docs/integrations/evlog) | + +## Sponsors + +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! 🚀 + + + + + + + + +
ScreenshotOne.comScreenshotOne.com
The screenshot API for developers
MisskeyHQMisskeyHQ
Decentralized microblogging SNS born on Earth
+ +### Organization Sponsors + + + + + +
LN Markets
LN Markets
+ +### Sponsors + + + + + + + + + + + + + + + + + + + + + + + + + +
Reece McDonald
Reece McDonald
あわわわとーにゅ
あわわわとーにゅ
nk
nk
supastarter
supastarter
Dexter Miguel
Dexter Miguel
herrfugbaum
herrfugbaum
Ryota Murakami
Ryota Murakami
David Cramer
David Cramer
Valerii Petryniak
Valerii Petryniak
Valerii Strilets
Valerii Strilets
Kyle Mistele
Kyle Mistele
christ12938
christ12938
Ryan Soderberg
Ryan Soderberg
shota
shota
Ellis Driscoll
Ellis Driscoll
Hoang Nguyen
Hoang Nguyen
Orestis Ioannou
Orestis Ioannou
+ +### Backers + + + + + + + + + + + + + + + + + + + + + + + + + +
David Walsh
David Walsh
Robbe Vaes
Robbe Vaes
Aidan Sunbury
Aidan Sunbury
soonoo
soonoo
Kevin Porten
Kevin Porten
Denis
Denis
Christopher Kapic
Christopher Kapic
Tom Ballinger
Tom Ballinger
Sam
Sam
Titoine
Titoine
Igor Makowski
Igor Makowski
hanayashiki
hanayashiki
Lev Dubinets
Lev Dubinets
Kelly Peilin Chan
Kelly Peilin Chan
Guy Ariely
Guy Ariely
Alex
Alex
Andrey Gubanov
Andrey Gubanov
+ +With thanks to [37 past sponsors](https://htmlpreview.github.io/?https://github.com/middleapi/static/blob/main/sponsors.svg) who helped get oRPC here. + +## References + +oRPC is inspired by existing solutions that prioritize type safety and developer experience. Special acknowledgments to: + +- [tRPC](https://trpc.io): For pioneering the concept of end-to-end type-safe RPC and influencing the development of type-safe APIs. +- [ts-rest](https://ts-rest.com): For its emphasis on contract-first development and OpenAPI integration, which have greatly inspired oRPC's feature set. + +## License + +Distributed under the MIT License. See [LICENSE](https://github.com/middleapi/orpc/blob/main/LICENSE) for more information. diff --git a/packages/cache/package.json b/packages/cache/package.json new file mode 100644 index 000000000..42d70cc01 --- /dev/null +++ b/packages/cache/package.json @@ -0,0 +1,103 @@ +{ + "name": "@orpc/experimental-cache", + "type": "module", + "version": "2.0.0-beta.34", + "description": "Tag-based caching and revalidation for oRPC procedures, with memory, Redis, Upstash, and Vercel adapters", + "license": "MIT", + "funding": [ + "https://github.com/sponsors/dinwwwh", + "https://opencollective.com/middleapi" + ], + "homepage": "https://orpc.dev", + "repository": { + "type": "git", + "url": "git+https://github.com/middleapi/orpc.git", + "directory": "packages/cache" + }, + "keywords": [ + "orpc", + "cache", + "caching", + "revalidation", + "stale-while-revalidate", + "redis", + "upstash", + "vercel", + "middleware", + "api", + "typescript" + ], + "sideEffects": false, + "publishConfig": { + "exports": { + "./package.json": "./package.json", + ".": { + "types": "./dist/index.d.mts", + "import": "./dist/index.mjs", + "default": "./dist/index.mjs" + }, + "./memory": { + "types": "./dist/adapters/memory.d.mts", + "import": "./dist/adapters/memory.mjs", + "default": "./dist/adapters/memory.mjs" + }, + "./redis": { + "types": "./dist/adapters/redis.d.mts", + "import": "./dist/adapters/redis.mjs", + "default": "./dist/adapters/redis.mjs" + }, + "./upstash": { + "types": "./dist/adapters/upstash.d.mts", + "import": "./dist/adapters/upstash.mjs", + "default": "./dist/adapters/upstash.mjs" + }, + "./vercel": { + "types": "./dist/adapters/vercel.d.mts", + "import": "./dist/adapters/vercel.mjs", + "default": "./dist/adapters/vercel.mjs" + } + } + }, + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./memory": "./src/adapters/memory.ts", + "./redis": "./src/adapters/redis.ts", + "./upstash": "./src/adapters/upstash.ts", + "./vercel": "./src/adapters/vercel.ts" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "unbuild", + "type:check": "tsc -b" + }, + "peerDependencies": { + "@upstash/redis": ">=1.35.6", + "@vercel/functions": ">=2.1.0", + "redis": ">=6.0.0" + }, + "peerDependenciesMeta": { + "@upstash/redis": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "redis": { + "optional": true + } + }, + "dependencies": { + "@orpc/client": "workspace:*", + "@orpc/server": "workspace:*", + "@orpc/shared": "workspace:*", + "@standardserver/core": "^0.8.2" + }, + "devDependencies": { + "@upstash/redis": "^1.38.2", + "@vercel/functions": "^3.9.5", + "redis": "^6.2.1" + } +} diff --git a/packages/cache/src/adapters/memory.test.ts b/packages/cache/src/adapters/memory.test.ts new file mode 100644 index 000000000..84dab9dd9 --- /dev/null +++ b/packages/cache/src/adapters/memory.test.ts @@ -0,0 +1,143 @@ +import { RPCJsonSerializer } from '@orpc/client' +import { describeCacheStoreContract } from '../../tests/__shared__/store-contract' +import { MemoryCacheStore } from './memory' + +describe('memoryCacheStore', () => { + beforeEach(() => { + vi.useFakeTimers() + vi.setSystemTime(0) + }) + + afterEach(() => { + vi.useRealTimers() + }) + + describeCacheStoreContract(() => new MemoryCacheStore()) + + it('encodes structurally equal non-string keys to the same entry', async () => { + const store = new MemoryCacheStore() + + await store.fetch([['planet', 'find'], { b: 2, a: 1 }], async () => 'v') + + await expect(store.fetch([['planet', 'find'], { a: 1, b: 2 }], async () => 'other')).resolves.toMatchObject({ output: 'v' }) + await expect(store.fetch([['planet', 'find'], { a: 1, b: 3 }], async () => 'other')).resolves.toMatchObject({ output: 'other' }) + await expect(store.fetch([['planet', 'list'], { a: 1, b: 2 }], async () => 'other')).resolves.toMatchObject({ output: 'other' }) + }) + + it('encodes complex key values, ignoring unsupported ones like blobs', async () => { + const store = new MemoryCacheStore() + + await store.fetch({ date: new Date(1), big: 1n }, async () => 'v') + await expect(store.fetch({ big: 1n, date: new Date(1) }, async () => 'other')).resolves.toMatchObject({ output: 'v' }) + await expect(store.fetch({ big: 2n, date: new Date(1) }, async () => 'other')).resolves.toMatchObject({ output: 'other' }) + + await store.fetch({ file: new Blob(['a']), id: 1 }, async () => 'blobbed') + await expect(store.fetch({ file: new Blob(['b']), id: 1 }, async () => 'other')).resolves.toMatchObject({ output: 'blobbed' }) + }) + + it('supports a custom key serializer', async () => { + const serializer = new RPCJsonSerializer() + const serializeSpy = vi.spyOn(serializer, 'serialize') + const store = new MemoryCacheStore({ serializer }) + + await store.fetch({ id: 1 }, async () => 'v') + + await expect(store.fetch({ id: 1 }, async () => 'other')).resolves.toMatchObject({ output: 'v' }) + expect(serializeSpy).toHaveBeenCalled() + }) + + it('returns fresh entries with a future expiresAt, then fills again at ttl without swr', async () => { + const store = new MemoryCacheStore() + + await expect(store.fetch('k', async () => 'v', { ttl: 1 })).resolves.toEqual({ output: 'v', tags: undefined, expiresAt: 1 }) + + vi.setSystemTime(999) + await expect(store.fetch('k', async () => 'other', { ttl: 1 })).resolves.toMatchObject({ output: 'v' }) + + vi.setSystemTime(1000) + await expect(store.fetch('k', async () => 'other', { ttl: 1 })).resolves.toEqual({ output: 'other', tags: undefined, expiresAt: 2 }) + }) + + it('serves stale entries within swr while one caller refreshes them in the background', async () => { + const store = new MemoryCacheStore() + await store.fetch('k', async () => 'v', { ttl: 1, swr: 1 }) + + vi.setSystemTime(1200) // past ttl, within swr + let finish!: (output: string) => void + const fill = vi.fn(() => new Promise((resolve) => { + finish = resolve + })) + const waitUntil = vi.fn() + + await expect(store.fetch('k', fill, { ttl: 1, swr: 1, waitUntil })).resolves.toEqual({ output: 'v', tags: undefined, expiresAt: 1 }) + await expect(store.fetch('k', fill, { ttl: 1, swr: 1, waitUntil })).resolves.toEqual({ output: 'v', tags: undefined, expiresAt: 1 }) + expect(waitUntil).toHaveBeenCalledTimes(2) + + finish('fresh') + await Promise.all(waitUntil.mock.calls.map(([refresh]) => refresh)) + expect(fill).toHaveBeenCalledTimes(1) // the second stale hit found the refreshed entry + + await expect(store.fetch('k', fill, { ttl: 1, swr: 1 })).resolves.toEqual({ output: 'fresh', tags: undefined, expiresAt: 2 }) + }) + + it('leaves a failed refresh to waitUntil and keeps serving the stale entry', async () => { + const store = new MemoryCacheStore() + await store.fetch('k', async () => 'v', { ttl: 1, swr: 1 }) + + vi.setSystemTime(1200) + const waitUntil = vi.fn() + const fill = vi.fn(async () => { + throw new Error('handler down') + }) + + await expect(store.fetch('k', fill, { ttl: 1, swr: 1, waitUntil })).resolves.toMatchObject({ output: 'v' }) + await expect(waitUntil.mock.calls[0]![0]).rejects.toThrow('handler down') + + await expect(store.fetch('k', fill, { ttl: 1, swr: 1, waitUntil })).resolves.toMatchObject({ output: 'v' }) + await expect(waitUntil.mock.calls[1]![0]).rejects.toThrow('handler down') + expect(fill).toHaveBeenCalledTimes(2) + }) + + it('lets a waiting refresh fill when the first one failed', async () => { + const store = new MemoryCacheStore() + await store.fetch('k', async () => 'v', { ttl: 1, swr: 1 }) + + vi.setSystemTime(1200) + let fail!: (error: Error) => void + const fill = vi.fn() + .mockImplementationOnce(() => new Promise((_, reject) => { + fail = reject + })) + .mockResolvedValue('fresh') + const waitUntil = vi.fn() + + await store.fetch('k', fill, { ttl: 1, swr: 1, waitUntil }) + await store.fetch('k', fill, { ttl: 1, swr: 1, waitUntil }) + fail(new Error('handler down')) + + await expect(waitUntil.mock.calls[0]![0]).rejects.toThrow('handler down') + await waitUntil.mock.calls[1]![0] + expect(fill).toHaveBeenCalledTimes(2) + + await expect(store.fetch('k', fill, { ttl: 1, swr: 1 })).resolves.toMatchObject({ output: 'fresh' }) + }) + + it('evicts past ttl + swr, and revalidation drops stale entries too', async () => { + const store = new MemoryCacheStore() + + await store.fetch('evicted', async () => 'v', { ttl: 1, swr: 1 }) + await store.fetch('stale', async () => 'v', { tags: ['a'], ttl: 1, swr: 1 }) + await store.fetch('k', async () => 'old', { tags: ['old'], ttl: 1 }) + + vi.setSystemTime(1000) // 'k' expired without swr, so it is filled again with new tags + await expect(store.fetch('k', async () => 'new', { tags: ['new'] })).resolves.toEqual({ output: 'new', tags: ['new'], expiresAt: undefined }) + + vi.setSystemTime(1200) // 'stale' and 'evicted' are stale + await store.revalidate({ tags: ['a', 'old'] }) + await expect(store.fetch('stale', async () => 'refilled', { tags: ['a'] })).resolves.toMatchObject({ output: 'refilled' }) + await expect(store.fetch('k', async () => 'refilled', { tags: ['new'] })).resolves.toMatchObject({ output: 'new' }) + + vi.setSystemTime(2000) // past ttl + swr + await expect(store.fetch('evicted', async () => 'refilled')).resolves.toMatchObject({ output: 'refilled' }) + }) +}) diff --git a/packages/cache/src/adapters/memory.ts b/packages/cache/src/adapters/memory.ts new file mode 100644 index 000000000..e9deb2c15 --- /dev/null +++ b/packages/cache/src/adapters/memory.ts @@ -0,0 +1,121 @@ +import type { Public } from '@orpc/shared' +import type { CacheEntry, CacheFetchOptions, CacheRevalidateOptions, CacheStore } from '../types' +import { RPCJsonSerializer } from '@orpc/client' +import { MemoryLock, nowInSeconds } from '@orpc/shared' +import { encodeCacheKey, isCacheEntryStale } from '../utils' + +export interface MemoryCacheStoreOptions { + /** + * Serializer used to encode non-string keys. + * + * @default RPCJsonSerializer + */ + serializer?: undefined | Public +} + +interface MemoryCacheStoreEntry { + output: unknown + /** + * The tags, and the version counter each had at set time, index-aligned. + * Both are absent together when the entry has no tags. + */ + tags?: readonly string[] + tagVersions?: readonly number[] + expiresAt: number | undefined + evictAt: number | undefined +} + +/** + * In-memory cache store with tag-based invalidation, intended for + * development, testing, and single-instance deployments. Expired and + * revalidated entries are removed lazily on the next `fetch` of their key, + * and concurrent callers of one key are coalesced within the process. + * + * @see {@link https://orpc.dev/docs/helpers/cache#adapters | Cache Helpers - Adapters} + */ +export class MemoryCacheStore implements CacheStore { + private readonly entries = new Map() + private readonly tagVersions = new Map() + private readonly serializer: Public + private readonly memoryLock = new MemoryLock() + + constructor(options: MemoryCacheStoreOptions = {}) { + this.serializer = options.serializer ?? new RPCJsonSerializer() + } + + async fetch(key: unknown, fill: () => Promise, options: CacheFetchOptions = {}): Promise { + const encodedKey = encodeCacheKey(key, this.serializer) + const entry = this.read(encodedKey) + + if (entry === undefined) { + return this.memoryLock.run(encodedKey, async (waited) => { + const current = waited ? this.read(encodedKey) : undefined + return current ?? this.write(encodedKey, await fill(), options) + }) + } + + if (isCacheEntryStale(entry)) { + const refresh = this.memoryLock.run(encodedKey, async (waited) => { + const current = waited ? this.read(encodedKey) : undefined + + if (current === undefined || isCacheEntryStale(current)) { + this.write(encodedKey, await fill(), options) + } + }) + + options.waitUntil?.(refresh) + } + + return entry + } + + async revalidate({ tags }: CacheRevalidateOptions): Promise { + for (const tag of tags) { + this.tagVersions.set(tag, (this.tagVersions.get(tag) ?? 0) + 1) + } + } + + private read(encodedKey: string): CacheEntry | undefined { + const entry = this.entries.get(encodedKey) + + if (!entry) { + return undefined + } + + if (entry.evictAt !== undefined && nowInSeconds() >= entry.evictAt) { + this.entries.delete(encodedKey) + return undefined + } + + const revalidated = entry.tags?.some( + (tag, index) => (this.tagVersions.get(tag) ?? 0) !== entry.tagVersions?.[index], + ) + + if (revalidated) { + this.entries.delete(encodedKey) + return undefined + } + + return { + output: entry.output, + tags: entry.tags, + expiresAt: entry.expiresAt, + } + } + + private write(encodedKey: string, output: unknown, options: CacheFetchOptions): CacheEntry { + const tags = options.tags + const expiresAt = options.ttl !== undefined ? nowInSeconds() + options.ttl : undefined + const evictAt = expiresAt !== undefined ? expiresAt + (options.swr ?? 0) : undefined + + this.entries.set(encodedKey, { + output, + tags, + tagVersions: tags?.map(tag => this.tagVersions.get(tag) ?? 0), + expiresAt, + evictAt, + }) + + return { output, tags, expiresAt } + } +} diff --git a/packages/cache/src/adapters/redis.test.ts b/packages/cache/src/adapters/redis.test.ts new file mode 100644 index 000000000..5dbf12b92 --- /dev/null +++ b/packages/cache/src/adapters/redis.test.ts @@ -0,0 +1,166 @@ +import { RPCSerializer } from '@orpc/client' +import { nowInSeconds, sleep } from '@orpc/shared' +import { createClient } from 'redis' +import { describeCacheStoreContract } from '../../tests/__shared__/store-contract' +import { RedisCacheStore } from './redis' + +const REDIS_URL = process.env.REDIS_URL + +describe.concurrent('redis cache store integration', { + skip: !REDIS_URL, + timeout: 20_000, +}, async () => { + const redis = createClient({ + url: REDIS_URL, + }) + + beforeAll(async () => { + await redis.connect() + }) + + function createTestingStore( + options: ConstructorParameters[1] = {}, + client = redis, + ) { + const prefix = `orpc-rediscachestore-${crypto.randomUUID()}:` + return { store: new RedisCacheStore(client, { prefix, ...options }), prefix } + } + + describeCacheStoreContract(() => createTestingStore().store) + + it('supports a custom serializer', async () => { + const serializer = new RPCSerializer() + const serializeSpy = vi.spyOn(serializer, 'serialize') + const deserializeSpy = vi.spyOn(serializer, 'deserialize') + const { store } = createTestingStore({ serializer }) + + await store.fetch('k', async () => ({ a: 1 })) + + await expect(store.fetch('k', async () => 'other')).resolves.toMatchObject({ output: { a: 1 } }) + expect(serializeSpy).toHaveBeenCalled() + expect(deserializeSpy).toHaveBeenCalled() + }) + + it('fills again at ttl without swr, and serves stale within the swr window while refreshing', async () => { + const { store } = createTestingStore() + + await store.fetch('no-swr', async () => 'v', { ttl: 1 }) + await store.fetch('swr', async () => 'v', { ttl: 1, swr: 10 }) + + await sleep(1500) + + await expect(store.fetch('no-swr', async () => 'refilled', { ttl: 1 })).resolves.toMatchObject({ output: 'refilled' }) + + const waitUntil = vi.fn() + const stale = await store.fetch('swr', async () => 'fresh', { ttl: 1, swr: 10, waitUntil }) + expect(stale.output).toBe('v') + expect(stale.expiresAt).toBeLessThanOrEqual(nowInSeconds()) + + expect(waitUntil).toHaveBeenCalledTimes(1) + await waitUntil.mock.calls[0]![0] + + const fresh = await store.fetch('swr', async () => 'other', { ttl: 1, swr: 10 }) + expect(fresh.output).toBe('fresh') + expect(fresh.expiresAt).toBeGreaterThan(stale.expiresAt!) + }) + + it('stores entries as hashes and tag counters under the prefixed key families, locking while filling', async () => { + const { store, prefix } = createTestingStore() + + await store.fetch('k', async () => { + await expect(redis.exists(`${prefix}l:k`)).resolves.toBe(1) + return 'v' + }, { tags: ['t'] }) + await store.revalidate({ tags: ['t'] }) + + await expect(redis.type(`${prefix}e:k`)).resolves.toBe('hash') + await expect(redis.exists(`${prefix}t:t`)).resolves.toBe(1) + await expect(redis.exists(`${prefix}l:k`)).resolves.toBe(0) + }) + + it('defaults to no prefix', async () => { + const store = new RedisCacheStore(redis) + const key = crypto.randomUUID() + + await store.fetch(key, async () => 'v') + + await expect(redis.exists(`e:${key}`)).resolves.toBe(1) + await expect(store.fetch(key, async () => 'other')).resolves.toMatchObject({ output: 'v' }) + }) + + it('treats tags missing from the snapshot as version zero', async () => { + const { store, prefix } = createTestingStore() + + await redis.hSet(`${prefix}e:k`, { output: JSON.stringify({ body: { json: 'v' } }), tags: '["t"]', tagVersions: '{}' }) + + await expect(store.fetch('k', async () => 'other')).resolves.toMatchObject({ output: 'v' }) + }) + + it('reloads scripts the server dropped, and rethrows other script errors', async () => { + const { store, prefix } = createTestingStore() + + await store.fetch('k', async () => 'v') + await redis.scriptFlush() + await expect(store.fetch('k', async () => 'other')).resolves.toMatchObject({ output: 'v' }) + + await redis.hSet(`${prefix}e:broken`, { output: '{}', tags: 'not json', tagVersions: '{}' }) + await expect(store.fetch('broken', async () => 'v')).rejects.toThrow() + }) + + it('lazily connects a closed client', async () => { + const lazyRedis = createClient({ url: REDIS_URL }) + const store = new RedisCacheStore(lazyRedis, { prefix: `orpc-redis-cache-store-${crypto.randomUUID()}:` }) + + expect(lazyRedis.isOpen).toBe(false) + await expect(store.fetch('k', async () => 'v')).resolves.toMatchObject({ output: 'v' }) + expect(lazyRedis.isOpen).toBe(true) + + await lazyRedis.destroy() + }) + + it('stays consistent under concurrent fetches and a revalidation on a shared tag', async () => { + const { store } = createTestingStore() + const keys = Array.from({ length: 20 }, (_, index) => `k${index}`) + + await Promise.all([ + ...keys.map(key => store.fetch(key, async () => key, { tags: ['t'] })), + store.revalidate({ tags: ['t'] }), + ]) + + const entries = await Promise.all(keys.map(key => store.fetch(key, async () => key, { tags: ['t'] }))) + expect(entries.map(entry => entry.output)).toEqual(keys) + }) + + it('frees waiters after lockTtl and leaves a lock taken over that way alone', async () => { + const { store: holderStore, prefix } = createTestingStore({ lockTtl: 1 }) + const waiterStore = new RedisCacheStore(redis, { prefix }) + let release!: () => void + const held = new Promise((resolve) => { + release = resolve + }) + let takenOver!: () => void + const takeover = new Promise((resolve) => { + takenOver = resolve + }) + + const holder = holderStore.fetch('k', async () => { + await takeover + return 'holder' + }) + await vi.waitFor(() => expect(redis.exists(`${prefix}l:k`)).resolves.toBe(1), { timeout: 5000 }) + + const waiter = waiterStore.fetch('k', async () => { + takenOver() + await held + return 'waiter' + }) + + await expect(holder).resolves.toMatchObject({ output: 'holder' }) + await expect(redis.exists(`${prefix}l:k`)).resolves.toBe(1) + + release() + await expect(waiter).resolves.toMatchObject({ output: 'waiter' }) + await expect(redis.exists(`${prefix}l:k`)).resolves.toBe(0) + await expect(holderStore.fetch('k', async () => 'other')).resolves.toMatchObject({ output: 'waiter' }) + }) +}) diff --git a/packages/cache/src/adapters/redis.ts b/packages/cache/src/adapters/redis.ts new file mode 100644 index 000000000..27f66e43f --- /dev/null +++ b/packages/cache/src/adapters/redis.ts @@ -0,0 +1,269 @@ +import type { Public } from '@orpc/shared' +import type { RedisClientType } from 'redis' +import type { CacheEntry, CacheFetchOptions, CacheRevalidateOptions, CacheStore } from '../types' +import { RPCJsonSerializer, RPCSerializer } from '@orpc/client' +import { nowInSeconds, sleep, stringifyJSON } from '@orpc/shared' +import { encodeCacheKey } from '../utils' + +/** + * Reads the entry as `[output, tags, expiresAt, shouldFill]`, dropping it when + * a tag was revalidated since it was stored. A missing or stale entry also + * takes the lock, and `shouldFill` reports whether this caller got it. + */ +const FETCH_SCRIPT = ` +local fields = redis.call('HMGET', KEYS[1], 'output', 'tags', 'tagVersions', 'expiresAt') +local output, tags, versions, expiresAt = fields[1], fields[2], fields[3], fields[4] + +if output and tags then + local names = cjson.decode(tags) + local snapshot = cjson.decode(versions) + local keys = {} + for i, name in ipairs(names) do + keys[i] = ARGV[3] .. name + end + local live = redis.call('MGET', unpack(keys)) + for i, name in ipairs(names) do + if tonumber(live[i] or 0) ~= (snapshot[name] or 0) then + redis.call('DEL', KEYS[1]) + output = false + break + end + end +end + +local stale = output and expiresAt and tonumber(expiresAt) <= tonumber(ARGV[4]) +local acquired = false +if not output or stale then + acquired = redis.call('SET', KEYS[2], ARGV[1], 'NX', 'PX', ARGV[2]) and true or false +end + +return { output or false, tags or false, expiresAt or false, acquired } +` + +/** + * Stores the entry with its tag versions snapshotted in the same step, then + * releases the caller's lock. + */ +const STORE_SCRIPT = ` +redis.call('DEL', KEYS[1]) +redis.call('HSET', KEYS[1], 'output', ARGV[2]) + +if ARGV[3] ~= '' then + local names = cjson.decode(ARGV[3]) + local keys = {} + for i, name in ipairs(names) do + keys[i] = ARGV[6] .. name + end + local live = redis.call('MGET', unpack(keys)) + local snapshot = {} + for i, name in ipairs(names) do + snapshot[name] = tonumber(live[i] or 0) + end + redis.call('HSET', KEYS[1], 'tags', ARGV[3], 'tagVersions', cjson.encode(snapshot)) +end + +if ARGV[4] ~= '' then + redis.call('HSET', KEYS[1], 'expiresAt', ARGV[4]) +end + +if ARGV[5] ~= '' then + redis.call('PEXPIRE', KEYS[1], ARGV[5]) +end + +if redis.call('GET', KEYS[2]) == ARGV[1] then + redis.call('DEL', KEYS[2]) +end +` + +/** + * Deletes the lock only while it still holds the caller's token, leaving one + * that expired and was taken over alone. + */ +const RELEASE_LOCK_SCRIPT = ` +if redis.call('GET', KEYS[1]) == ARGV[1] then + return redis.call('DEL', KEYS[1]) +end +return 0 +` + +const REVALIDATE_SCRIPT = ` +for _, key in ipairs(KEYS) do + redis.call('INCR', key) +end +` + +/** + * Replies arrive parsed from some clients, such as Upstash, and raw from others. + */ +function parseReply(value: unknown): unknown { + return typeof value === 'string' ? JSON.parse(value) : value +} + +export interface BaseRedisCacheStoreOptions { + /** + * The prefix to use for Redis keys. + * + * @default undefined + */ + prefix?: string + + /** + * Serializer for cached outputs. + * + * @default RPCSerializer + */ + serializer?: undefined | Public + + /** + * How long a lock may be held, in seconds, so a crashed holder frees its + * waiters. A fill outlasting it lets the next waiter fill as well. + * + * @default 10 + */ + lockTtl?: number +} + +/** + * Cache store for Redis-compatible databases, driven by Lua scripts so a hit + * is one round trip and a miss two. Entries are hashes retained for + * `ttl + swr`; tag counters have no expiry since expiring one would resurrect + * stale entries. Revalidated entries are removed lazily on the next `fetch` + * of their key. Concurrent callers of one key are coalesced through a lock + * taken in the same script that reads the entry, so it spans processes. + * Subclasses only run the scripts through their client. + * + * @see {@link https://orpc.dev/docs/helpers/cache#adapters | Cache Helpers - Adapters} + */ +export abstract class BaseRedisCacheStore implements CacheStore { + private readonly prefix: string + private readonly tagPrefix: string + private readonly serializer: Public + private readonly lockTtl: number + private readonly keySerializer = new RPCJsonSerializer() + + constructor(options: BaseRedisCacheStoreOptions = {}) { + this.prefix = options.prefix ?? '' + this.tagPrefix = `${this.prefix}t:` + this.serializer = options.serializer ?? new RPCSerializer() + this.lockTtl = options.lockTtl ?? 10 + } + + async fetch(key: unknown, fill: () => Promise, options: CacheFetchOptions = {}): Promise { + const encodedKey = encodeCacheKey(key, this.keySerializer) + const entryKey = `${this.prefix}e:${encodedKey}` + const lockKey = `${this.prefix}l:${encodedKey}` + const token = crypto.randomUUID() + + while (true) { + const [output, tags, expiresAt, shouldFill] = await this.run( + FETCH_SCRIPT, + [entryKey, lockKey], + [token, String(this.lockTtl * 1000), this.tagPrefix, String(nowInSeconds())], + ) as [unknown, unknown, unknown, unknown] + + if (output !== null) { + const entry: CacheEntry = { + output: this.serializer.deserialize((parseReply(output) as { body?: unknown }).body as any), + tags: tags === null ? undefined : parseReply(tags) as string[], + expiresAt: expiresAt === null ? undefined : Number(expiresAt), + } + + if (shouldFill) { + const refresh = this.store(entryKey, lockKey, token, fill, options) + options.waitUntil?.(refresh) + } + + return entry + } + + if (shouldFill) { + return this.store(entryKey, lockKey, token, fill, options) + } + + await sleep(50) + } + } + + async revalidate({ tags }: CacheRevalidateOptions): Promise { + await this.run(REVALIDATE_SCRIPT, tags.map(tag => `${this.tagPrefix}${tag}`), []) + } + + /** + * Runs a Lua script through the client, by sha where the client allows it. + */ + protected abstract run(script: string, keys: string[], args: string[]): Promise + + private async store(entryKey: string, lockKey: string, token: string, fill: () => Promise, options: CacheFetchOptions): Promise { + let output: unknown + let serialized: string + + try { + output = await fill() + serialized = stringifyJSON({ body: this.serializer.serialize(output) }) + } + catch (error) { + await this.run(RELEASE_LOCK_SCRIPT, [lockKey], [token]) + throw error + } + + const tags = options.tags?.length ? options.tags : undefined + const expiresAt = options.ttl !== undefined ? nowInSeconds() + options.ttl : undefined + const retention = options.ttl !== undefined ? options.ttl + (options.swr ?? 0) : undefined + + await this.run(STORE_SCRIPT, [entryKey, lockKey], [ + token, + serialized, + tags !== undefined ? stringifyJSON(tags) : '', + expiresAt !== undefined ? String(expiresAt) : '', + retention !== undefined ? String(Math.ceil(retention * 1000)) : '', + this.tagPrefix, + ]) + + return { output, tags, expiresAt } + } +} + +export type RedisCacheStoreOptions = BaseRedisCacheStoreOptions + +/** + * Cache store adapter for Redis. Connects the client lazily when needed and + * runs the scripts by sha, loading each once per client and again if the + * server dropped it. + * + * @see {@link https://orpc.dev/docs/helpers/cache#adapters | Cache Helpers - Adapters} + */ +export class RedisCacheStore extends BaseRedisCacheStore { + private readonly scriptShas = new Map>>() + + constructor( + private readonly redis: RedisClientType, + options: RedisCacheStoreOptions = {}, + ) { + super(options) + } + + protected async run(script: string, keys: string[], args: string[], reloaded = false): Promise { + if (!this.redis.isOpen) { + await this.redis.connect() + } + + let sha = this.scriptShas.get(script) + + if (sha === undefined) { + sha = await this.redis.scriptLoad(script) + this.scriptShas.set(script, sha) + } + + try { + return await this.redis.evalSha(sha, { keys, arguments: args }) + } + catch (error) { + if (!reloaded && error instanceof Error && error.message.startsWith('NOSCRIPT')) { + this.scriptShas.delete(script) + return await this.run(script, keys, args, true) + } + + throw error + } + } +} diff --git a/packages/cache/src/adapters/upstash.test.ts b/packages/cache/src/adapters/upstash.test.ts new file mode 100644 index 000000000..2847fa31e --- /dev/null +++ b/packages/cache/src/adapters/upstash.test.ts @@ -0,0 +1,177 @@ +import { RPCSerializer } from '@orpc/client' +import { nowInSeconds, sleep } from '@orpc/shared' +import { Redis } from '@upstash/redis' +import { describeCacheStoreContract } from '../../tests/__shared__/store-contract' +import { UpstashCacheStore } from './upstash' + +const UPSTASH_REDIS_REST_URL = process.env.UPSTASH_REDIS_REST_URL +const UPSTASH_REDIS_REST_TOKEN = process.env.UPSTASH_REDIS_REST_TOKEN + +/** + * These tests depend on a real Upstash redis server — make sure to set the + * `UPSTASH_REDIS_REST_URL`, `UPSTASH_REDIS_REST_TOKEN` envs. + */ +describe.concurrent('upstash cache store integration', { + // TODO: Upstash is not compatible with Node 26 yet — temporarily disable these tests and revisit in the future. + skip: !UPSTASH_REDIS_REST_URL || !UPSTASH_REDIS_REST_TOKEN || process.versions.node.startsWith('26.'), + timeout: 20_000, +}, () => { + const redis = new Redis({ + url: UPSTASH_REDIS_REST_URL, + token: UPSTASH_REDIS_REST_TOKEN, + }) + + function createTestingStore( + options: ConstructorParameters[1] = {}, + client = redis, + ) { + const prefix = `orpc-upstashcachestore-${crypto.randomUUID()}:` + return { store: new UpstashCacheStore(client, { prefix, ...options }), prefix } + } + + describeCacheStoreContract(() => createTestingStore().store) + + it('supports a custom serializer', async () => { + const serializer = new RPCSerializer() + const serializeSpy = vi.spyOn(serializer, 'serialize') + const deserializeSpy = vi.spyOn(serializer, 'deserialize') + const { store } = createTestingStore({ serializer }) + + await store.fetch('k', async () => ({ a: 1 })) + + await expect(store.fetch('k', async () => 'other')).resolves.toMatchObject({ output: { a: 1 } }) + expect(serializeSpy).toHaveBeenCalled() + expect(deserializeSpy).toHaveBeenCalled() + }) + + it('fills again at ttl without swr, and serves stale within the swr window while refreshing', async () => { + const { store } = createTestingStore() + + await store.fetch('no-swr', async () => 'v', { ttl: 1 }) + await store.fetch('swr', async () => 'v', { ttl: 1, swr: 10 }) + + await sleep(1500) + + await expect(store.fetch('no-swr', async () => 'refilled', { ttl: 1 })).resolves.toMatchObject({ output: 'refilled' }) + + const waitUntil = vi.fn() + const stale = await store.fetch('swr', async () => 'fresh', { ttl: 1, swr: 10, waitUntil }) + expect(stale.output).toBe('v') + expect(stale.expiresAt).toBeLessThanOrEqual(nowInSeconds()) + + expect(waitUntil).toHaveBeenCalledTimes(1) + await waitUntil.mock.calls[0]![0] + + const fresh = await store.fetch('swr', async () => 'other', { ttl: 1, swr: 10 }) + expect(fresh.output).toBe('fresh') + expect(fresh.expiresAt).toBeGreaterThan(stale.expiresAt!) + }) + + it('stores entries as hashes and tag counters under the prefixed key families, locking while filling', async () => { + const { store, prefix } = createTestingStore() + + await store.fetch('k', async () => { + await expect(redis.exists(`${prefix}l:k`)).resolves.toBe(1) + return 'v' + }, { tags: ['t'] }) + await store.revalidate({ tags: ['t'] }) + + await expect(redis.type(`${prefix}e:k`)).resolves.toBe('hash') + await expect(redis.exists(`${prefix}t:t`)).resolves.toBe(1) + await expect(redis.exists(`${prefix}l:k`)).resolves.toBe(0) + }) + + it('defaults to no prefix', async () => { + const store = new UpstashCacheStore(redis) + const key = crypto.randomUUID() + + await store.fetch(key, async () => 'v') + + await expect(redis.exists(`e:${key}`)).resolves.toBe(1) + await expect(store.fetch(key, async () => 'other')).resolves.toMatchObject({ output: 'v' }) + }) + + it('treats tags missing from the snapshot as version zero', async () => { + const { store, prefix } = createTestingStore() + + await redis.hset(`${prefix}e:k`, { output: JSON.stringify({ body: { json: 'v' } }), tags: '["t"]', tagVersions: '{}' }) + + await expect(store.fetch('k', async () => 'other')).resolves.toMatchObject({ output: 'v' }) + }) + + it('reloads scripts the server dropped, and rethrows other script errors', async () => { + const { store, prefix } = createTestingStore() + + await store.fetch('k', async () => 'v') + await redis.scriptFlush() + await expect(store.fetch('k', async () => 'other')).resolves.toMatchObject({ output: 'v' }) + + await redis.hset(`${prefix}e:broken`, { output: '{}', tags: 'not json', tagVersions: '{}' }) + await expect(store.fetch('broken', async () => 'v')).rejects.toThrow() + }) + + it('reads entries when the client does not parse JSON replies', async () => { + const rawRedis = new Redis({ + url: UPSTASH_REDIS_REST_URL, + token: UPSTASH_REDIS_REST_TOKEN, + automaticDeserialization: false, + }) + const { store } = createTestingStore({}, rawRedis) + + await store.fetch('k', async () => ({ a: 1 }), { tags: ['t'], ttl: 60 }) + + const entry = await store.fetch('k', async () => 'other', { tags: ['t'], ttl: 60 }) + expect(entry.output).toEqual({ a: 1 }) + expect(entry.tags).toEqual(['t']) + expect(entry.expiresAt).toBeGreaterThan(nowInSeconds()) + + await store.revalidate({ tags: ['t'] }) + await expect(store.fetch('k', async () => 'refilled', { tags: ['t'] })).resolves.toMatchObject({ output: 'refilled' }) + }) + + it('stays consistent under concurrent fetches and a revalidation on a shared tag', async () => { + const { store } = createTestingStore() + const keys = Array.from({ length: 20 }, (_, index) => `k${index}`) + + await Promise.all([ + ...keys.map(key => store.fetch(key, async () => key, { tags: ['t'] })), + store.revalidate({ tags: ['t'] }), + ]) + + const entries = await Promise.all(keys.map(key => store.fetch(key, async () => key, { tags: ['t'] }))) + expect(entries.map(entry => entry.output)).toEqual(keys) + }) + + it('frees waiters after lockTtl and leaves a lock taken over that way alone', async () => { + const { store: holderStore, prefix } = createTestingStore({ lockTtl: 1 }) + const waiterStore = new UpstashCacheStore(redis, { prefix }) + let release!: () => void + const held = new Promise((resolve) => { + release = resolve + }) + let takenOver!: () => void + const takeover = new Promise((resolve) => { + takenOver = resolve + }) + + const holder = holderStore.fetch('k', async () => { + await takeover + return 'holder' + }) + await vi.waitFor(() => expect(redis.exists(`${prefix}l:k`)).resolves.toBe(1), { timeout: 5000 }) + + const waiter = waiterStore.fetch('k', async () => { + takenOver() + await held + return 'waiter' + }) + + await expect(holder).resolves.toMatchObject({ output: 'holder' }) + await expect(redis.exists(`${prefix}l:k`)).resolves.toBe(1) + + release() + await expect(waiter).resolves.toMatchObject({ output: 'waiter' }) + await expect(redis.exists(`${prefix}l:k`)).resolves.toBe(0) + await expect(holderStore.fetch('k', async () => 'other')).resolves.toMatchObject({ output: 'waiter' }) + }) +}) diff --git a/packages/cache/src/adapters/upstash.ts b/packages/cache/src/adapters/upstash.ts new file mode 100644 index 000000000..3292c842e --- /dev/null +++ b/packages/cache/src/adapters/upstash.ts @@ -0,0 +1,34 @@ +import type { Redis } from '@upstash/redis' +import type { BaseRedisCacheStoreOptions } from './redis' +import { BaseRedisCacheStore } from './redis' + +export type UpstashCacheStoreOptions = BaseRedisCacheStoreOptions + +/** + * Cache store adapter for Upstash Redis. Shares its key and entry format with + * `RedisCacheStore`, so both can serve the same database, and runs the + * scripts by sha through the client's own script cache. + * + * @see {@link https://orpc.dev/docs/helpers/cache#adapters | Cache Helpers - Adapters} + */ +export class UpstashCacheStore extends BaseRedisCacheStore { + private readonly scripts = new Map>() + + constructor( + private readonly redis: Redis, + options: UpstashCacheStoreOptions = {}, + ) { + super(options) + } + + protected run(script: string, keys: string[], args: string[]): Promise { + let prepared = this.scripts.get(script) + + if (prepared === undefined) { + prepared = this.redis.createScript(script) + this.scripts.set(script, prepared) + } + + return prepared.exec(keys, args) + } +} diff --git a/packages/cache/src/adapters/vercel.test.ts b/packages/cache/src/adapters/vercel.test.ts new file mode 100644 index 000000000..22d222d34 --- /dev/null +++ b/packages/cache/src/adapters/vercel.test.ts @@ -0,0 +1,151 @@ +import type { RuntimeCache } from '@vercel/functions' +import { RPCSerializer } from '@orpc/client' +import { getCache } from '@vercel/functions' +import { describeCacheStoreContract } from '../../tests/__shared__/store-contract' +import { VercelCacheStore } from './vercel' + +describe('vercelCacheStore', () => { + describe('against the in-memory getCache fallback', () => { + function createTestingStore() { + return new VercelCacheStore({ + cache: getCache({ namespace: crypto.randomUUID() }), + }) + } + + describeCacheStoreContract(createTestingStore) + + it('defaults to getCache when no cache is given', async () => { + const store = new VercelCacheStore() + const key = crypto.randomUUID() + + await store.fetch(key, async () => 'v') + + await expect(store.fetch(key, async () => 'other')).resolves.toMatchObject({ output: 'v' }) + }) + }) + + describe('against a mocked runtime cache', () => { + function createMockedCache() { + const values = new Map() + + const cache = { + get: vi.fn(async (key: string) => values.get(key) ?? null), + set: vi.fn(async (key: string, value: unknown) => { + values.set(key, value) + }), + delete: vi.fn(async (key: string) => { + values.delete(key) + }), + expireTag: vi.fn(async () => {}), + } satisfies RuntimeCache + + return cache + } + + beforeEach(() => { + vi.useFakeTimers() + vi.setSystemTime(0) + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('maps ttl + swr to the retention it passes on', async () => { + const cache = createMockedCache() + const store = new VercelCacheStore({ cache }) + + await store.fetch('k', async () => 'v', { tags: ['t'], ttl: 1, swr: 1 }) + + expect(cache.set).toHaveBeenCalledWith('k', expect.objectContaining({ tags: ['t'], expiresAt: 1, evictAt: 2 }), { tags: ['t'], ttl: 2 }) + }) + + it('maps a ttl without swr to its exact retention', async () => { + const cache = createMockedCache() + const store = new VercelCacheStore({ cache }) + + await store.fetch('k', async () => 'v', { ttl: 1 }) + + expect(cache.set).toHaveBeenCalledWith('k', expect.objectContaining({ expiresAt: 1, evictAt: 1 }), { ttl: 1 }) + }) + + it('omits ttl and tags options when unset', async () => { + const cache = createMockedCache() + const store = new VercelCacheStore({ cache }) + + await store.fetch('k', async () => 'v') + + expect(cache.set).toHaveBeenCalledWith('k', expect.objectContaining({ tags: undefined }), {}) + }) + + it('serves stale entries within the swr window, refreshes through waitUntil, and evicts defensively', async () => { + const cache = createMockedCache() + const store = new VercelCacheStore({ cache }) + + await store.fetch('k', async () => 'v', { ttl: 1, swr: 1 }) + + vi.setSystemTime(1200) // past ttl, within swr + const waitUntil = vi.fn() + await expect(store.fetch('k', async () => { + throw new Error('handler down') + }, { ttl: 1, swr: 1, waitUntil })).resolves.toEqual({ output: 'v', tags: undefined, expiresAt: 1 }) + await expect(waitUntil.mock.calls[0]![0]).rejects.toThrow('handler down') + + vi.setSystemTime(2000) // past ttl + swr, backend has not evicted yet + await expect(store.fetch('k', async () => 'refilled', { ttl: 1, swr: 1 })).resolves.toMatchObject({ output: 'refilled' }) + expect(cache.delete).toHaveBeenCalledWith('k') + }) + + it('refreshes once for concurrent stale hits, and again when the first refresh failed', async () => { + const cache = createMockedCache() + const store = new VercelCacheStore({ cache }) + await store.fetch('k', async () => 'v', { ttl: 1, swr: 1 }) + + vi.setSystemTime(1200) + let finish!: (output: string) => void + const fill = vi.fn(() => new Promise((resolve) => { + finish = resolve + })) + const waitUntil = vi.fn() + + await store.fetch('k', fill, { ttl: 1, swr: 1, waitUntil }) + await store.fetch('k', fill, { ttl: 1, swr: 1, waitUntil }) + finish('fresh') + await Promise.all(waitUntil.mock.calls.map(([refresh]) => refresh)) + expect(fill).toHaveBeenCalledTimes(1) + await expect(store.fetch('k', fill, { ttl: 1, swr: 1 })).resolves.toMatchObject({ output: 'fresh' }) + + vi.setSystemTime(2400) // stale again + let fail!: (error: Error) => void + const failingFill = vi.fn() + .mockImplementationOnce(() => new Promise((_, reject) => { + fail = reject + })) + .mockResolvedValue('fresher') + const waitUntilAgain = vi.fn() + + await store.fetch('k', failingFill, { ttl: 1, swr: 1, waitUntil: waitUntilAgain }) + await store.fetch('k', failingFill, { ttl: 1, swr: 1, waitUntil: waitUntilAgain }) + fail(new Error('handler down')) + + await expect(waitUntilAgain.mock.calls[0]![0]).rejects.toThrow('handler down') + await waitUntilAgain.mock.calls[1]![0] + expect(failingFill).toHaveBeenCalledTimes(2) + await expect(store.fetch('k', failingFill, { ttl: 1, swr: 1 })).resolves.toMatchObject({ output: 'fresher' }) + }) + + it('supports a custom serializer', async () => { + const cache = createMockedCache() + const serializer = new RPCSerializer() + const serializeSpy = vi.spyOn(serializer, 'serialize') + const deserializeSpy = vi.spyOn(serializer, 'deserialize') + const store = new VercelCacheStore({ cache, serializer }) + + await store.fetch('k', async () => ({ a: 1 })) + + await expect(store.fetch('k', async () => 'other')).resolves.toMatchObject({ output: { a: 1 } }) + expect(serializeSpy).toHaveBeenCalled() + expect(deserializeSpy).toHaveBeenCalled() + }) + }) +}) diff --git a/packages/cache/src/adapters/vercel.ts b/packages/cache/src/adapters/vercel.ts new file mode 100644 index 000000000..8c3520ccf --- /dev/null +++ b/packages/cache/src/adapters/vercel.ts @@ -0,0 +1,131 @@ +import type { Public } from '@orpc/shared' +import type { RuntimeCache } from '@vercel/functions' +import type { CacheEntry, CacheFetchOptions, CacheRevalidateOptions, CacheStore } from '../types' +import { RPCJsonSerializer, RPCSerializer } from '@orpc/client' +import { MemoryLock, nowInSeconds } from '@orpc/shared' +import { getCache } from '@vercel/functions' +import { encodeCacheKey, isCacheEntryStale } from '../utils' + +interface VercelCacheStoreEnvelope { + /** + * The cached output, encoded with the store's serializer. + */ + output: unknown + tags?: readonly string[] + expiresAt?: number | undefined + evictAt?: number | undefined +} + +export interface VercelCacheStoreOptions { + /** + * The Vercel Runtime Cache to use. + * + * @default getCache() + */ + cache?: RuntimeCache + + /** + * Serializer for cached outputs. + * + * @default RPCSerializer + */ + serializer?: undefined | Public +} + +/** + * Cache store adapter for the Vercel Runtime Cache. Tags are expired + * natively via `expireTag`, and entries are retained for `ttl + swr`. + * Outside Vercel, the default `getCache()` falls back to an in-memory + * cache. Concurrent callers of one key are coalesced within the process, + * since the Runtime Cache has no atomic primitive. + * + * @see {@link https://orpc.dev/docs/helpers/cache#adapters | Cache Helpers - Adapters} + */ +export class VercelCacheStore implements CacheStore { + private readonly cache: RuntimeCache + private readonly serializer: Public + private readonly memoryLock = new MemoryLock() + + /** + * Key encoding has no serializer option, so one is built here rather than + * per call by {@link encodeCacheKey}. + */ + private readonly keySerializer = new RPCJsonSerializer() + + constructor(options: VercelCacheStoreOptions = {}) { + this.cache = options.cache ?? getCache() + this.serializer = options.serializer ?? new RPCSerializer() + } + + async fetch(key: unknown, fill: () => Promise, options: CacheFetchOptions = {}): Promise { + const encodedKey = encodeCacheKey(key, this.keySerializer) + const entry = await this.read(encodedKey) + + if (entry === undefined) { + return this.memoryLock.run(encodedKey, async (waited) => { + const current = waited ? await this.read(encodedKey) : undefined + return current ?? this.write(encodedKey, await fill(), options) + }) + } + + if (isCacheEntryStale(entry)) { + const refresh = this.memoryLock.run(encodedKey, async (waited) => { + const current = waited ? await this.read(encodedKey) : undefined + + if (current === undefined || isCacheEntryStale(current)) { + await this.write(encodedKey, await fill(), options) + } + }) + + options.waitUntil?.(refresh) + } + + return entry + } + + async revalidate({ tags }: CacheRevalidateOptions): Promise { + await this.cache.expireTag([...tags]) + } + + private async read(encodedKey: string): Promise { + const envelope = await this.cache.get(encodedKey) as VercelCacheStoreEnvelope | null | undefined + + if (envelope == null) { + return undefined + } + + if (envelope.evictAt !== undefined && nowInSeconds() >= envelope.evictAt) { + await this.cache.delete(encodedKey) + return undefined + } + + return { + output: this.serializer.deserialize(envelope.output as any), + tags: envelope.tags, + expiresAt: envelope.expiresAt, + } + } + + private async write(encodedKey: string, output: unknown, options: CacheFetchOptions): Promise { + const serialized = this.serializer.serialize(output) + + const tags = options.tags + const retention = options.ttl !== undefined ? options.ttl + (options.swr ?? 0) : undefined + const expiresAt = options.ttl !== undefined ? nowInSeconds() + options.ttl : undefined + const evictAt = retention !== undefined ? nowInSeconds() + retention : undefined + + const envelope: VercelCacheStoreEnvelope = { + output: serialized, + tags, + expiresAt, + evictAt, + } + + await this.cache.set(encodedKey, envelope, { + ...(tags?.length ? { tags: [...tags] } : {}), + ...(retention !== undefined ? { ttl: retention } : {}), + }) + + return { output, tags, expiresAt } + } +} diff --git a/packages/cache/src/handler-plugin.test.ts b/packages/cache/src/handler-plugin.test.ts new file mode 100644 index 000000000..29b7e067a --- /dev/null +++ b/packages/cache/src/handler-plugin.test.ts @@ -0,0 +1,243 @@ +import type { AnyProcedure } from '@orpc/server' +import type { StandardHandlerPlugin } from '@orpc/server/standard' +import type { StandardHeaders } from '@standardserver/core' +import type { CacheHandlerPluginContext, CacheHandlerPluginHeader } from './handler-plugin' +import type { CacheContext } from './types' +import { call, ORPCError, os } from '@orpc/server' +import { RPCHandler } from '@orpc/server/fetch' +import { decodeCacheTagHeader, toArray } from '@orpc/shared' +import { MemoryCacheStore } from './adapters/memory' +import { CACHE_HANDLER_PLUGIN_CONTEXT_SYMBOL, CacheHandlerPlugin } from './handler-plugin' +import { cache, revalidate } from './middleware' + +type RecordedChecks = Exclude +type PartialCheck = Partial & { tags: readonly string[] } + +/** + * Sets response headers from inside the cache plugin's interceptor, standing + * in for a handler or inner plugin that set its own. + */ +function presetHeadersPlugin(preset: StandardHeaders): StandardHandlerPlugin { + return { + name: '~preset-headers', + init: options => ({ + ...options, + interceptors: [...toArray(options.interceptors), async (interceptorOptions) => { + const response = await interceptorOptions.next() + return { ...response, headers: { ...response.headers, ...preset } } + }], + }), + } +} + +function createTestingHandler(headers?: readonly CacheHandlerPluginHeader[], preset?: StandardHeaders) { + const handlerFn = vi.fn() + const handler = new RPCHandler(os.handler(handlerFn), { + allowMethods: ['GET', 'POST'], + plugins: [ + new CacheHandlerPlugin({ headers: headers ?? [] }), + // Registered last so its interceptor runs innermost, before the plugin looks. + ...preset ? [presetHeadersPlugin(preset)] : [], + ], + }) + + return { + handlerFn, + + /** + * Records checks against the called procedure and path, as the + * middlewares do, then runs `then` inside the same handler call. Each + * check may override either field to simulate a nested call. + */ + record(checks: { caches?: PartialCheck[], revalidations?: PartialCheck[] }, then?: () => void) { + handlerFn.mockImplementationOnce(({ context, path, procedure }) => { + const recorded: RecordedChecks = context[CACHE_HANDLER_PLUGIN_CONTEXT_SYMBOL] + recorded.caches.push(...toArray(checks.caches).map(check => ({ path, procedure, ...check }))) + recorded.revalidations.push(...toArray(checks.revalidations).map(check => ({ path, procedure, ...check }))) + then?.() + }) + }, + + async handle(init?: RequestInit): Promise { + const { response } = await handler.handle(new Request('http://localhost:3000', init)) + return response! + }, + } +} + +const POST = { + method: 'POST', + body: JSON.stringify({}), + headers: { 'content-type': 'application/json' }, +} satisfies RequestInit + +describe('cacheHandlerPlugin', () => { + it('does nothing until headers are configured', async () => { + const { handlerFn, handle } = createTestingHandler() + handlerFn.mockImplementationOnce(({ context }) => { + expect(context[CACHE_HANDLER_PLUGIN_CONTEXT_SYMBOL]).toBeUndefined() + }) + + const response = await handle() + + expect(handlerFn).toHaveBeenCalledTimes(1) + expect(response.headers.get('orpc-cache-tag')).toBe(null) + }) + + describe('orpc-cache-tag & orpc-cache-tag-invalidation', () => { + const headers = ['orpc-cache-tag', 'orpc-cache-tag-invalidation'] as const + + it('reflects the first check of each kind belonging to the called procedure', async () => { + const { record, handle } = createTestingHandler(headers) + record({ + caches: [{ tags: ['planets', 'planet:1'] }, { tags: ['ignored'] }], + revalidations: [{ tags: ['revalidated'] }, { tags: ['ignored'] }], + }) + + const response = await handle() + + expect(response.headers.get('orpc-cache-tag')).toBe('planets,planet:1') + expect(response.headers.get('orpc-cache-tag-invalidation')).toBe('revalidated') + }) + + it('sets each header only when its own kind of check ran', async () => { + const cacheOnly = createTestingHandler(headers) + cacheOnly.record({ caches: [{ tags: ['a'] }] }) + const first = await cacheOnly.handle() + + expect(first.headers.get('orpc-cache-tag')).toBe('a') + expect(first.headers.get('orpc-cache-tag-invalidation')).toBe(null) + + const revalidationOnly = createTestingHandler(headers) + revalidationOnly.record({ revalidations: [{ tags: ['b'] }] }) + const second = await revalidationOnly.handle() + + expect(second.headers.get('orpc-cache-tag')).toBe(null) + expect(second.headers.get('orpc-cache-tag-invalidation')).toBe('b') + }) + + it('ignores checks recorded for other procedures or paths', async () => { + const other = os.handler(() => 'other') + const { record, handle } = createTestingHandler(headers) + record({ + caches: [ + { procedure: other as AnyProcedure, tags: ['other-procedure'] }, + { path: ['nested'], tags: ['other-path'] }, + ], + revalidations: [{ procedure: other as AnyProcedure, tags: ['other-procedure'] }], + }) + + const response = await handle() + + expect(response.headers.get('orpc-cache-tag')).toBe(null) + expect(response.headers.get('orpc-cache-tag-invalidation')).toBe(null) + }) + + it('skips headers when no check ran, or its tags are empty', async () => { + const noChecks = await createTestingHandler(headers).handle() + + expect(noChecks.headers.get('orpc-cache-tag')).toBe(null) + expect(noChecks.headers.get('orpc-cache-tag-invalidation')).toBe(null) + + const emptyTags = createTestingHandler(headers) + emptyTags.record({ caches: [{ tags: [] }], revalidations: [{ tags: [] }] }) + const response = await emptyTags.handle() + + expect(response.headers.get('orpc-cache-tag')).toBe(null) + expect(response.headers.get('orpc-cache-tag-invalidation')).toBe(null) + }) + + it('skips headers on error responses', async () => { + const { record, handle } = createTestingHandler(headers) + record({ caches: [{ tags: ['planets'] }] }, () => { + throw new ORPCError('INTERNAL_SERVER_ERROR') + }) + + const response = await handle() + + expect(response.status).toBe(500) + expect(response.headers.get('orpc-cache-tag')).toBe(null) + }) + + it('percent-encodes tags containing special characters', async () => { + const { record, handle } = createTestingHandler(headers) + record({ caches: [{ tags: ['a,b', 'tiếng việt'] }] }) + + const header = (await handle()).headers.get('orpc-cache-tag')! + + expect(header).toBe('a%2Cb,ti%E1%BA%BFng%20vi%E1%BB%87t') + expect(decodeCacheTagHeader(header)).toEqual(['a,b', 'tiếng việt']) + }) + }) + + describe('cache-control & cache-tag', () => { + const headers = ['cache-control', 'cache-tag'] as const + + it('reflects the root check, leaving unconfigured headers alone', async () => { + const { record, handle } = createTestingHandler(headers) + record({ caches: [{ tags: ['planets', 'a,b'], ttl: 2, swr: 1 }] }) + + const response = await handle() + + expect(response.headers.get('orpc-cache-tag')).toBe(null) // only configured headers are set + expect(response.headers.get('cache-tag')).toBe('planets,a%2Cb') + expect(response.headers.get('cache-control')).toBe('public, max-age=2, stale-while-revalidate=1') + }) + + it('holds entries without a ttl for a year, and skips Cache-Tag without tags', async () => { + const { record, handle } = createTestingHandler(headers) + record({ caches: [{ tags: [] }] }) + + const response = await handle() + + expect(response.headers.get('cache-tag')).toBe(null) + expect(response.headers.get('cache-control')).toBe('public, max-age=31536000') + }) + + it('reflects the root check whatever the request method', async () => { + const { record, handle } = createTestingHandler(headers) + record({ caches: [{ tags: ['planets'], ttl: 2 }] }) + + const response = await handle(POST) + + expect(response.headers.get('cache-tag')).toBe('planets') + expect(response.headers.get('cache-control')).toBe('public, max-age=2') + }) + + it('skips HTTP caching headers without a root cache check', async () => { + const { record, handle } = createTestingHandler(headers) + record({ revalidations: [{ tags: ['planets'] }] }) + + const response = await handle() + + expect(response.headers.get('cache-tag')).toBe(null) + expect(response.headers.get('cache-control')).toBe(null) + }) + }) + + it('only reflects the tags of the procedure the client called in nested calls', async () => { + const inner = os + .$context() + .use(cache({ key: 'inner', tags: ['inner-tag'] })) + .use(revalidate({ tags: ['inner-revalidated'] })) + .handler(() => 'inner') + + const outer = os + .$context() + .use(cache({ key: 'outer', tags: ['outer-tag'] })) + .use(revalidate({ tags: ['outer-revalidated'] })) + .handler(async ({ context }) => `outer:${await call(inner, undefined, { context })}`) + + const handler = new RPCHandler({ outer, inner }, { + allowMethods: ['GET'], + plugins: [new CacheHandlerPlugin({ headers: ['orpc-cache-tag', 'orpc-cache-tag-invalidation'] })], + }) + + const { response } = await handler.handle(new Request('http://localhost:3000/outer'), { + context: { 'cache/store': new MemoryCacheStore() }, + }) + + expect(response!.headers.get('orpc-cache-tag')).toBe('outer-tag') + expect(response!.headers.get('orpc-cache-tag-invalidation')).toBe('outer-revalidated') + }) +}) diff --git a/packages/cache/src/handler-plugin.ts b/packages/cache/src/handler-plugin.ts new file mode 100644 index 000000000..9a253fa3b --- /dev/null +++ b/packages/cache/src/handler-plugin.ts @@ -0,0 +1,132 @@ +import type { AnyProcedure, Context } from '@orpc/server' +import type { StandardHandlerInterceptor, StandardHandlerOptions, StandardHandlerPlugin } from '@orpc/server/standard' +import type { StandardHeaders } from '@standardserver/core' +import { encodeCacheTagHeader, isDeepEqual, toArray } from '@orpc/shared' + +export const CACHE_HANDLER_PLUGIN_CONTEXT_SYMBOL: unique symbol = Symbol.for('ORPC_CACHE_HANDLER_PLUGIN_CONTEXT') + +export interface CacheHandlerPluginContext { + [CACHE_HANDLER_PLUGIN_CONTEXT_SYMBOL]?: { + /** + * The cache lookups performed during this request, in the order they ran. + * `ttl` carries the remaining freshness in seconds on hits and the + * resolved fresh lifetime on stores. + */ + caches: { procedure: AnyProcedure, path: readonly string[], tags?: readonly string[] | undefined, ttl?: number | undefined, swr?: number | undefined }[] + + /** + * The tag revalidations committed during this request, in the order they ran. + */ + revalidations: { procedure: AnyProcedure, path: readonly string[], tags: readonly string[] }[] + } +} + +/** + * The response headers the cache handler plugin can set. + * + * @see {@link https://orpc.dev/docs/helpers/cache#handler-plugin | Cache Helpers - Handler Plugin} + */ +export type CacheHandlerPluginHeader + = | 'orpc-cache-tag' + | 'orpc-cache-tag-invalidation' + | 'cache-control' + | 'cache-tag' + +export interface CacheHandlerPluginOptions<_T extends Context> { + /** + * The response headers to set from the root procedure's cache activity; + * only listed headers are set. `orpc-cache-tag` carries the tags the + * response depends on and `orpc-cache-tag-invalidation` the tags + * revalidated by the request, for client-side revalidation. `cache-tag` + * and `cache-control` are their standard HTTP counterparts for response + * caches in front, such as CDNs or Cloudflare Workers Caching. + * + * @default [] + */ + headers: readonly CacheHandlerPluginHeader[] +} + +/** + * Reflects the cache activity of the `cache` and `revalidate` middlewares + * into the configured response headers. Only the first check belonging to + * the procedure the client called is reflected, so nested procedure calls + * never leak their tags into the response. Does nothing until headers are + * configured. + * + * @see {@link https://orpc.dev/docs/helpers/cache#handler-plugin | Cache Helpers - Handler Plugin} + */ +export class CacheHandlerPlugin implements StandardHandlerPlugin { + name = '~cache' + + private readonly headers: Set + + constructor(options: CacheHandlerPluginOptions) { + this.headers = new Set(options.headers) + } + + init(options: StandardHandlerOptions): StandardHandlerOptions { + if (!this.headers.size) { + return options + } + + const interceptor: StandardHandlerInterceptor = async (interceptorOptions) => { + const pluginContext: Exclude = { caches: [], revalidations: [] } + + const response = await interceptorOptions.next({ + ...interceptorOptions, + context: { + ...interceptorOptions.context, + [CACHE_HANDLER_PLUGIN_CONTEXT_SYMBOL]: pluginContext, + } satisfies CacheHandlerPluginContext, + }) + + const isRoot = (check: { procedure: AnyProcedure, path: readonly string[] }) => + check.procedure === interceptorOptions.procedure && isDeepEqual(check.path, interceptorOptions.path) + + const rootCache = pluginContext.caches.find(isRoot) + const rootRevalidation = pluginContext.revalidations.find(isRoot) + + const headers: StandardHeaders = {} + + const cacheTag = rootCache?.tags?.length ? encodeCacheTagHeader(rootCache.tags) : undefined + + if (cacheTag !== undefined && this.headers.has('orpc-cache-tag')) { + headers['orpc-cache-tag'] = cacheTag + } + + if (rootRevalidation?.tags.length && this.headers.has('orpc-cache-tag-invalidation')) { + headers['orpc-cache-tag-invalidation'] = encodeCacheTagHeader(rootRevalidation.tags) + } + + if (cacheTag !== undefined && this.headers.has('cache-tag')) { + headers['cache-tag'] = cacheTag + } + + if (rootCache !== undefined && this.headers.has('cache-control')) { + /** + * `max-age` rather than `s-maxage`, which carries `proxy-revalidate` + * semantics ([RFC 9111](https://www.rfc-editor.org/rfc/rfc9111#section-5.2.2.10)) + * and so forbids the stale reuse `stale-while-revalidate` grants. + * Entries without a ttl stay valid until revalidated, so caches hold + * them for a year and rely on tag purges. + */ + const maxAge = rootCache.ttl ?? 31536000 + const staleWhileRevalidate = rootCache.swr ? `, stale-while-revalidate=${rootCache.swr}` : '' + headers['cache-control'] = `public, max-age=${maxAge}${staleWhileRevalidate}` + } + + return { + ...response, + headers: { ...response.headers, ...headers }, + } + } + + return { + ...options, + interceptors: [ + ...toArray(options.interceptors), + interceptor, + ], + } + } +} diff --git a/packages/cache/src/index.test.ts b/packages/cache/src/index.test.ts new file mode 100644 index 000000000..f49364e31 --- /dev/null +++ b/packages/cache/src/index.test.ts @@ -0,0 +1,8 @@ +it('exports the plugin, middleware factories, and key helper', async () => { + await expect(import('./index')).resolves.toMatchObject({ + CacheHandlerPlugin: expect.any(Function), + cache: expect.any(Function), + revalidate: expect.any(Function), + encodeCacheKey: expect.any(Function), + }) +}) diff --git a/packages/cache/src/index.ts b/packages/cache/src/index.ts new file mode 100644 index 000000000..795670487 --- /dev/null +++ b/packages/cache/src/index.ts @@ -0,0 +1,4 @@ +export * from './handler-plugin' +export * from './middleware' +export * from './types' +export * from './utils' diff --git a/packages/cache/src/middleware.test-d.ts b/packages/cache/src/middleware.test-d.ts new file mode 100644 index 000000000..767799ad7 --- /dev/null +++ b/packages/cache/src/middleware.test-d.ts @@ -0,0 +1,130 @@ +import type { CacheContext, CacheStore } from './types' +import { os, type } from '@orpc/server' +import { cache, revalidate } from './middleware' + +describe('cache', () => { + it('can infer context & input types', () => { + os + .$context<{ 'userId': string, 'cache/store': CacheStore }>() + .input(type<{ id: number }>()) + .use(({ next }) => { + return next({ + context: { + db: 'postgres', + }, + }) + }) + .use( + cache({ + key: async ({ context }, input) => { + expectTypeOf(input.id).toBeNumber() + expectTypeOf(context.userId).toBeString() + expectTypeOf(context.db).toBeString() + expectTypeOf(context['cache/store']).toEqualTypeOf() + + return `planet:${input.id}` + }, + tags: ({ context }, input) => { + expectTypeOf(input.id).toBeNumber() + expectTypeOf(context.userId).toBeString() + expectTypeOf(context.db).toBeString() + + return [`planet:${input.id}`] + }, + ttl: ({ context }, input) => { + expectTypeOf(input.id).toBeNumber() + expectTypeOf(context.userId).toBeString() + + return 1000 + }, + swr: ({ context }, input) => { + expectTypeOf(input.id).toBeNumber() + expectTypeOf(context.userId).toBeString() + + return 500 + }, + enabled: ({ context }, input) => { + expectTypeOf(input.id).toBeNumber() + expectTypeOf(context.userId).toBeString() + + return true + }, + }), + ) + .handler(({ context, input }) => { + expectTypeOf(context['cache/store']).toEqualTypeOf() + expectTypeOf(context.userId).toBeString() + expectTypeOf(context.db).toBeString() + expectTypeOf(input.id).toBeNumber() + + return 'ok' + }) + }) + + it('key is optional and accepts any material', () => { + const base = os.$context().input(type<{ id: number }>()) + + void base.use(cache()) + void base.use(cache({})) + void base.use(cache({ key: 'k' })) + void base.use(cache({ key: (_, input) => ({ id: input.id }) })) + }) + + it('requires the cache store to be declared in the initial context', () => { + void os.$context().use(cache({ key: 'k' })) + + // @ts-expect-error - initial context must provide the cache store + void os.use(cache({ key: 'k' })) + }) +}) + +describe('revalidate', () => { + it('can infer context & input types', () => { + os + .$context<{ 'userId': string, 'cache/store': CacheStore }>() + .input(type<{ id: number }>()) + .use( + revalidate({ + tags: async ({ context }, input) => { + expectTypeOf(input.id).toBeNumber() + expectTypeOf(context.userId).toBeString() + expectTypeOf(context['cache/store']).toEqualTypeOf() + + return [`planet:${input.id}`] + }, + }), + ) + .handler(({ context, input }) => { + expectTypeOf(context['cache/store']).toEqualTypeOf() + expectTypeOf(context.userId).toBeString() + expectTypeOf(input.id).toBeNumber() + + return 'ok' + }) + }) + + it('requires a non-empty tag list, but a function may decline', () => { + const base = os.$context() + + void base.use(revalidate({ tags: ['planets', 'planet:1'] })) + void base.use(revalidate({ tags: () => ['planets'] })) + void base.use(revalidate({ tags: () => undefined })) + void base.use(revalidate({ tags: () => null })) + + // @ts-expect-error - tags is required + void base.use(revalidate({})) + + // @ts-expect-error - tags must not be empty + void base.use(revalidate({ tags: [] })) + + // @ts-expect-error - tags must be a list + void base.use(revalidate({ tags: 'planets' })) + }) + + it('requires the cache store to be declared in the initial context', () => { + void os.$context().use(revalidate({ tags: ['t'] })) + + // @ts-expect-error - initial context must provide the cache store + void os.use(revalidate({ tags: ['t'] })) + }) +}) diff --git a/packages/cache/src/middleware.test.ts b/packages/cache/src/middleware.test.ts new file mode 100644 index 000000000..f5e3a3637 --- /dev/null +++ b/packages/cache/src/middleware.test.ts @@ -0,0 +1,341 @@ +import type { CacheHandlerPluginContext } from './handler-plugin' +import type { CacheContext, CacheEntry, CacheStore } from './types' +import { call, os, type } from '@orpc/server' +import { nowInSeconds } from '@orpc/shared' +import { MemoryCacheStore } from './adapters/memory' +import { CACHE_HANDLER_PLUGIN_CONTEXT_SYMBOL } from './handler-plugin' +import { cache, revalidate } from './middleware' + +/** + * A store that serves `entry` when given, and otherwise fills like a miss. + */ +function createStore(entry?: CacheEntry) { + return { + fetch: vi.fn(async (_key, fill, options) => entry ?? { + output: await fill(), + tags: options?.tags, + expiresAt: options?.ttl !== undefined ? nowInSeconds() + options.ttl : undefined, + }), + revalidate: vi.fn().mockResolvedValue(undefined), + } +} + +describe('cache', () => { + it('fills through the store on miss and returns the output', async () => { + const store = createStore() + const handlerFn = vi.fn().mockReturnValue('fresh') + const procedure = os + .$context() + .use(cache({ key: 'k', tags: ['t1', 't2'], ttl: 60, swr: 30 })) + .handler(handlerFn) + + await expect( + call(procedure, undefined, { context: { 'cache/store': store } }), + ).resolves.toBe('fresh') + + expect(handlerFn).toHaveBeenCalledTimes(1) + expect(store.fetch).toHaveBeenCalledWith('k', expect.any(Function), { tags: ['t1', 't2'], ttl: 60, swr: 30, waitUntil: undefined }) + }) + + describe('key derivation', () => { + it('derives the key from the procedure path and input by default', async () => { + const store = createStore() + const procedure = os.$context().input(type()).use(cache()).handler(() => 'ok') + + await call(procedure, { id: 1 }, { context: { 'cache/store': store }, path: ['planet', 'find'] }) + await call(procedure, { id: 1 }, { context: { 'cache/store': store }, path: ['planet', 'find'] }) + await call(procedure, { id: 2 }, { context: { 'cache/store': store }, path: ['planet', 'find'] }) + await call(procedure, { id: 1 }, { context: { 'cache/store': store }, path: ['user', 'find'] }) + + const keys = store.fetch.mock.calls.map(([key]) => key) + expect(keys[0]).toEqual([['planet', 'find'], { id: 1 }]) // the procedure path and input + expect(keys[0]).toEqual(keys[1]) // same path + input + expect(keys[0]).not.toEqual(keys[2]) // different input + expect(keys[0]).not.toEqual(keys[3]) // different path + }) + + it('uses a provided key as-is, whatever its type', async () => { + const store = createStore() + const material = os + .$context() + .input(type()) + .use(cache({ key: (_, input) => ({ id: input.id }) })) + .handler(() => 'ok') + const verbatim = os.$context().use(cache({ key: 'k' })).handler(() => 'ok') + + await call(material, { id: 1, page: 1 }, { context: { 'cache/store': store }, path: ['planet', 'find'] }) + await call(material, { id: 1, page: 2 }, { context: { 'cache/store': store }, path: ['planet', 'find'] }) + await call(verbatim, undefined, { context: { 'cache/store': store } }) + + const keys = store.fetch.mock.calls.map(([key]) => key) + expect(keys[0]).toEqual({ id: 1 }) // the resolved material, not combined with the path + expect(keys[0]).toEqual(keys[1]) // same material despite different inputs + expect(keys[2]).toBe('k') + }) + + it('derives the default key from the full input when input schemas are stacked', async () => { + const store = createStore() + const procedure = os + .$context() + .input(type<{ id: number }>(raw => ({ id: (raw as any).id }))) + .use(cache()) + .input(type<{ page: number }>(raw => ({ page: (raw as any).page }))) + .handler(() => 'ok') + + await call(procedure, { id: 1, page: 1 } as any, { context: { 'cache/store': store } }) + await call(procedure, { id: 1, page: 2 } as any, { context: { 'cache/store': store } }) + + // The middleware only validated `id` at its position, but the key still + // covers the full input, so different pages never share an entry. + const keys = store.fetch.mock.calls.map(([key]) => key) + expect(keys[0]).not.toEqual(keys[1]) + }) + }) + + it.each<[string, CacheEntry, unknown]>([ + ['a fresh entry', { output: 'cached', tags: ['t'], expiresAt: nowInSeconds() + 60 }, 'cached'], + ['an entry that never expires', { output: 'cached', tags: [] }, 'cached'], + ['a cached undefined output', { output: undefined, tags: [] }, undefined], + ])('serves %s without running the handler', async (_, entry, expected) => { + const store = createStore(entry) + const handlerFn = vi.fn().mockReturnValue('fresh') + const procedure = os.$context().use(cache({ key: 'k' })).handler(handlerFn) + + await expect( + call(procedure, undefined, { context: { 'cache/store': store } }), + ).resolves.toBe(expected) + + expect(handlerFn).not.toHaveBeenCalled() + }) + + it('key, tags, ttl, swr, enabled can be async functions', async () => { + const store = createStore() + const keyFn = vi.fn().mockResolvedValueOnce('k') + const tagsFn = vi.fn().mockResolvedValueOnce(['t']) + const ttlFn = vi.fn().mockResolvedValueOnce(60) + const swrFn = vi.fn().mockResolvedValueOnce(30) + const enabledFn = vi.fn().mockResolvedValueOnce(true) + const mw = cache({ key: keyFn, tags: tagsFn, ttl: ttlFn, swr: swrFn, enabled: enabledFn }) + const procedure = os.$context().input(type<{ id: number }>()).use(mw).handler(() => 'fresh') + + await expect( + call(procedure, { id: 1 }, { context: { 'cache/store': store } }), + ).resolves.toBe('fresh') + + for (const fn of [keyFn, tagsFn, ttlFn, swrFn, enabledFn]) { + expect(fn).toHaveBeenCalledTimes(1) + expect(fn).toHaveBeenCalledWith(expect.objectContaining({ context: expect.any(Object) }), { id: 1 }) + } + expect(store.fetch).toHaveBeenCalledWith('k', expect.any(Function), { tags: ['t'], ttl: 60, swr: 30, waitUntil: undefined }) + }) + + it('skips the store when enabled resolves to false', async () => { + const store = createStore({ output: 'cached', tags: [] }) + const handlerFn = vi.fn().mockReturnValue('fresh') + const procedure = os.$context().use(cache({ key: 'k', enabled: false })).handler(handlerFn) + + await expect( + call(procedure, undefined, { context: { 'cache/store': store } }), + ).resolves.toBe('fresh') + + expect(handlerFn).toHaveBeenCalledTimes(1) + expect(store.fetch).not.toHaveBeenCalled() + }) + + it('hands cache/waitUntil to the store', async () => { + const store = createStore() + const waitUntil = vi.fn() + const procedure = os.$context().use(cache({ key: 'k' })).handler(() => 'fresh') + + await call(procedure, undefined, { context: { 'cache/store': store, 'cache/waitUntil': waitUntil } }) + + expect(store.fetch).toHaveBeenCalledWith('k', expect.any(Function), expect.objectContaining({ waitUntil })) + }) + + it('records the entry into the handler plugin context with its remaining ttl', async () => { + const stale = createStore({ output: 'stale', tags: ['stored'], expiresAt: nowInSeconds() - 1 }) + const pluginContext = { caches: [], revalidations: [] } + const procedure = os + .$context() + .use(cache({ key: 'k', tags: ['t'], swr: 30 })) + .handler(() => 'fresh') + const context = { [CACHE_HANDLER_PLUGIN_CONTEXT_SYMBOL]: pluginContext } + + await call(procedure, undefined, { context: { 'cache/store': stale, ...context }, path: ['__path__'] }) + await call(procedure, undefined, { context: { 'cache/store': createStore(), ...context }, path: ['__path__'] }) + + expect(pluginContext.caches).toEqual([ + { procedure, path: ['__path__'], tags: ['stored'], ttl: 0, swr: 30 }, // the stale entry + { procedure, path: ['__path__'], tags: ['t'], ttl: undefined, swr: 30 }, // the never expiring fill + ]) + }) + + it('propagates store failures and records no check', async () => { + const store = createStore() + store.fetch.mockRejectedValueOnce(new Error('store down')) + const pluginContext = { caches: [], revalidations: [] } + const procedure = os.$context().use(cache({ key: 'k' })).handler(() => 'fresh') + + await expect( + call(procedure, undefined, { context: { 'cache/store': store, [CACHE_HANDLER_PLUGIN_CONTEXT_SYMBOL]: pluginContext } }), + ).rejects.toThrow('store down') + + expect(pluginContext.caches).toEqual([]) + }) + + describe('with the memory store', () => { + it('runs the handler once for concurrent misses', async () => { + const store = new MemoryCacheStore() + let finish!: (output: string) => void + const handlerFn = vi.fn(() => new Promise((resolve) => { + finish = resolve + })) + const procedure = os.$context().use(cache({ key: 'k' })).handler(handlerFn) + const run = () => call(procedure, undefined, { context: { 'cache/store': store } }) + + const results = Promise.all([run(), run(), run()]) + await vi.waitFor(() => expect(handlerFn).toHaveBeenCalledTimes(1)) + finish('fresh') + + await expect(results).resolves.toEqual(['fresh', 'fresh', 'fresh']) + expect(handlerFn).toHaveBeenCalledTimes(1) + }) + + it('serves concurrent stale hits immediately and refreshes once through waitUntil', async () => { + const store = new MemoryCacheStore() + await store.fetch('k', async () => 'stale', { ttl: 0, swr: 60 }) + + let finish!: (output: string) => void + const handlerFn = vi.fn(() => new Promise((resolve) => { + finish = resolve + })) + const waitUntil = vi.fn() + const procedure = os.$context().use(cache({ key: 'k', ttl: 60 })).handler(handlerFn) + const run = () => call(procedure, undefined, { context: { 'cache/store': store, 'cache/waitUntil': waitUntil } }) + + await expect(Promise.all([run(), run()])).resolves.toEqual(['stale', 'stale']) + expect(waitUntil).toHaveBeenCalledTimes(2) + + finish('fresh') + await Promise.all(waitUntil.mock.calls.map(([refresh]) => refresh)) + expect(handlerFn).toHaveBeenCalledTimes(1) + + await expect(run()).resolves.toBe('fresh') + }) + + it('leaves refresh failures unhandled without waitUntil', async ({ onTestFinished }) => { + // Vitest reports unhandled rejections as failures, so its listeners step aside for this test. + const listeners = process.rawListeners('unhandledRejection') as NodeJS.UnhandledRejectionListener[] + process.removeAllListeners('unhandledRejection') + const unhandledRejectionHandler = vi.fn() + process.on('unhandledRejection', unhandledRejectionHandler) + + onTestFinished(() => { + process.off('unhandledRejection', unhandledRejectionHandler) + for (const listener of listeners) { + process.on('unhandledRejection', listener) + } + }) + + const store = new MemoryCacheStore() + await store.fetch('k', async () => 'stale', { ttl: 0, swr: 60 }) + const procedure = os.$context().use(cache({ key: 'k' })).handler(() => { + throw new Error('handler down') + }) + + await expect( + call(procedure, undefined, { context: { 'cache/store': store } }), + ).resolves.toBe('stale') + + await vi.waitFor(() => expect(unhandledRejectionHandler).toHaveBeenCalledWith(new Error('handler down'), expect.any(Promise))) + }) + }) +}) + +describe('revalidate', () => { + it('revalidates tags after the handler succeeds', async () => { + const store = createStore() + const pluginContext = { caches: [], revalidations: [] } + const order: string[] = [] + store.revalidate.mockImplementation(async () => { + order.push('revalidate') + }) + const procedure = os + .$context() + .use(revalidate({ tags: ['t1', 't2'] })) + .handler(() => { + order.push('handler') + return 'done' + }) + + await expect( + call(procedure, undefined, { context: { 'cache/store': store, [CACHE_HANDLER_PLUGIN_CONTEXT_SYMBOL]: pluginContext }, path: ['__path__'] }), + ).resolves.toBe('done') + + expect(order).toEqual(['handler', 'revalidate']) + expect(store.revalidate).toHaveBeenCalledWith({ tags: ['t1', 't2'] }) + expect(pluginContext.revalidations).toEqual([{ procedure, path: ['__path__'], tags: ['t1', 't2'] }]) + }) + + it('tags can be an async function', async () => { + const store = createStore() + const tagsFn = vi.fn().mockResolvedValueOnce(['t']) + const procedure = os.$context().input(type<{ id: number }>()).use(revalidate({ tags: tagsFn })).handler(() => 'done') + + await call(procedure, { id: 1 }, { context: { 'cache/store': store } }) + + expect(tagsFn).toHaveBeenCalledWith(expect.objectContaining({ context: expect.any(Object) }), { id: 1 }) + expect(store.revalidate).toHaveBeenCalledWith({ tags: ['t'] }) + }) + + it('skips the revalidation when the handler throws', async () => { + const store = createStore() + const procedure = os.$context().use(revalidate({ tags: ['t'] })).handler(() => { + throw new Error('handler down') + }) + + await expect( + call(procedure, undefined, { context: { 'cache/store': store } }), + ).rejects.toThrow('handler down') + + expect(store.revalidate).not.toHaveBeenCalled() + }) + + it.each([ + ['null', null], + ['undefined', undefined], + ])('skips the revalidation when tags resolve to %s', async (_, tags) => { + const store = createStore() + const pluginContext = { caches: [], revalidations: [] } + const procedure = os.$context().use(revalidate({ tags: () => tags })).handler(() => 'done') + + await expect( + call(procedure, undefined, { context: { 'cache/store': store, [CACHE_HANDLER_PLUGIN_CONTEXT_SYMBOL]: pluginContext } }), + ).resolves.toBe('done') + + expect(store.revalidate).not.toHaveBeenCalled() + expect(pluginContext.revalidations).toEqual([]) + }) +}) + +describe('cache + revalidate combined', () => { + it('revalidates before filling on miss, and skips the revalidation on hit', async () => { + const store = new MemoryCacheStore() + const revalidateSpy = vi.spyOn(store, 'revalidate') + const handlerFn = vi.fn(() => 'fresh') + const procedure = os + .$context() + .use(cache({ key: 'k', tags: ['t'] })) + .use(revalidate({ tags: ['t'] })) + .handler(handlerFn) + const run = () => call(procedure, undefined, { context: { 'cache/store': store } }) + + // The revalidation runs inside the fill, so the stored entry snapshots the bumped tag and survives. + await expect(run()).resolves.toBe('fresh') + expect(revalidateSpy).toHaveBeenCalledTimes(1) + + await expect(run()).resolves.toBe('fresh') + expect(handlerFn).toHaveBeenCalledTimes(1) + expect(revalidateSpy).toHaveBeenCalledTimes(1) + }) +}) diff --git a/packages/cache/src/middleware.ts b/packages/cache/src/middleware.ts new file mode 100644 index 000000000..8a595a2bf --- /dev/null +++ b/packages/cache/src/middleware.ts @@ -0,0 +1,142 @@ +import type { Middleware, MiddlewareOptions } from '@orpc/server' +import type { Promisable, Value } from '@orpc/shared' +import type { CacheHandlerPluginContext } from './handler-plugin' +import type { CacheContext } from './types' +import { nowInSeconds, value } from '@orpc/shared' +import { CACHE_HANDLER_PLUGIN_CONTEXT_SYMBOL } from './handler-plugin' + +export interface CacheMiddlewareOptions< + TInContext extends CacheContext, + TInput, +> { + /** + * The key identifying the cache entry, or any serializable value to derive + * it from. Used as given, so procedures sharing a key share an entry. + * + * @default the procedure path and input + */ + // Spelled out instead of `unknown`, which absorbs the function form and drops its contextual typing. + key?: Value, [options: MiddlewareOptions>, input: TInput]> + + /** + * Tags associated with the entry. Revalidating any of them invalidates the entry. + * + * @default [] + */ + tags?: Value, [options: MiddlewareOptions>, input: TInput]> + + /** + * Fresh lifetime in seconds. `undefined` means the entry never expires by time. + * + * @default undefined + */ + ttl?: Value, [options: MiddlewareOptions>, input: TInput]> + + /** + * Extra stale-while-revalidate window in seconds after `ttl`. + * Stale entries are served immediately while the procedure re-executes in the background. + * + * @default 0 + */ + swr?: Value, [options: MiddlewareOptions>, input: TInput]> + + /** + * When resolved to `false`, skips both the cache lookup and the store for this request. + * + * @default true + */ + enabled?: Value, [options: MiddlewareOptions>, input: TInput]> +} + +/** + * Creates a middleware that caches procedure output in the context's `cache/store`, + * with tag-based revalidation and optional stale-while-revalidate. + * By default the key is derived from the procedure path and input. + * + * @see {@link https://orpc.dev/docs/helpers/cache#cache-middleware | Cache Helpers - Cache Middleware} + */ +export function cache< + TInContext extends CacheContext, + TInput, +>( + options: CacheMiddlewareOptions = {}, +): Middleware { + return async function cache(middlewareOptions, input, done) { + const [keyMaterial, tags, ttl, swr, enabled = true] = await Promise.all([ + value(options.key, middlewareOptions, input), + value(options.tags, middlewareOptions, input), + value(options.ttl, middlewareOptions, input), + value(options.swr, middlewareOptions, input), + value(options.enabled, middlewareOptions, input), + ]) + + if (!enabled) { + return middlewareOptions.next() + } + + const key = 'key' in options ? keyMaterial : [middlewareOptions.path, input] + + const store = middlewareOptions.context['cache/store'] + const pluginContext = (middlewareOptions.context as CacheHandlerPluginContext)[CACHE_HANDLER_PLUGIN_CONTEXT_SYMBOL] + + const entry = await store.fetch(key, async () => (await middlewareOptions.next()).output, { + tags, + ttl, + swr, + waitUntil: middlewareOptions.context['cache/waitUntil'], + }) + + pluginContext?.caches.push({ + procedure: middlewareOptions.procedure, + path: middlewareOptions.path, + tags: entry.tags, + ttl: entry.expiresAt !== undefined ? Math.max(0, entry.expiresAt - nowInSeconds()) : undefined, + swr, + }) + + return done({ output: entry.output }) + } +} + +export interface RevalidateMiddlewareOptions< + TInContext extends CacheContext, + TInput, +> { + /** + * The tags to revalidate. Resolving to `null` or `undefined` skips the revalidation. + */ + tags: Value, [options: MiddlewareOptions>, input: TInput]> +} + +/** + * Creates a middleware that revalidates cache tags in the context's `cache/store` + * after the procedure succeeds, typically on mutations. Errors skip the revalidation entirely. + * + * @see {@link https://orpc.dev/docs/helpers/cache#revalidate-middleware | Cache Helpers - Revalidate Middleware} + */ +export function revalidate< + TInContext extends CacheContext, + TInput, +>( + options: RevalidateMiddlewareOptions, +): Middleware { + return async function revalidate(middlewareOptions, input) { + const result = await middlewareOptions.next() + + const tags = await value(options.tags, middlewareOptions, input) + + if (tags) { + const store = middlewareOptions.context['cache/store'] + await store.revalidate({ tags }) + + const pluginContext = (middlewareOptions.context as CacheHandlerPluginContext)[CACHE_HANDLER_PLUGIN_CONTEXT_SYMBOL] + pluginContext?.revalidations.push({ + procedure: middlewareOptions.procedure, + path: middlewareOptions.path, + tags, + }) + } + + return result + } +} diff --git a/packages/cache/src/types.ts b/packages/cache/src/types.ts new file mode 100644 index 000000000..20e4c054d --- /dev/null +++ b/packages/cache/src/types.ts @@ -0,0 +1,100 @@ +export interface CacheEntry { + /** + * The cached procedure output. + */ + output: unknown + + /** + * The tags recorded when the entry was stored, absent when it has none. + */ + tags?: readonly string[] | undefined + + /** + * The time (unix timestamp in seconds) when the entry stops being fresh. + * `undefined` means the entry never becomes stale. + */ + expiresAt?: number | undefined +} + +export interface CacheFetchOptions { + /** + * Tags associated with the entry. Revalidating any of them invalidates the entry. + * + * @default [] + */ + tags?: readonly string[] + + /** + * Fresh lifetime in seconds. `undefined` means the entry never expires by time. + * + * @default undefined + */ + ttl?: number + + /** + * Extra stale-while-revalidate window in seconds after `ttl`. + * During this window the store still returns the entry with a past `expiresAt`. + * Ignored when `ttl` is `undefined`. + * + * @default 0 + */ + swr?: number + + /** + * Takes ownership of the background refresh of a stale entry, like + * `ctx.waitUntil` on Cloudflare Workers. The promise rejects when the + * refresh fails, so this is also where such failures are handled; without + * it they surface as unhandled rejections. + */ + waitUntil?: (promise: Promise) => void +} + +export interface CacheRevalidateOptions { + /** + * The tags to revalidate. + */ + tags: readonly [string, ...string[]] +} + +/** + * Storage contract used by the cache middleware. Implementations own expiry, + * tag tracking, and how concurrent callers of one key are coalesced. + * + * @see {@link https://orpc.dev/docs/helpers/cache#basic-usage | Cache Helpers - Basic Usage} + */ +export interface CacheStore { + /** + * Resolves the entry stored under `key`, filling it through `fill` when + * there is none. Concurrent callers of one key fill once and share that + * entry. A stale entry, past `expiresAt` but within `swr`, is returned as is + * while one caller refreshes it in the background. Keys may be any + * serializable value; implementations encode them stably, so structurally + * equal keys resolve the same entry. + */ + fetch(key: unknown, fill: () => Promise, options?: CacheFetchOptions): Promise + + /** + * Invalidates every entry associated with any of the given tags. + */ + revalidate(options: CacheRevalidateOptions): Promise +} + +/** + * The context required by the cache and revalidate middlewares. + * + * @see {@link https://orpc.dev/docs/helpers/cache#basic-usage | Cache Helpers - Basic Usage} + */ +export interface CacheContext { + /** + * The cache store shared by every cached procedure behind one handler. + */ + 'cache/store': CacheStore + + /** + * Takes ownership of background work such as stale-while-revalidate + * refreshes, like `ctx.waitUntil` on Cloudflare Workers. The promise rejects + * when the refresh fails, so this is also where such failures are handled; + * without it they surface as unhandled rejections. + */ + 'cache/waitUntil'?: (promise: Promise) => void +} diff --git a/packages/cache/src/utils.test.ts b/packages/cache/src/utils.test.ts new file mode 100644 index 000000000..cb7f8d8ff --- /dev/null +++ b/packages/cache/src/utils.test.ts @@ -0,0 +1,25 @@ +import { RPCJsonSerializer } from '@orpc/client' +import { encodeCacheKey } from './utils' + +describe('encodeCacheKey', () => { + const serializer = new RPCJsonSerializer() + + it('uses string keys verbatim', () => { + expect(encodeCacheKey('planet:1', serializer)).toBe('planet:1') + }) + + it('encodes structurally equal keys identically, regardless of property order', () => { + expect(encodeCacheKey([['planet', 'find'], { b: 2, a: 1 }], serializer)) + .toBe(encodeCacheKey([['planet', 'find'], { a: 1, b: 2 }], serializer)) + + expect(encodeCacheKey({ date: new Date(1), big: 1n }, serializer)) + .toBe(encodeCacheKey({ big: 1n, date: new Date(1) }, serializer)) + + expect(encodeCacheKey({ big: 1n }, serializer)).not.toBe(encodeCacheKey({ big: 2n }, serializer)) + }) + + it('ignores unsupported values like blobs', () => { + expect(encodeCacheKey({ file: new Blob(['a']), id: 1 }, serializer)) + .toBe(encodeCacheKey({ file: new Blob(['b']), id: 1 }, serializer)) + }) +}) diff --git a/packages/cache/src/utils.ts b/packages/cache/src/utils.ts new file mode 100644 index 000000000..6340c7cc7 --- /dev/null +++ b/packages/cache/src/utils.ts @@ -0,0 +1,32 @@ +import type { RPCJsonSerializer } from '@orpc/client' +import type { Public } from '@orpc/shared' +import type { CacheEntry } from './types' +import { deepSortKeys, nowInSeconds, stringifyJSON } from '@orpc/shared' + +/** + * Encodes a cache key into a stable string: strings are used verbatim, while + * any other value is serialized with the RPC JSON serializer first, so + * complex values become plain JSON, then canonicalized by sorting object + * keys and meta entries. Structurally equal keys always encode identically, + * and unsupported values like blobs are ignored. + * + * @see {@link https://orpc.dev/docs/helpers/cache#adapters | Cache Helpers - Adapters} + */ +export function encodeCacheKey(key: unknown, serializer: Public): string { + if (typeof key === 'string') { + return key + } + + const { json, meta } = serializer.serialize(key) + + return stringifyJSON([deepSortKeys(json), meta?.map(entry => stringifyJSON(entry)).sort()]) +} + +/** + * Whether the entry is past its fresh lifetime. + * + * @see {@link https://orpc.dev/docs/helpers/cache#adapters | Cache Helpers - Adapters} + */ +export function isCacheEntryStale(entry: CacheEntry): boolean { + return entry.expiresAt !== undefined && nowInSeconds() >= entry.expiresAt +} diff --git a/packages/cache/tests/__shared__/store-contract.ts b/packages/cache/tests/__shared__/store-contract.ts new file mode 100644 index 000000000..92d6ddc81 --- /dev/null +++ b/packages/cache/tests/__shared__/store-contract.ts @@ -0,0 +1,116 @@ +import type { CacheStore } from '../../src' +import { expect, it, vi } from 'vitest' + +/** + * The behavior every {@link CacheStore} must share, run against one adapter. + * Adapter suites keep only what is specific to their backend. + */ +export function describeCacheStoreContract(createStore: () => CacheStore): void { + it('fills a miss once, then serves the entry with its tags', async () => { + const store = createStore() + const fill = vi.fn(async () => ({ nested: [1, 2] })) + + await expect(store.fetch('k', fill, { tags: ['t'] })).resolves.toEqual({ output: { nested: [1, 2] }, tags: ['t'], expiresAt: undefined }) + await expect(store.fetch('k', fill, { tags: ['t'] })).resolves.toEqual({ output: { nested: [1, 2] }, tags: ['t'], expiresAt: undefined }) + expect(fill).toHaveBeenCalledTimes(1) + + await store.fetch('u', async () => undefined) + await expect(store.fetch('u', async () => 'refilled')).resolves.toEqual({ output: undefined, tags: undefined, expiresAt: undefined }) + }) + + it('fills each key separately', async () => { + const store = createStore() + + await store.fetch('a', async () => 'a') + + await expect(store.fetch('b', async () => 'b')).resolves.toMatchObject({ output: 'b' }) + await expect(store.fetch('a', async () => 'refilled')).resolves.toMatchObject({ output: 'a' }) + }) + + it('preserves Date, Map, Set, and BigInt outputs', async () => { + const store = createStore() + const output = { + date: new Date('2026-01-02T03:04:05.678Z'), + map: new Map([['a', 1]]), + set: new Set([1, 2]), + big: 123n, + } + + await store.fetch('k', async () => output) + + await expect(store.fetch('k', async () => 'refilled')).resolves.toMatchObject({ output }) + }) + + it('invalidates entries by any of their tags, leaving others alone', async () => { + const store = createStore() + + await store.fetch('multi', async () => 'v', { tags: ['a', 'b'] }) + await store.fetch('other', async () => 'v', { tags: ['c'] }) + + await store.revalidate({ tags: ['a'] }) + + await expect(store.fetch('multi', async () => 'refilled', { tags: ['a', 'b'] })).resolves.toMatchObject({ output: 'refilled' }) + await expect(store.fetch('other', async () => 'refilled', { tags: ['c'] })).resolves.toMatchObject({ output: 'v' }) + }) + + it('revalidates many tags at once', async () => { + const store = createStore() + + await store.fetch('a', async () => 'v', { tags: ['a'] }) + await store.fetch('b', async () => 'v', { tags: ['b'] }) + + await store.revalidate({ tags: ['a', 'b'] }) + + await expect(store.fetch('a', async () => 'refilled', { tags: ['a'] })).resolves.toMatchObject({ output: 'refilled' }) + await expect(store.fetch('b', async () => 'refilled', { tags: ['b'] })).resolves.toMatchObject({ output: 'refilled' }) + }) + + it('keeps entries filled after a revalidation', async () => { + const store = createStore() + + await store.fetch('k', async () => 'old', { tags: ['t'] }) + await store.revalidate({ tags: ['t'] }) + await store.fetch('k', async () => 'new', { tags: ['t'] }) + + await expect(store.fetch('k', async () => 'newer', { tags: ['t'] })).resolves.toMatchObject({ output: 'new' }) + }) + + it('fills once for concurrent callers of one key', async () => { + const store = createStore() + let finish!: (output: string) => void + const fill = vi.fn(() => new Promise((resolve) => { + finish = resolve + })) + + const fetches = Promise.all([store.fetch('k', fill), store.fetch('k', fill), store.fetch('k', fill)]) + await vi.waitFor(() => expect(fill).toHaveBeenCalledTimes(1), { timeout: 5000 }) + finish('v') + + const entries = await fetches + expect(entries.map(entry => entry.output)).toEqual(['v', 'v', 'v']) + expect(fill).toHaveBeenCalledTimes(1) + }) + + it('lets a waiter fill when the holder failed to', async () => { + const store = createStore() + let fail!: (error: Error) => void + let started!: () => void + const holding = new Promise((resolve) => { + started = resolve + }) + + const first = store.fetch('k', () => { + started() + return new Promise((_, reject) => { + fail = reject + }) + }) + await holding + + const second = store.fetch('k', async () => 'fresh') + fail(new Error('handler down')) + + await expect(first).rejects.toThrow('handler down') + await expect(second).resolves.toMatchObject({ output: 'fresh' }) + }) +} diff --git a/packages/cache/tests/e2e.test.ts b/packages/cache/tests/e2e.test.ts new file mode 100644 index 000000000..1fe72f6bb --- /dev/null +++ b/packages/cache/tests/e2e.test.ts @@ -0,0 +1,77 @@ +import type { CacheContext } from '../src' +import { os } from '@orpc/server' +import { RPCHandler } from '@orpc/server/fetch' +import { z } from 'zod' +import { cache, CacheHandlerPlugin, revalidate } from '../src' +import { MemoryCacheStore } from '../src/adapters/memory' + +it('works', async () => { + const findHandlerFn = vi.fn(({ input }) => ({ id: input.id, name: `Planet ${input.id}` })) + + const router = { + planet: { + find: os + .$context() + .input(z.object({ id: z.number() })) + .use( + cache({ + key: (_, input) => `planet:${input.id}`, + tags: (_, input) => ['planets', `planet:${input.id}`], + }), + ) + .handler(findHandlerFn), + update: os + .$context() + .input(z.object({ id: z.number(), name: z.string() })) + .use( + revalidate({ tags: (_, input) => ['planets', `planet:${input.id}`] }), + ) + .handler(({ input }) => input), + }, + } + + const handler = new RPCHandler(router, { + plugins: [ + new CacheHandlerPlugin({ headers: ['orpc-cache-tag', 'orpc-cache-tag-invalidation'] }), + ], + }) + + const store = new MemoryCacheStore() + + const request = (path: string, body: unknown) => new Request(`https://example.com/${path}`, { + method: 'POST', + body: JSON.stringify({ json: body }), + headers: { + 'Content-Type': 'application/json', + }, + }) + + const find = () => handler.handle(request('planet/find', { id: 1 }), { + context: { 'cache/store': store }, + }) + + // miss: the handler runs and the response carries the cache tags + const first = await find() + expect(first.response?.status).toBe(200) + expect(first.response?.headers.get('orpc-cache-tag')).toBe('planets,planet:1') + expect(findHandlerFn).toHaveBeenCalledTimes(1) + + // hit: the handler does not re-run and the response body is identical + const second = await find() + expect(second.response?.status).toBe(200) + expect(second.response?.headers.get('orpc-cache-tag')).toBe('planets,planet:1') + expect(findHandlerFn).toHaveBeenCalledTimes(1) + await expect(second.response?.json()).resolves.toEqual(await first.response?.clone().json()) + + // update: revalidates the tags and reflects them in the invalidation header + const update = await handler.handle(request('planet/update', { id: 1, name: 'Mars' }), { + context: { 'cache/store': store }, + }) + expect(update.response?.status).toBe(200) + expect(update.response?.headers.get('orpc-cache-tag-invalidation')).toBe('planets,planet:1') + + // miss again: the revalidation evicted the entry + const third = await find() + expect(third.response?.status).toBe(200) + expect(findHandlerFn).toHaveBeenCalledTimes(2) +}) diff --git a/packages/cache/tests/redis-adapters-compatibility.test.ts b/packages/cache/tests/redis-adapters-compatibility.test.ts new file mode 100644 index 000000000..b13ef4c1e --- /dev/null +++ b/packages/cache/tests/redis-adapters-compatibility.test.ts @@ -0,0 +1,129 @@ +import type { CacheStore } from '../src' +import { nowInSeconds, sleep } from '@orpc/shared' +import { Redis } from '@upstash/redis' +import { createClient } from 'redis' +import { RedisCacheStore } from '../src/adapters/redis' +import { UpstashCacheStore } from '../src/adapters/upstash' + +const UPSTASH_REDIS_REST_URL = process.env.UPSTASH_REDIS_REST_URL +const UPSTASH_REDIS_REST_TOKEN = process.env.UPSTASH_REDIS_REST_TOKEN + +/** + * These tests require a real Upstash Redis server. + * Set `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` before running them. + * + * When adding new tests, always use unique keys to avoid conflicts with other cases. + * + * All adapters must connect to the same server. + */ +describe.concurrent('cache redis adapters compatibility', { timeout: 20_000 }, () => { + const stores: Array<{ name: string, store: CacheStore }> = [] + const prefix = `redis-adapters:${crypto.randomUUID()}:` + + if (UPSTASH_REDIS_REST_URL && UPSTASH_REDIS_REST_TOKEN) { + const redis = createClient({ url: `rediss://default:${UPSTASH_REDIS_REST_TOKEN}@${new URL(UPSTASH_REDIS_REST_URL).host}:6379` }) + + afterAll(() => { + redis.close() + }) + + stores.push({ name: 'redis', store: new RedisCacheStore(redis, { prefix }) }) + } + + // TODO: Upstash is not compatible with Node 26 yet — temporarily disable these tests and revisit in the future. + if (UPSTASH_REDIS_REST_URL && UPSTASH_REDIS_REST_TOKEN && !process.versions.node.startsWith('26.')) { + const upstashRedis = new Redis({ url: UPSTASH_REDIS_REST_URL, token: UPSTASH_REDIS_REST_TOKEN }) + + stores.push({ name: 'upstash', store: new UpstashCacheStore(upstashRedis, { prefix }) }) + } + + describe.skipIf(stores.length < 2)('cross-adapter compatibility', () => { + for (const source of stores) { + for (const target of stores) { + if (source === target) { + continue + } + + it(`shares entries and revalidations: ${source.name} → ${target.name}`, async () => { + const id = crypto.randomUUID() + const tag = `tag:${crypto.randomUUID()}` + const output = { date: new Date('2026-01-02T03:04:05.678Z'), big: 123n } + + await source.store.fetch([['planet', 'find'], { b: 2, id }], async () => output, { tags: [tag], ttl: 60 }) + + const entry = await target.store.fetch([['planet', 'find'], { id, b: 2 }], async () => 'refilled', { tags: [tag], ttl: 60 }) + expect(entry.output).toEqual(output) + expect(entry.tags).toEqual([tag]) + expect(entry.expiresAt).toBeGreaterThan(nowInSeconds()) + + await target.store.revalidate({ tags: [tag] }) + + await expect(source.store.fetch([['planet', 'find'], { b: 2, id }], async () => 'refilled', { tags: [tag] })).resolves.toMatchObject({ output: 'refilled' }) + }) + + it(`shares tag counters: ${source.name} → ${target.name}`, async () => { + const key = `counter:${crypto.randomUUID()}` + const tag = `tag:${crypto.randomUUID()}` + + await source.store.fetch(key, async () => 'v1', { tags: [tag] }) + await target.store.revalidate({ tags: [tag] }) + + await expect(target.store.fetch(key, async () => 'v2', { tags: [tag] })).resolves.toMatchObject({ output: 'v2' }) + await expect(source.store.fetch(key, async () => 'v3', { tags: [tag] })).resolves.toMatchObject({ output: 'v2' }) + + await source.store.revalidate({ tags: [tag] }) + await expect(target.store.fetch(key, async () => 'v4', { tags: [tag] })).resolves.toMatchObject({ output: 'v4' }) + }) + + it(`shares retention: ${source.name} → ${target.name}`, async () => { + const noSwr = `no-swr:${crypto.randomUUID()}` + const swr = `swr:${crypto.randomUUID()}` + + await source.store.fetch(noSwr, async () => 'v', { ttl: 1 }) + await source.store.fetch(swr, async () => 'v', { ttl: 1, swr: 10 }) + + await sleep(1500) + + await expect(target.store.fetch(noSwr, async () => 'refilled', { ttl: 1 })).resolves.toMatchObject({ output: 'refilled' }) + + const waitUntil = (_promise: Promise) => {} + const stale = await target.store.fetch(swr, async () => 'refilled', { ttl: 1, swr: 10, waitUntil }) + expect(stale.output).toBe('v') + expect(stale.expiresAt).toBeLessThanOrEqual(nowInSeconds()) + }) + + it(`shares locks: ${source.name} → ${target.name}`, async () => { + const key = `lock:${crypto.randomUUID()}` + let release!: () => void + const held = new Promise((resolve) => { + release = resolve + }) + let acquired!: () => void + const holding = new Promise((resolve) => { + acquired = resolve + }) + + const holder = source.store.fetch(key, async () => { + acquired() + await held + return 'held' + }) + await holding + + let settled = false + const waiter = target.store.fetch(key, async () => 'refilled').then((entry) => { + settled = true + return entry + }) + + await sleep(300) + expect(settled).toBe(false) + + release() + await holder + await expect(waiter).resolves.toMatchObject({ output: 'held' }) + }) + } + } + }) +}) diff --git a/packages/cache/tsconfig.json b/packages/cache/tsconfig.json new file mode 100644 index 000000000..211b35f0c --- /dev/null +++ b/packages/cache/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.lib.json", + "references": [ + { "path": "../client" }, + { "path": "../server" }, + { "path": "../shared" } + ], + "include": ["package.json", "src"], + "exclude": [ + "**/*.bench.*", + "**/*.test.*", + "**/*.test-d.ts", + "**/__tests__/**", + "**/__mocks__/**", + "**/__snapshots__/**" + ] +} diff --git a/packages/client/README.md b/packages/client/README.md index 86cb6d543..9897311b4 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -46,29 +46,30 @@ You can read the documentation [here](https://orpc.dev). **Built-in features** -| Package | Purpose | Docs | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | -| [@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) | -| [@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) | -| [@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) | +| Package | Purpose | Docs | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | +| [@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) | +| [@orpc/experimental-cache](https://www.npmjs.com/package/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](https://orpc.dev/docs/helpers/cache) | +| [@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) | +| [@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) | **Framework & ecosystem integrations** -| Package | Purpose | Docs | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [@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) | -| [@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) | -| [@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) | -| [@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) | -| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | -| [@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) | -| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | -| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | -| [@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) | -| [@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) | -| [@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), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | -| [@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) | +| Package | Purpose | Docs | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [@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) | +| [@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) | +| [@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) | +| [@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) | +| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | +| [@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) | +| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | +| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | +| [@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) | +| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | +| [@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) | **Observability** diff --git a/packages/cloudflare/README.md b/packages/cloudflare/README.md index a86549e00..35ed7f1ad 100644 --- a/packages/cloudflare/README.md +++ b/packages/cloudflare/README.md @@ -46,29 +46,30 @@ You can read the documentation [here](https://orpc.dev). **Built-in features** -| Package | Purpose | Docs | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | -| [@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) | -| [@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) | -| [@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) | +| Package | Purpose | Docs | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | +| [@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) | +| [@orpc/experimental-cache](https://www.npmjs.com/package/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](https://orpc.dev/docs/helpers/cache) | +| [@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) | +| [@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) | **Framework & ecosystem integrations** -| Package | Purpose | Docs | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [@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) | -| [@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) | -| [@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) | -| [@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) | -| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | -| [@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) | -| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | -| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | -| [@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) | -| [@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) | -| [@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), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | -| [@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) | +| Package | Purpose | Docs | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [@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) | +| [@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) | +| [@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) | +| [@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) | +| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | +| [@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) | +| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | +| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | +| [@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) | +| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | +| [@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) | **Observability** diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index eaf47cc93..ce40be020 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -2,7 +2,7 @@ "name": "@orpc/cloudflare", "type": "module", "version": "2.0.0-beta.34", - "description": "Cloudflare integration for oRPC: Durable Object pub/sub, Workers rate limiting, and Workers Traces adapters", + "description": "Cloudflare integration for oRPC: Durable Object pub/sub, Workers rate limiting, Workers Caching, and Workers Traces adapters", "license": "MIT", "funding": [ "https://github.com/sponsors/dinwwwh", @@ -21,6 +21,7 @@ "durable-objects", "ratelimit", "pubsub", + "cache", "tracing", "observability", "typescript" @@ -52,6 +53,7 @@ }, "dependencies": { "@orpc/client": "workspace:*", + "@orpc/experimental-cache": "workspace:*", "@orpc/publisher": "workspace:*", "@orpc/ratelimit": "workspace:*", "@orpc/shared": "workspace:*", diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index 4661b1d1d..fd63927ff 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -3,3 +3,4 @@ export * from './publisher-object' export * from './ratelimit' export type { experimental_CloudflareTracerOptions } from './tracer' export { experimental_CloudflareTracer } from './tracer' +export * from './workers-cache' diff --git a/packages/cloudflare/src/workers-cache.test.ts b/packages/cloudflare/src/workers-cache.test.ts new file mode 100644 index 000000000..6d602a792 --- /dev/null +++ b/packages/cloudflare/src/workers-cache.test.ts @@ -0,0 +1,65 @@ +import { nowInSeconds } from '@orpc/shared' +import { cache } from 'cloudflare:workers' +import { describe, expect, it, vi } from 'vitest' +import { experimental_WorkersCacheStore } from './workers-cache' + +describe('experimental_WorkersCacheStore', () => { + const createPurger = () => ({ + purge: vi.fn(async () => ({ success: true })), + }) as any + + it('fills every time and stores nothing', async () => { + const purger = createPurger() + const store = new experimental_WorkersCacheStore({ cache: purger }) + const fill = vi.fn(async () => 'v') + + const entry = await store.fetch('k', fill, { tags: ['t'], ttl: 1000 }) + expect(entry.output).toBe('v') + expect(entry.tags).toEqual(['t']) + expect(entry.expiresAt).toBeGreaterThan(nowInSeconds()) + + await expect(store.fetch('k', fill)).resolves.toEqual({ output: 'v', tags: undefined, expiresAt: undefined }) + expect(fill).toHaveBeenCalledTimes(2) + expect(purger.purge).not.toHaveBeenCalled() + }) + + it('purges encoded tags through workers caching', async () => { + const purger = createPurger() + const store = new experimental_WorkersCacheStore({ cache: purger }) + + await store.revalidate({ tags: ['planets', 'a,b'] }) + + expect(purger.purge).toHaveBeenCalledTimes(1) + expect(purger.purge).toHaveBeenCalledWith({ tags: ['planets', 'a%2Cb'] }) + }) + + it('defaults to the cache exported by cloudflare:workers', () => { + const store = new experimental_WorkersCacheStore() + + expect((store as any).cache).toBe(cache) + }) + + it('throws a bare error when the purge fails without messages', async () => { + const store = new experimental_WorkersCacheStore({ + cache: { + purge: vi.fn(async () => ({ success: false })) as any, + }, + }) + + await expect(store.revalidate({ tags: ['planets'] })).rejects.toThrow( + 'experimental_WorkersCacheStore failed to purge tags', + ) + }) + + it('throws when the purge fails, including error messages', async () => { + const store = new experimental_WorkersCacheStore({ + cache: { + purge: vi.fn(async () => ({ success: false, errors: [{ code: 429, message: 'Rate limited' }] })), + }, + }) + + await expect(store.revalidate({ tags: ['planets'] })).rejects.toThrow( + 'experimental_WorkersCacheStore failed to purge tags: Rate limited', + ) + }) +}) diff --git a/packages/cloudflare/src/workers-cache.ts b/packages/cloudflare/src/workers-cache.ts new file mode 100644 index 000000000..b264fd902 --- /dev/null +++ b/packages/cloudflare/src/workers-cache.ts @@ -0,0 +1,47 @@ +import type { CacheEntry, CacheFetchOptions, CacheRevalidateOptions, CacheStore } from '@orpc/experimental-cache' +import { encodeCacheTag, nowInSeconds, toArray } from '@orpc/shared' +import * as workers from 'cloudflare:workers' + +export interface experimental_WorkersCacheStoreOptions { + /** + * The Workers Caching purge surface, such as `ctx.cache`. + * + * @default cache from `cloudflare:workers` + */ + cache?: CacheContext +} + +/** + * Purge-only cache store for Cloudflare Workers Caching. Responses are cached + * in front of the Worker through `Cache-Control` and `Cache-Tag` headers (see + * the `CacheHandlerPlugin` `headers` option), so `fetch` always fills and + * stores nothing; `revalidate` purges the tags through Workers Caching. + * + * @see {@link https://orpc.dev/docs/helpers/cache#adapters | Cache Helpers - Adapters} + */ +export class experimental_WorkersCacheStore implements CacheStore { + private readonly cache: CacheContext + + constructor(options: experimental_WorkersCacheStoreOptions = {}) { + this.cache = options.cache ?? workers.cache + } + + async fetch(_key: unknown, fill: () => Promise, options: CacheFetchOptions = {}): Promise { + return { + output: await fill(), + tags: options.tags, + expiresAt: options.ttl !== undefined ? nowInSeconds() + options.ttl : undefined, + } + } + + async revalidate({ tags }: CacheRevalidateOptions): Promise { + const result = await this.cache.purge({ + tags: tags.map(tag => encodeCacheTag(tag)), + }) + + if (!result.success) { + const messages = toArray(result.errors).map(error => error.message).filter(Boolean).join('; ') + throw new Error(`experimental_WorkersCacheStore failed to purge tags${messages ? `: ${messages}` : ''}`) + } + } +} diff --git a/packages/contract/README.md b/packages/contract/README.md index 3a15ee4f5..eeaf4a3a0 100644 --- a/packages/contract/README.md +++ b/packages/contract/README.md @@ -46,29 +46,30 @@ You can read the documentation [here](https://orpc.dev). **Built-in features** -| Package | Purpose | Docs | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | -| [@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) | -| [@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) | -| [@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) | +| Package | Purpose | Docs | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | +| [@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) | +| [@orpc/experimental-cache](https://www.npmjs.com/package/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](https://orpc.dev/docs/helpers/cache) | +| [@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) | +| [@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) | **Framework & ecosystem integrations** -| Package | Purpose | Docs | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [@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) | -| [@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) | -| [@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) | -| [@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) | -| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | -| [@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) | -| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | -| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | -| [@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) | -| [@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) | -| [@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), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | -| [@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) | +| Package | Purpose | Docs | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [@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) | +| [@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) | +| [@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) | +| [@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) | +| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | +| [@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) | +| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | +| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | +| [@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) | +| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | +| [@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) | **Observability** diff --git a/packages/effect/README.md b/packages/effect/README.md index 7f75d75d7..1f0cf579d 100644 --- a/packages/effect/README.md +++ b/packages/effect/README.md @@ -46,29 +46,30 @@ You can read the documentation [here](https://orpc.dev). **Built-in features** -| Package | Purpose | Docs | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | -| [@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) | -| [@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) | -| [@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) | +| Package | Purpose | Docs | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | +| [@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) | +| [@orpc/experimental-cache](https://www.npmjs.com/package/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](https://orpc.dev/docs/helpers/cache) | +| [@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) | +| [@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) | **Framework & ecosystem integrations** -| Package | Purpose | Docs | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [@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) | -| [@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) | -| [@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) | -| [@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) | -| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | -| [@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) | -| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | -| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | -| [@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) | -| [@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) | -| [@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), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | -| [@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) | +| Package | Purpose | Docs | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [@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) | +| [@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) | +| [@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) | +| [@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) | +| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | +| [@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) | +| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | +| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | +| [@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) | +| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | +| [@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) | **Observability** diff --git a/packages/evlog/README.md b/packages/evlog/README.md index 7b7605575..82a02f248 100644 --- a/packages/evlog/README.md +++ b/packages/evlog/README.md @@ -46,29 +46,30 @@ You can read the documentation [here](https://orpc.dev). **Built-in features** -| Package | Purpose | Docs | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | -| [@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) | -| [@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) | -| [@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) | +| Package | Purpose | Docs | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | +| [@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) | +| [@orpc/experimental-cache](https://www.npmjs.com/package/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](https://orpc.dev/docs/helpers/cache) | +| [@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) | +| [@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) | **Framework & ecosystem integrations** -| Package | Purpose | Docs | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [@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) | -| [@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) | -| [@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) | -| [@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) | -| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | -| [@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) | -| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | -| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | -| [@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) | -| [@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) | -| [@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), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | -| [@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) | +| Package | Purpose | Docs | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [@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) | +| [@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) | +| [@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) | +| [@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) | +| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | +| [@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) | +| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | +| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | +| [@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) | +| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | +| [@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) | **Observability** diff --git a/packages/hibernation/README.md b/packages/hibernation/README.md index f43a9cfad..78acf1c1c 100644 --- a/packages/hibernation/README.md +++ b/packages/hibernation/README.md @@ -46,29 +46,30 @@ You can read the documentation [here](https://orpc.dev). **Built-in features** -| Package | Purpose | Docs | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | -| [@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) | -| [@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) | -| [@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) | +| Package | Purpose | Docs | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | +| [@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) | +| [@orpc/experimental-cache](https://www.npmjs.com/package/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](https://orpc.dev/docs/helpers/cache) | +| [@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) | +| [@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) | **Framework & ecosystem integrations** -| Package | Purpose | Docs | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [@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) | -| [@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) | -| [@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) | -| [@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) | -| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | -| [@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) | -| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | -| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | -| [@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) | -| [@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) | -| [@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), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | -| [@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) | +| Package | Purpose | Docs | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [@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) | +| [@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) | +| [@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) | +| [@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) | +| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | +| [@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) | +| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | +| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | +| [@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) | +| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | +| [@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) | **Observability** diff --git a/packages/json-schema/README.md b/packages/json-schema/README.md index 9cbda9abd..c50ad99f3 100644 --- a/packages/json-schema/README.md +++ b/packages/json-schema/README.md @@ -46,29 +46,30 @@ You can read the documentation [here](https://orpc.dev). **Built-in features** -| Package | Purpose | Docs | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | -| [@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) | -| [@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) | -| [@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) | +| Package | Purpose | Docs | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | +| [@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) | +| [@orpc/experimental-cache](https://www.npmjs.com/package/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](https://orpc.dev/docs/helpers/cache) | +| [@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) | +| [@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) | **Framework & ecosystem integrations** -| Package | Purpose | Docs | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [@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) | -| [@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) | -| [@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) | -| [@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) | -| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | -| [@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) | -| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | -| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | -| [@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) | -| [@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) | -| [@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), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | -| [@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) | +| Package | Purpose | Docs | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [@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) | +| [@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) | +| [@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) | +| [@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) | +| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | +| [@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) | +| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | +| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | +| [@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) | +| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | +| [@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) | **Observability** diff --git a/packages/msw/README.md b/packages/msw/README.md index 67cfd6dbb..c367883b3 100644 --- a/packages/msw/README.md +++ b/packages/msw/README.md @@ -46,29 +46,30 @@ You can read the documentation [here](https://orpc.dev). **Built-in features** -| Package | Purpose | Docs | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | -| [@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) | -| [@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) | -| [@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) | +| Package | Purpose | Docs | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | +| [@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) | +| [@orpc/experimental-cache](https://www.npmjs.com/package/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](https://orpc.dev/docs/helpers/cache) | +| [@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) | +| [@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) | **Framework & ecosystem integrations** -| Package | Purpose | Docs | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [@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) | -| [@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) | -| [@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) | -| [@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) | -| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | -| [@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) | -| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | -| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | -| [@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) | -| [@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) | -| [@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), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | -| [@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) | +| Package | Purpose | Docs | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [@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) | +| [@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) | +| [@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) | +| [@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) | +| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | +| [@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) | +| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | +| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | +| [@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) | +| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | +| [@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) | **Observability** diff --git a/packages/nest/README.md b/packages/nest/README.md index 36e3abef8..48543cad5 100644 --- a/packages/nest/README.md +++ b/packages/nest/README.md @@ -46,29 +46,30 @@ You can read the documentation [here](https://orpc.dev). **Built-in features** -| Package | Purpose | Docs | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | -| [@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) | -| [@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) | -| [@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) | +| Package | Purpose | Docs | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | +| [@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) | +| [@orpc/experimental-cache](https://www.npmjs.com/package/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](https://orpc.dev/docs/helpers/cache) | +| [@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) | +| [@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) | **Framework & ecosystem integrations** -| Package | Purpose | Docs | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [@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) | -| [@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) | -| [@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) | -| [@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) | -| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | -| [@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) | -| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | -| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | -| [@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) | -| [@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) | -| [@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), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | -| [@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) | +| Package | Purpose | Docs | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [@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) | +| [@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) | +| [@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) | +| [@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) | +| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | +| [@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) | +| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | +| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | +| [@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) | +| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | +| [@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) | **Observability** diff --git a/packages/next/README.md b/packages/next/README.md index 0f7b97c71..b19cbe32f 100644 --- a/packages/next/README.md +++ b/packages/next/README.md @@ -46,29 +46,30 @@ You can read the documentation [here](https://orpc.dev). **Built-in features** -| Package | Purpose | Docs | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | -| [@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) | -| [@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) | -| [@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) | +| Package | Purpose | Docs | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | +| [@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) | +| [@orpc/experimental-cache](https://www.npmjs.com/package/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](https://orpc.dev/docs/helpers/cache) | +| [@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) | +| [@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) | **Framework & ecosystem integrations** -| Package | Purpose | Docs | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [@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) | -| [@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) | -| [@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) | -| [@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) | -| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | -| [@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) | -| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | -| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | -| [@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) | -| [@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) | -| [@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), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | -| [@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) | +| Package | Purpose | Docs | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [@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) | +| [@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) | +| [@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) | +| [@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) | +| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | +| [@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) | +| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | +| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | +| [@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) | +| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | +| [@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) | **Observability** diff --git a/packages/node/README.md b/packages/node/README.md index 1763f2e20..64c348bb1 100644 --- a/packages/node/README.md +++ b/packages/node/README.md @@ -46,29 +46,30 @@ You can read the documentation [here](https://orpc.dev). **Built-in features** -| Package | Purpose | Docs | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | -| [@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) | -| [@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) | -| [@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) | +| Package | Purpose | Docs | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | +| [@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) | +| [@orpc/experimental-cache](https://www.npmjs.com/package/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](https://orpc.dev/docs/helpers/cache) | +| [@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) | +| [@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) | **Framework & ecosystem integrations** -| Package | Purpose | Docs | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [@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) | -| [@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) | -| [@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) | -| [@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) | -| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | -| [@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) | -| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | -| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | -| [@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) | -| [@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) | -| [@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), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | -| [@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) | +| Package | Purpose | Docs | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [@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) | +| [@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) | +| [@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) | +| [@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) | +| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | +| [@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) | +| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | +| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | +| [@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) | +| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | +| [@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) | **Observability** diff --git a/packages/openapi/README.md b/packages/openapi/README.md index fa0af325f..a512f6795 100644 --- a/packages/openapi/README.md +++ b/packages/openapi/README.md @@ -46,29 +46,30 @@ You can read the documentation [here](https://orpc.dev). **Built-in features** -| Package | Purpose | Docs | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | -| [@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) | -| [@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) | -| [@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) | +| Package | Purpose | Docs | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | +| [@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) | +| [@orpc/experimental-cache](https://www.npmjs.com/package/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](https://orpc.dev/docs/helpers/cache) | +| [@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) | +| [@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) | **Framework & ecosystem integrations** -| Package | Purpose | Docs | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [@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) | -| [@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) | -| [@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) | -| [@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) | -| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | -| [@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) | -| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | -| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | -| [@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) | -| [@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) | -| [@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), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | -| [@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) | +| Package | Purpose | Docs | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [@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) | +| [@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) | +| [@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) | +| [@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) | +| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | +| [@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) | +| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | +| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | +| [@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) | +| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | +| [@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) | **Observability** diff --git a/packages/opentelemetry/README.md b/packages/opentelemetry/README.md index 4543f5cf5..383567647 100644 --- a/packages/opentelemetry/README.md +++ b/packages/opentelemetry/README.md @@ -46,29 +46,30 @@ You can read the documentation [here](https://orpc.dev). **Built-in features** -| Package | Purpose | Docs | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | -| [@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) | -| [@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) | -| [@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) | +| Package | Purpose | Docs | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | +| [@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) | +| [@orpc/experimental-cache](https://www.npmjs.com/package/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](https://orpc.dev/docs/helpers/cache) | +| [@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) | +| [@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) | **Framework & ecosystem integrations** -| Package | Purpose | Docs | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [@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) | -| [@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) | -| [@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) | -| [@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) | -| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | -| [@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) | -| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | -| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | -| [@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) | -| [@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) | -| [@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), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | -| [@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) | +| Package | Purpose | Docs | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [@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) | +| [@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) | +| [@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) | +| [@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) | +| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | +| [@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) | +| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | +| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | +| [@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) | +| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | +| [@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) | **Observability** diff --git a/packages/pinia-colada/README.md b/packages/pinia-colada/README.md index ed950516c..7b3226b87 100644 --- a/packages/pinia-colada/README.md +++ b/packages/pinia-colada/README.md @@ -46,29 +46,30 @@ You can read the documentation [here](https://orpc.dev). **Built-in features** -| Package | Purpose | Docs | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | -| [@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) | -| [@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) | -| [@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) | +| Package | Purpose | Docs | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | +| [@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) | +| [@orpc/experimental-cache](https://www.npmjs.com/package/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](https://orpc.dev/docs/helpers/cache) | +| [@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) | +| [@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) | **Framework & ecosystem integrations** -| Package | Purpose | Docs | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [@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) | -| [@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) | -| [@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) | -| [@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) | -| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | -| [@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) | -| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | -| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | -| [@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) | -| [@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) | -| [@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), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | -| [@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) | +| Package | Purpose | Docs | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [@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) | +| [@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) | +| [@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) | +| [@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) | +| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | +| [@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) | +| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | +| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | +| [@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) | +| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | +| [@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) | **Observability** diff --git a/packages/pino/README.md b/packages/pino/README.md index a832ed718..3350ad841 100644 --- a/packages/pino/README.md +++ b/packages/pino/README.md @@ -46,29 +46,30 @@ You can read the documentation [here](https://orpc.dev). **Built-in features** -| Package | Purpose | Docs | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | -| [@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) | -| [@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) | -| [@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) | +| Package | Purpose | Docs | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | +| [@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) | +| [@orpc/experimental-cache](https://www.npmjs.com/package/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](https://orpc.dev/docs/helpers/cache) | +| [@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) | +| [@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) | **Framework & ecosystem integrations** -| Package | Purpose | Docs | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [@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) | -| [@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) | -| [@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) | -| [@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) | -| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | -| [@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) | -| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | -| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | -| [@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) | -| [@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) | -| [@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), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | -| [@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) | +| Package | Purpose | Docs | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [@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) | +| [@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) | +| [@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) | +| [@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) | +| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | +| [@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) | +| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | +| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | +| [@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) | +| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | +| [@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) | **Observability** diff --git a/packages/publisher/README.md b/packages/publisher/README.md index 14cd8db10..3c3123376 100644 --- a/packages/publisher/README.md +++ b/packages/publisher/README.md @@ -46,29 +46,30 @@ You can read the documentation [here](https://orpc.dev). **Built-in features** -| Package | Purpose | Docs | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | -| [@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) | -| [@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) | -| [@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) | +| Package | Purpose | Docs | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | +| [@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) | +| [@orpc/experimental-cache](https://www.npmjs.com/package/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](https://orpc.dev/docs/helpers/cache) | +| [@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) | +| [@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) | **Framework & ecosystem integrations** -| Package | Purpose | Docs | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [@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) | -| [@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) | -| [@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) | -| [@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) | -| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | -| [@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) | -| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | -| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | -| [@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) | -| [@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) | -| [@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), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | -| [@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) | +| Package | Purpose | Docs | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [@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) | +| [@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) | +| [@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) | +| [@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) | +| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | +| [@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) | +| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | +| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | +| [@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) | +| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | +| [@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) | **Observability** diff --git a/packages/ratelimit/README.md b/packages/ratelimit/README.md index 6057a49e5..b67bd3392 100644 --- a/packages/ratelimit/README.md +++ b/packages/ratelimit/README.md @@ -46,29 +46,30 @@ You can read the documentation [here](https://orpc.dev). **Built-in features** -| Package | Purpose | Docs | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | -| [@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) | -| [@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) | -| [@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) | +| Package | Purpose | Docs | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | +| [@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) | +| [@orpc/experimental-cache](https://www.npmjs.com/package/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](https://orpc.dev/docs/helpers/cache) | +| [@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) | +| [@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) | **Framework & ecosystem integrations** -| Package | Purpose | Docs | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [@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) | -| [@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) | -| [@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) | -| [@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) | -| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | -| [@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) | -| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | -| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | -| [@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) | -| [@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) | -| [@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), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | -| [@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) | +| Package | Purpose | Docs | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [@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) | +| [@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) | +| [@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) | +| [@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) | +| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | +| [@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) | +| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | +| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | +| [@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) | +| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | +| [@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) | **Observability** diff --git a/packages/server/README.md b/packages/server/README.md index 79ddd059d..f83662c0b 100644 --- a/packages/server/README.md +++ b/packages/server/README.md @@ -46,29 +46,30 @@ You can read the documentation [here](https://orpc.dev). **Built-in features** -| Package | Purpose | Docs | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | -| [@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) | -| [@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) | -| [@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) | +| Package | Purpose | Docs | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | +| [@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) | +| [@orpc/experimental-cache](https://www.npmjs.com/package/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](https://orpc.dev/docs/helpers/cache) | +| [@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) | +| [@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) | **Framework & ecosystem integrations** -| Package | Purpose | Docs | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [@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) | -| [@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) | -| [@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) | -| [@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) | -| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | -| [@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) | -| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | -| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | -| [@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) | -| [@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) | -| [@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), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | -| [@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) | +| Package | Purpose | Docs | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [@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) | +| [@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) | +| [@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) | +| [@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) | +| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | +| [@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) | +| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | +| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | +| [@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) | +| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | +| [@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) | **Observability** diff --git a/packages/shared/README.md b/packages/shared/README.md index ec4a4d292..cdc3f3f3f 100644 --- a/packages/shared/README.md +++ b/packages/shared/README.md @@ -46,29 +46,30 @@ You can read the documentation [here](https://orpc.dev). **Built-in features** -| Package | Purpose | Docs | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | -| [@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) | -| [@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) | -| [@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) | +| Package | Purpose | Docs | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | +| [@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) | +| [@orpc/experimental-cache](https://www.npmjs.com/package/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](https://orpc.dev/docs/helpers/cache) | +| [@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) | +| [@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) | **Framework & ecosystem integrations** -| Package | Purpose | Docs | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [@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) | -| [@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) | -| [@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) | -| [@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) | -| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | -| [@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) | -| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | -| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | -| [@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) | -| [@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) | -| [@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), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | -| [@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) | +| Package | Purpose | Docs | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [@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) | +| [@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) | +| [@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) | +| [@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) | +| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | +| [@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) | +| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | +| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | +| [@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) | +| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | +| [@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) | **Observability** diff --git a/packages/shared/src/http.test.ts b/packages/shared/src/http.test.ts index 77fcb41ed..371ab387d 100644 --- a/packages/shared/src/http.test.ts +++ b/packages/shared/src/http.test.ts @@ -1,4 +1,7 @@ import { + decodeCacheTagHeader, + encodeCacheTag, + encodeCacheTagHeader, isCompressibleContentType, isNoTransformCacheControl, matchesHttpPath, @@ -9,6 +12,7 @@ import { pathToHttpPath, varyByAcceptEncoding, } from './http' +import { tryDecodeURIComponent } from './uri' describe('pathToHttpPath', () => { it('produces a leading slash', () => { @@ -248,3 +252,51 @@ describe('isNoTransformCacheControl', () => { expect(isNoTransformCacheControl('no-transform-extension')).toBe(false) }) }) + +describe('encodeCacheTag', () => { + it.each([ + ['leaves plain tags alone', 'planets', 'planets'], + ['leaves other printable ASCII alone', 'a1!~*\'()-_.:/?', 'a1!~*\'()-_.:/?'], + ['escapes the comma separator', 'a,b', 'a%2Cb'], + ['escapes the percent escape', '100%', '100%25'], + ['escapes uppercase letters by code point', 'Planets', '%50lanets'], + ['escapes spaces', 'sp ace', 'sp%20ace'], + ['escapes control characters', 'a\nb', 'a%0Ab'], + ['escapes delete', 'a\x7Fb', 'a%7Fb'], + ['escapes non-ASCII as UTF-8', 'tiếng việt', 'ti%E1%BA%BFng%20vi%E1%BB%87t'], + ['escapes astral characters as UTF-8', 'a😀', 'a%F0%9F%98%80'], + ])('%s', (_, tag, encoded) => { + expect(encodeCacheTag(tag)).toBe(encoded) + expect(tryDecodeURIComponent(encoded)).toBe(tag) + }) + + it('keeps case-folded tags distinct', () => { + expect(encodeCacheTag('Planets')).not.toBe(encodeCacheTag('planets')) + expect(encodeCacheTag('Planets').toLowerCase()).not.toBe(encodeCacheTag('planets').toLowerCase()) + }) + + it('encodes identically across calls, since the pattern is shared', () => { + expect(encodeCacheTag('A,B%C')).toBe(encodeCacheTag('A,B%C')) + expect(encodeCacheTag('A,B%C')).toBe('%41%2C%42%25%43') + }) +}) + +describe('encodeCacheTagHeader & decodeCacheTagHeader', () => { + it('joins encoded tags with commas, and round-trips the list', () => { + const tags = ['plain', 'a,b', '100%', 'CamelCase', 'tiếng việt', 'sp ace'] + + expect(encodeCacheTagHeader(['a', 'b'])).toBe('a,b') + expect(encodeCacheTagHeader(tags)).toBe(tags.map(tag => encodeCacheTag(tag)).join(',')) + expect(decodeCacheTagHeader(encodeCacheTagHeader(tags))).toEqual(tags) + }) + + it('keeps empty tags instead of dropping them', () => { + expect(encodeCacheTagHeader(['a', '', 'b'])).toBe('a,,b') + expect(decodeCacheTagHeader('a,,b')).toEqual(['a', '', 'b']) + expect(decodeCacheTagHeader('')).toEqual(['']) + }) + + it('decodes malformed escapes as-is', () => { + expect(decodeCacheTagHeader('%zz')).toEqual(['%zz']) + }) +}) diff --git a/packages/shared/src/http.ts b/packages/shared/src/http.ts index b037e89c0..3e2cf9998 100644 --- a/packages/shared/src/http.ts +++ b/packages/shared/src/http.ts @@ -129,3 +129,54 @@ export function isCompressibleContentType(contentType: string | null | undefined return COMPRESSIBLE_CONTENT_TYPE_REGEX.test(contentType) } + +/** + * Characters a cache tag cannot carry literally: `%` and `,` because they are + * the encoding's own syntax, `A-Z` because caches like Cloudflare Workers + * Caching fold case, and anything outside printable ASCII because it cannot + * appear in a header value. + * + * Safe to share across calls: `String.prototype.replace` resets `lastIndex` + * on a global regex before it matches. + */ +const UNSAFE_CACHE_TAG_CHARS = /[^\x21-\x7E]|[%,A-Z]/gu + +/** + * Percent-encodes only {@link UNSAFE_CACHE_TAG_CHARS} in a cache tag, so + * typical tags stay readable. Uppercase letters are encoded because caches + * like Cloudflare Workers Caching match tags case-insensitively; the encoded + * form stays unambiguous under case folding. Reach for this where tags travel + * one at a time, such as a purge API. + * + * @see {@link https://orpc.dev/docs/helpers/cache#handler-plugin | Cache Helpers - Handler Plugin} + */ +export function encodeCacheTag(tag: string): string { + // encodeURIComponent emits the UTF-8 bytes a percent escape needs, but leaves + // the URI unreserved set alone, so the uppercase letters in it are escaped by + // hand. Those are single-byte ASCII, so the code point is the byte. + return tag.replace( + UNSAFE_CACHE_TAG_CHARS, + char => char >= 'A' && char <= 'Z' + ? `%${char.charCodeAt(0).toString(16).toUpperCase()}` + : encodeURIComponent(char), + ) +} + +/** + * Joins {@link encodeCacheTag}-encoded tags with commas into a header value. + * + * @see {@link https://orpc.dev/docs/helpers/cache#handler-plugin | Cache Helpers - Handler Plugin} + */ +export function encodeCacheTagHeader(tags: readonly string[]): string { + return tags.map(tag => encodeCacheTag(tag)).join(',') +} + +/** + * Decodes a header value produced by {@link encodeCacheTagHeader} back into + * tags, empty ones included. Every non-empty tag list round-trips exactly. + * + * @see {@link https://orpc.dev/docs/helpers/cache#handler-plugin | Cache Helpers - Handler Plugin} + */ +export function decodeCacheTagHeader(header: string): string[] { + return header.split(',').map(tryDecodeURIComponent) +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index b904d0f29..150c6bcd1 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -9,6 +9,7 @@ export * from './http' export * from './id' export * from './interceptor' export * from './iterator' +export * from './lock' export * from './object' export * from './plugin' export * from './promise' @@ -16,6 +17,7 @@ export * from './proxy' export * from './queue' export * from './signal' export * from './stream' +export * from './time' export * from './tracing' export * from './types' export * from './uri' diff --git a/packages/shared/src/lock.test.ts b/packages/shared/src/lock.test.ts new file mode 100644 index 000000000..f1e9d35cd --- /dev/null +++ b/packages/shared/src/lock.test.ts @@ -0,0 +1,50 @@ +import { MemoryLock } from './lock' + +describe('memoryLock', () => { + it('runs callbacks one key at a time, in order, telling later callers they waited', async () => { + const lock = new MemoryLock() + const order: string[] = [] + let release!: () => void + const held = new Promise((resolve) => { + release = resolve + }) + + const first = lock.run('k', async (waited) => { + order.push(`first:${waited}`) + await held + return 'first' + }) + const second = lock.run('k', async (waited) => { + order.push(`second:${waited}`) + return 'second' + }) + const third = lock.run('k', async (waited) => { + order.push(`third:${waited}`) + return 'third' + }) + + await expect(lock.run('other', async waited => waited)).resolves.toBe(false) + expect(order).toEqual(['first:false']) + + release() + + await expect(Promise.all([first, second, third])).resolves.toEqual(['first', 'second', 'third']) + expect(order).toEqual(['first:false', 'second:true', 'third:true']) + }) + + it('hands the turn on when a callback throws, and frees the key afterwards', async () => { + const lock = new MemoryLock() + let fail!: (error: Error) => void + + const first = lock.run('k', () => new Promise((_, reject) => { + fail = reject + })) + const second = lock.run('k', async waited => waited) + + fail(new Error('boom')) + + await expect(first).rejects.toThrow('boom') + await expect(second).resolves.toBe(true) + await expect(lock.run('k', async waited => waited)).resolves.toBe(false) + }) +}) diff --git a/packages/shared/src/lock.ts b/packages/shared/src/lock.ts new file mode 100644 index 000000000..5572a418c --- /dev/null +++ b/packages/shared/src/lock.ts @@ -0,0 +1,30 @@ +/** + * A per-key mutex for one process. Callers of one key run one at a time in + * order, while other keys run independently. + * + * @see {@link https://orpc.dev/docs/helpers/cache#adapters | Cache Helpers - Adapters} + */ +export class MemoryLock { + private readonly pending = new Map>() + + /** + * Runs `fn` once the key is free. `waited` is `true` when another caller + * held it first. + */ + async run(key: string, fn: (waited: boolean) => Promise): Promise { + const previous = this.pending.get(key) + const run = () => fn(previous !== undefined) + const current = previous?.then(run, run) ?? run() + + this.pending.set(key, current) + + try { + return await current + } + finally { + if (this.pending.get(key) === current) { + this.pending.delete(key) + } + } + } +} diff --git a/packages/shared/src/object.test.ts b/packages/shared/src/object.test.ts index 83639b88e..e8a2b9e4f 100644 --- a/packages/shared/src/object.test.ts +++ b/packages/shared/src/object.test.ts @@ -1,7 +1,7 @@ import * as a from 'arktype' import * as v from 'valibot' import z from 'zod' -import { bindMethods, clone, findDeepMatches, get, getConstructor, getConstructors, getOwn, isPlainObject, isPropertyKey, mergeTwoLevels, NullProtoObj, omit, set } from './object' +import { bindMethods, clone, deepSortKeys, findDeepMatches, get, getConstructor, getConstructors, getOwn, isPlainObject, isPropertyKey, mergeTwoLevels, NullProtoObj, omit, set } from './object' it('findDeepMatches', () => { const { maps, values } = findDeepMatches(v => typeof v === 'string', { @@ -647,3 +647,22 @@ describe('bindMethods', () => { expect(methods.double()).toBe(246) }) }) + +describe('deepSortKeys', () => { + it('sorts plain object keys recursively, including inside arrays', () => { + expect(deepSortKeys({ b: 2, a: { d: 4, c: 3 }, list: [{ y: 1, x: 0 }] })) + .toEqual({ a: { c: 3, d: 4 }, b: 2, list: [{ x: 0, y: 1 }] }) + + expect(Object.keys(deepSortKeys({ b: 2, a: 1 }) as object)).toEqual(['a', 'b']) + }) + + it('returns non-plain values as-is', () => { + const date = new Date() + const map = new Map([['b', 2], ['a', 1]]) + + expect(deepSortKeys(date)).toBe(date) + expect(deepSortKeys(map)).toBe(map) + expect(deepSortKeys('str')).toBe('str') + expect(deepSortKeys(undefined)).toBeUndefined() + }) +}) diff --git a/packages/shared/src/object.ts b/packages/shared/src/object.ts index 8f26148fa..8d45a55b6 100644 --- a/packages/shared/src/object.ts +++ b/packages/shared/src/object.ts @@ -163,6 +163,27 @@ export function mergeTwoLevels(first: unknown, second: unknown): unknown { return result } +/** + * Recursively rebuilds plain objects with their keys in sorted order, so two + * structurally equal values produce the same serialized form. Arrays are + * mapped, anything else is returned as-is. + */ +export function deepSortKeys(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map(deepSortKeys) + } + + if (isPlainObject(value)) { + const sorted: Record = {} + for (const key of Object.keys(value).sort()) { + sorted[key] = deepSortKeys(value[key]) + } + return sorted + } + + return value +} + export function omit( obj: T, keys: readonly K[], diff --git a/packages/shared/src/time.test.ts b/packages/shared/src/time.test.ts new file mode 100644 index 000000000..0212ed38f --- /dev/null +++ b/packages/shared/src/time.test.ts @@ -0,0 +1,12 @@ +import { nowInSeconds } from './time' + +describe('nowInSeconds', () => { + it('floors the current time to whole seconds', () => { + vi.useFakeTimers() + vi.setSystemTime(1999) + + expect(nowInSeconds()).toBe(1) + + vi.useRealTimers() + }) +}) diff --git a/packages/shared/src/time.ts b/packages/shared/src/time.ts new file mode 100644 index 000000000..7ff3e4ccd --- /dev/null +++ b/packages/shared/src/time.ts @@ -0,0 +1,6 @@ +/** + * The current unix time in seconds. + */ +export function nowInSeconds(): number { + return Math.floor(Date.now() / 1000) +} diff --git a/packages/swr/README.md b/packages/swr/README.md index 26de3a584..32e4a452f 100644 --- a/packages/swr/README.md +++ b/packages/swr/README.md @@ -46,29 +46,30 @@ You can read the documentation [here](https://orpc.dev). **Built-in features** -| Package | Purpose | Docs | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | -| [@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) | -| [@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) | -| [@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) | +| Package | Purpose | Docs | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | +| [@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) | +| [@orpc/experimental-cache](https://www.npmjs.com/package/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](https://orpc.dev/docs/helpers/cache) | +| [@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) | +| [@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) | **Framework & ecosystem integrations** -| Package | Purpose | Docs | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [@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) | -| [@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) | -| [@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) | -| [@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) | -| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | -| [@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) | -| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | -| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | -| [@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) | -| [@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) | -| [@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), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | -| [@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) | +| Package | Purpose | Docs | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [@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) | +| [@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) | +| [@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) | +| [@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) | +| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | +| [@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) | +| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | +| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | +| [@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) | +| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | +| [@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) | **Observability** diff --git a/packages/tanstack-query/README.md b/packages/tanstack-query/README.md index a75191330..a2d379bf4 100644 --- a/packages/tanstack-query/README.md +++ b/packages/tanstack-query/README.md @@ -46,29 +46,30 @@ You can read the documentation [here](https://orpc.dev). **Built-in features** -| Package | Purpose | Docs | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | -| [@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) | -| [@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) | -| [@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) | +| Package | Purpose | Docs | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | +| [@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) | +| [@orpc/experimental-cache](https://www.npmjs.com/package/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](https://orpc.dev/docs/helpers/cache) | +| [@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) | +| [@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) | **Framework & ecosystem integrations** -| Package | Purpose | Docs | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [@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) | -| [@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) | -| [@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) | -| [@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) | -| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | -| [@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) | -| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | -| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | -| [@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) | -| [@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) | -| [@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), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | -| [@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) | +| Package | Purpose | Docs | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [@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) | +| [@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) | +| [@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) | +| [@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) | +| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | +| [@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) | +| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | +| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | +| [@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) | +| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | +| [@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) | **Observability** diff --git a/packages/trpc/README.md b/packages/trpc/README.md index 9c5ab22f7..02854af1e 100644 --- a/packages/trpc/README.md +++ b/packages/trpc/README.md @@ -46,29 +46,30 @@ You can read the documentation [here](https://orpc.dev). **Built-in features** -| Package | Purpose | Docs | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | -| [@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) | -| [@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) | -| [@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) | +| Package | Purpose | Docs | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | +| [@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) | +| [@orpc/experimental-cache](https://www.npmjs.com/package/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](https://orpc.dev/docs/helpers/cache) | +| [@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) | +| [@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) | **Framework & ecosystem integrations** -| Package | Purpose | Docs | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [@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) | -| [@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) | -| [@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) | -| [@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) | -| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | -| [@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) | -| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | -| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | -| [@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) | -| [@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) | -| [@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), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | -| [@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) | +| Package | Purpose | Docs | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [@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) | +| [@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) | +| [@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) | +| [@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) | +| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | +| [@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) | +| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | +| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | +| [@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) | +| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | +| [@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) | **Observability** diff --git a/packages/valibot/README.md b/packages/valibot/README.md index cb1e91ca5..0c493bba7 100644 --- a/packages/valibot/README.md +++ b/packages/valibot/README.md @@ -46,29 +46,30 @@ You can read the documentation [here](https://orpc.dev). **Built-in features** -| Package | Purpose | Docs | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | -| [@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) | -| [@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) | -| [@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) | +| Package | Purpose | Docs | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | +| [@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) | +| [@orpc/experimental-cache](https://www.npmjs.com/package/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](https://orpc.dev/docs/helpers/cache) | +| [@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) | +| [@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) | **Framework & ecosystem integrations** -| Package | Purpose | Docs | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [@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) | -| [@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) | -| [@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) | -| [@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) | -| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | -| [@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) | -| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | -| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | -| [@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) | -| [@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) | -| [@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), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | -| [@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) | +| Package | Purpose | Docs | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [@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) | +| [@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) | +| [@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) | +| [@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) | +| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | +| [@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) | +| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | +| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | +| [@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) | +| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | +| [@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) | **Observability** diff --git a/packages/zod/README.md b/packages/zod/README.md index ff23c7467..5d3216984 100644 --- a/packages/zod/README.md +++ b/packages/zod/README.md @@ -46,29 +46,30 @@ You can read the documentation [here](https://orpc.dev). **Built-in features** -| Package | Purpose | Docs | -| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | -| [@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) | -| [@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) | -| [@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) | +| Package | Purpose | Docs | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher) | Pub/Sub with memory, Redis, and Upstash adapters. | [Publisher](https://orpc.dev/docs/helpers/publisher) | +| [@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) | +| [@orpc/experimental-cache](https://www.npmjs.com/package/@orpc/experimental-cache) | Tag-based caching and revalidation with memory, Redis, Upstash, and Vercel adapters. | [Cache](https://orpc.dev/docs/helpers/cache) | +| [@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) | +| [@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) | **Framework & ecosystem integrations** -| Package | Purpose | Docs | -| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [@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) | -| [@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) | -| [@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) | -| [@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) | -| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | -| [@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) | -| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | -| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | -| [@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) | -| [@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) | -| [@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), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | -| [@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) | +| Package | Purpose | Docs | +| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [@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) | +| [@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) | +| [@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) | +| [@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) | +| [@orpc/swr](https://www.npmjs.com/package/@orpc/swr) | Integrate with [SWR](https://swr.vercel.app/). | [SWR](https://orpc.dev/docs/integrations/swr) | +| [@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) | +| [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect) | Integrate with [Effect](https://effect.website/). | [Effect](https://orpc.dev/docs/integrations/effect) | +| [@orpc/nest](https://www.npmjs.com/package/@orpc/nest) | Implement your contract with [NestJS](https://nestjs.com/). | [NestJS](https://orpc.dev/docs/integrations/nest) | +| [@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) | +| [@orpc/bun](https://www.npmjs.com/package/@orpc/bun) | Adapters for [Bun's Redis](https://bun.sh/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit) | +| [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare) | Adapters for [Cloudflare Workers](https://developers.cloudflare.com/workers/). | [Cache](https://orpc.dev/docs/helpers/cache), [Publisher](https://orpc.dev/docs/helpers/publisher), [Rate Limit](https://orpc.dev/docs/helpers/ratelimit), [Traces](https://orpc.dev/docs/integrations/cloudflare-traces) | +| [@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) | **Observability** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3945ccaac..b67d9a2ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -133,6 +133,9 @@ importers: '@orpc/evlog': specifier: workspace:* version: link:packages/evlog + '@orpc/experimental-cache': + specifier: workspace:* + version: link:packages/cache '@orpc/experimental-effect': specifier: workspace:* version: link:packages/effect @@ -316,6 +319,9 @@ importers: '@orpc/evlog': specifier: workspace:* version: link:../../packages/evlog + '@orpc/experimental-cache': + specifier: workspace:* + version: link:../../packages/cache '@orpc/openapi': specifier: workspace:* version: link:../../packages/openapi @@ -420,6 +426,9 @@ importers: '@orpc/client': specifier: workspace:* version: link:../client + '@orpc/experimental-cache': + specifier: workspace:* + version: link:../cache '@orpc/publisher': specifier: workspace:* version: link:../publisher @@ -441,7 +450,32 @@ importers: version: 1.4.0 redis: specifier: ^6.2.1 - version: 6.2.1(@opentelemetry/api@1.9.1) + version: 6.2.1(@node-rs/xxhash@1.7.7)(@opentelemetry/api@1.9.1) + + packages/cache: + dependencies: + '@orpc/client': + specifier: workspace:* + version: link:../client + '@orpc/server': + specifier: workspace:* + version: link:../server + '@orpc/shared': + specifier: workspace:* + version: link:../shared + '@standardserver/core': + specifier: ^0.8.2 + version: 0.8.2 + devDependencies: + '@upstash/redis': + specifier: ^1.38.2 + version: 1.38.3 + '@vercel/functions': + specifier: ^3.9.5 + version: 3.9.5(ws@8.21.3) + redis: + specifier: ^6.2.1 + version: 6.2.1(@node-rs/xxhash@1.7.7)(@opentelemetry/api@1.9.1) packages/client: dependencies: @@ -467,6 +501,9 @@ importers: '@orpc/client': specifier: workspace:* version: link:../client + '@orpc/experimental-cache': + specifier: workspace:* + version: link:../cache '@orpc/publisher': specifier: workspace:* version: link:../publisher @@ -864,7 +901,7 @@ importers: version: 1.38.3 redis: specifier: ^6.2.1 - version: 6.2.1(@opentelemetry/api@1.9.1) + version: 6.2.1(@node-rs/xxhash@1.7.7)(@opentelemetry/api@1.9.1) packages/ratelimit: dependencies: @@ -886,7 +923,7 @@ importers: version: 1.38.3 redis: specifier: ^6.2.1 - version: 6.2.1(@opentelemetry/api@1.9.1) + version: 6.2.1(@node-rs/xxhash@1.7.7)(@opentelemetry/api@1.9.1) packages/server: dependencies: @@ -15927,27 +15964,28 @@ snapshots: dependencies: quansync: 1.0.0 - '@redis/bloom@6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1))': + '@redis/bloom@6.2.1(@redis/client@6.2.1(@node-rs/xxhash@1.7.7)(@opentelemetry/api@1.9.1))': dependencies: - '@redis/client': 6.2.1(@opentelemetry/api@1.9.1) + '@redis/client': 6.2.1(@node-rs/xxhash@1.7.7)(@opentelemetry/api@1.9.1) - '@redis/client@6.2.1(@opentelemetry/api@1.9.1)': + '@redis/client@6.2.1(@node-rs/xxhash@1.7.7)(@opentelemetry/api@1.9.1)': dependencies: cluster-key-slot: 1.1.2 optionalDependencies: + '@node-rs/xxhash': 1.7.7 '@opentelemetry/api': 1.9.1 - '@redis/json@6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1))': + '@redis/json@6.2.1(@redis/client@6.2.1(@node-rs/xxhash@1.7.7)(@opentelemetry/api@1.9.1))': dependencies: - '@redis/client': 6.2.1(@opentelemetry/api@1.9.1) + '@redis/client': 6.2.1(@node-rs/xxhash@1.7.7)(@opentelemetry/api@1.9.1) - '@redis/search@6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1))': + '@redis/search@6.2.1(@redis/client@6.2.1(@node-rs/xxhash@1.7.7)(@opentelemetry/api@1.9.1))': dependencies: - '@redis/client': 6.2.1(@opentelemetry/api@1.9.1) + '@redis/client': 6.2.1(@node-rs/xxhash@1.7.7)(@opentelemetry/api@1.9.1) - '@redis/time-series@6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1))': + '@redis/time-series@6.2.1(@redis/client@6.2.1(@node-rs/xxhash@1.7.7)(@opentelemetry/api@1.9.1))': dependencies: - '@redis/client': 6.2.1(@opentelemetry/api@1.9.1) + '@redis/client': 6.2.1(@node-rs/xxhash@1.7.7)(@opentelemetry/api@1.9.1) '@replit/codemirror-css-color-picker@6.3.0(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.9)': dependencies: @@ -23672,13 +23710,13 @@ snapshots: unified: 11.0.5 vfile: 6.0.3 - redis@6.2.1(@opentelemetry/api@1.9.1): + redis@6.2.1(@node-rs/xxhash@1.7.7)(@opentelemetry/api@1.9.1): dependencies: - '@redis/bloom': 6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1)) - '@redis/client': 6.2.1(@opentelemetry/api@1.9.1) - '@redis/json': 6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1)) - '@redis/search': 6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1)) - '@redis/time-series': 6.2.1(@redis/client@6.2.1(@opentelemetry/api@1.9.1)) + '@redis/bloom': 6.2.1(@redis/client@6.2.1(@node-rs/xxhash@1.7.7)(@opentelemetry/api@1.9.1)) + '@redis/client': 6.2.1(@node-rs/xxhash@1.7.7)(@opentelemetry/api@1.9.1) + '@redis/json': 6.2.1(@redis/client@6.2.1(@node-rs/xxhash@1.7.7)(@opentelemetry/api@1.9.1)) + '@redis/search': 6.2.1(@redis/client@6.2.1(@node-rs/xxhash@1.7.7)(@opentelemetry/api@1.9.1)) + '@redis/time-series': 6.2.1(@redis/client@6.2.1(@node-rs/xxhash@1.7.7)(@opentelemetry/api@1.9.1)) transitivePeerDependencies: - '@node-rs/xxhash' - '@opentelemetry/api' diff --git a/tests/plugins/all-plugins.test.ts b/tests/plugins/all-plugins.test.ts index 132dfbb0d..25e0fb6a2 100644 --- a/tests/plugins/all-plugins.test.ts +++ b/tests/plugins/all-plugins.test.ts @@ -16,6 +16,7 @@ import { import { oc } from '@orpc/contract' import { RequestValidationLinkPlugin, ResponseValidationLinkPlugin } from '@orpc/contract/plugins' import { EvlogHandlerPlugin } from '@orpc/evlog' +import { CacheHandlerPlugin } from '@orpc/experimental-cache' import { HibernationHandlerPlugin } from '@orpc/hibernation' import { SmartCoercionHandlerPlugin, SmartCoercionLinkPlugin } from '@orpc/json-schema' import { BatchResponseCompressionHandlerPlugin, StaticFileHandlerPlugin, TmpFileUploadHandlerPlugin } from '@orpc/node' @@ -71,6 +72,7 @@ const router = implementer.router({ function createHandlerPlugins() { return [ + new CacheHandlerPlugin({ headers: ['cache-control'] }), new BatchHandlerPlugin(), new BatchResponseCompressionHandlerPlugin({ threshold: 0 }), new CORSHandlerPlugin(),