diff --git a/README.md b/README.md index 244fba805..7231455f7 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..d94f560dc --- /dev/null +++ b/apps/content/docs/helpers/cache.mdx @@ -0,0 +1,335 @@ +--- +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 + +Everything builds on the `CacheStore` interface: `fetch` returns the entry under a key and fills it when there is none, and `revalidate` invalidates entries by tag. A router shares one store, passed through the request context as `cache/store`, as the `CacheContext` interface describes. Use one of these adapters or write your own: + +| Name | Adapter for | +| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | +| [`MemoryCacheStore`](#memory) | In-memory storage | +| [`RedisCacheStore`](#redis) | [Redis](https://github.com/redis/redis) | +| [`UpstashCacheStore`](#upstash) | [Upstash Redis](https://github.com/upstash/redis-js) | +| [`BunRedisCacheStore`](#bun) | [Bun's Redis](https://bun.com/docs/runtime/redis) | +| [`VercelCacheStore`](#vercel) | [Vercel Runtime Cache](https://vercel.com/docs/caching/runtime-cache) | +| [`experimental_WorkersCacheStore`](#cloudflare-workers-caching) | [Cloudflare Workers Caching](https://developers.cloudflare.com/workers/cache/), purge only | + +```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 is fresh for `ttl` seconds and kept for a further `swr` window, during which `fetch` still returns it with a past `expiresAt` while one caller refreshes it in the background. Revalidating a tag invalidates every entry carrying it, fresh or stale. Durations are in seconds throughout. + +## Cache Middleware + +The `cache` helper creates middleware that caches the output of [procedures](/docs/procedure). A hit returns the cached output without running the handler; a miss runs the handler once, even for concurrent callers, and stores the result. The `key`, `tags`, `ttl`, `swr`, and `enabled` options accept static values or functions of the middleware options and input. + +`key` defaults to the procedure path and input. When provided, it is used as given, so procedures sharing a key 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 hand the output straight to their serializer. Values it cannot represent, such as [AsyncIteratorObject](/docs/async-iterator-object), readable streams, Blob, and File, do not survive the round trip, so do not cache procedures returning them. +::: + +:::warning +An entry is shared by everyone using its key. If output depends on the requester, put the distinguishing part in `key`, or resolve `enabled` to `false` to bypass caching for that request. +::: + +### Stale While Revalidate + +Past `ttl` but within `swr`, the middleware returns the stale output at once and re-runs the procedure in the background to refresh the entry. Concurrent stale hits refresh once, and nothing older than `ttl + swr` is ever served. + +On runtimes that stop pending work once the response is sent, such as Cloudflare Workers, pass `cache/waitUntil` through the context so 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 pass 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 or a function of the middleware options and input. When the procedure throws, or `tags` resolves to `null` or `undefined`, nothing is revalidated. When the store fails to revalidate, the request fails even though the mutation already ran, so the failure is visible; retrying such a request repeats the mutation. + +```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 activity of [Cache Middleware](#cache-middleware) and [Revalidate Middleware](#revalidate-middleware) into response headers. Only the headers you list are set: + +- `orpc-cache-tag` carries the tags the response depends on. +- `orpc-cache-tag-invalidation` carries the tags the request revalidated, 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, and the encoded form stays unambiguous under case folding. `decodeCacheTagHeader` from `@orpc/shared` parses 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` listed, a response cache in front serves cached responses without invoking your server. Pair it with a purge-capable store, such as `experimental_WorkersCacheStore`, so revalidations purge the front cache too. 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 responses must stay 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, so nested procedures never leak their tags into the response. Headers appear only on successful responses. +::: + +:::tip[Cross-Origin Clients] +The headers carry oRPC-specific names on purpose: CDN conventions like `Cache-Tag` can be consumed and stripped by intermediaries before reaching the browser, while these always arrive intact. 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. +::: + +## Adapters + +### Memory + +Stores entries in the process. Suited to development, testing, and single-instance deployments. + +```ts +import { MemoryCacheStore } from '@orpc/experimental-cache/memory' + +const store = new MemoryCacheStore({ + /** + * Serializer used to encode non-string keys. + * + * @default RPCJsonSerializer + */ + serializer: undefined, +}) +``` + +### Redis + +Stores entries as Redis hashes and drives every operation through Lua scripts, so a hit costs one round trip and a miss two. The lock taken on a miss is released when the fill finishes, or after `lockTtl` if it never does. The client is connected lazily when needed. + +```ts +import { RedisCacheStore } from '@orpc/experimental-cache/redis' +import { createClient } from 'redis' + +const client = createClient({ url: 'redis://localhost:6379' }) + +const store = new RedisCacheStore(client, { + /** + * The prefix to use for Redis keys. + * + * @default undefined + */ + prefix: undefined, + + /** + * Serializer for keys and cached outputs. + * + * @default RPCJsonSerializer + */ + serializer: undefined, + + /** + * How long a lock may be held, in seconds, so a crashed holder frees its waiters. + * + * @default 10 + */ + lockTtl: 10, +}) +``` + +The Redis, Upstash, and Bun stores share `BaseRedisCacheStore` from `@orpc/experimental-cache/base-redis`, which holds the scripts and the flow. A store for another Redis-compatible client only has to run a script. + +### Upstash + +The [Redis](#redis) adapter for Upstash's REST client. It shares the key and entry format with `RedisCacheStore`, so both can serve the same database. + +```ts +import { UpstashCacheStore } from '@orpc/experimental-cache/upstash' +import { Redis } from '@upstash/redis' + +const redis = Redis.fromEnv() + +const store = new UpstashCacheStore(redis, { + /** + * The prefix to use for Redis keys. + * + * @default undefined + */ + prefix: undefined, + + /** + * Serializer for keys and cached outputs. + * + * @default RPCJsonSerializer + */ + serializer: undefined, + + /** + * How long a lock may be held, in seconds, so a crashed holder frees its waiters. + * + * @default 10 + */ + lockTtl: 10, +}) +``` + +### Bun + +The [Redis](#redis) adapter for Bun's built-in Redis client, from `@orpc/bun`. It shares the key and entry format with `RedisCacheStore`, so both can serve the same database. + +```ts +import { BunRedisCacheStore } from '@orpc/bun' +import { redis } from 'bun' + +const store = new BunRedisCacheStore(redis, { + /** + * The prefix to use for Redis keys. + * + * @default undefined + */ + prefix: undefined, + + /** + * Serializer for keys and cached outputs. + * + * @default RPCJsonSerializer + */ + serializer: undefined, + + /** + * How long a lock may be held, in seconds, so a crashed holder frees its waiters. + * + * @default 10 + */ + lockTtl: 10, +}) +``` + +### Vercel + +Stores entries in the [Vercel Runtime Cache](https://vercel.com/docs/caching/runtime-cache), expiring tags natively through `expireTag`. Outside Vercel, the default `getCache()` falls back to an in-memory cache. + +```ts +import { VercelCacheStore } from '@orpc/experimental-cache/vercel' +import { getCache } from '@vercel/functions' + +const store = new VercelCacheStore({ + /** + * The Vercel Runtime Cache to use. + * + * @default getCache() + */ + cache: getCache(), + + /** + * Serializer for keys and cached outputs. + * + * @default RPCJsonSerializer + */ + serializer: undefined, +}) +``` + +### Cloudflare Workers Caching + +A purge-only store from `@orpc/cloudflare`. [Workers Caching](https://developers.cloudflare.com/workers/cache/) caches whole responses in front of the Worker through the `cache-control` and `cache-tag` headers of the [Handler Plugin](#handler-plugin), so every `fetch` runs the procedure and stores nothing, and `revalidate` purges the tags from the front cache. + +It requires `"cache": { "enabled": true }` in your wrangler configuration. Purges are scoped to the calling entrypoint, tags match case-insensitively, and purge calls use the Free tier rate limits regardless of your plan. + +```ts +import { experimental_WorkersCacheStore as WorkersCacheStore } from '@orpc/cloudflare' + +const store = new WorkersCacheStore({ + /** + * The Workers Caching purge surface, such as `ctx.cache`. + * + * @default cache from `cloudflare:workers` + */ + cache: undefined, +}) +``` 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 1dcfaf548..962a457d7 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 add18b676..4dffe92c0 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 080b12064..4cc43fb11 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..70626ec47 --- /dev/null +++ b/packages/bun/src/redis-cache.test.ts @@ -0,0 +1,251 @@ +import { RPCJsonSerializer } 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 RPCJsonSerializer() + 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({ 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('reloads a script once the server answers NOSCRIPT for its cached sha', async () => { + const { store } = createTestingStore() + const scriptShas = Reflect.get(store, 'scriptShas') as Map + const unknownSha = '0'.repeat(40) + + await store.fetch('k', async () => 'v') + for (const script of scriptShas.keys()) { + scriptShas.set(script, unknownSha) + } + + await expect(store.fetch('k', async () => 'other')).resolves.toMatchObject({ output: 'v' }) + await expect(store.fetch('k2', async () => 'w')).resolves.toMatchObject({ output: 'w' }) + expect([...scriptShas.values()]).not.toContain(unknownSha) + }, { 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('drops output computed before a revalidation that landed during its fill', async () => { + const { store } = createTestingStore() + let finish!: (output: string) => void + let started!: () => void + const filling = new Promise((resolve) => { + started = resolve + }) + + const first = store.fetch('k', () => { + started() + return new Promise((resolve) => { + finish = resolve + }) + }, { tags: ['t'] }) + await filling + await store.revalidate({ tags: ['t'] }) + finish('outdated') + + await expect(first).resolves.toMatchObject({ output: 'outdated' }) + await expect(store.fetch('k', async () => 'fresh', { tags: ['t'] })).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..5cee49651 --- /dev/null +++ b/packages/bun/src/redis-cache.ts @@ -0,0 +1,49 @@ +import type { BaseRedisCacheStoreOptions } from '@orpc/experimental-cache/base-redis' +import type { RedisClient } from 'bun' +import { BaseRedisCacheStore } from '@orpc/experimental-cache/base-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 once more + * when 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[]): Promise { + try { + return await this.evalSha(script, keys, args) + } + catch (error) { + if (error instanceof Error && error.message.startsWith('NOSCRIPT')) { + this.scriptShas.delete(script) + return this.evalSha(script, keys, args) + } + + throw error + } + } + + private async evalSha(script: string, keys: string[], args: string[]): 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) + } + + return this.redis.send('EVALSHA', [sha, String(keys.length), ...keys, ...args]) + } +} 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..49c7a85b0 --- /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('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..2d59657bb --- /dev/null +++ b/packages/cache/package.json @@ -0,0 +1,109 @@ +{ + "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" + }, + "./base-redis": { + "types": "./dist/adapters/base-redis.d.mts", + "import": "./dist/adapters/base-redis.mjs", + "default": "./dist/adapters/base-redis.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", + "./base-redis": "./src/adapters/base-redis.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:*", + "@standard-server/core": "~0.9.0" + }, + "devDependencies": { + "@upstash/redis": "^1.38.2", + "@vercel/functions": "^3.9.5", + "redis": "^6.2.1" + } +} diff --git a/packages/cache/src/adapters/base-key-value.ts b/packages/cache/src/adapters/base-key-value.ts new file mode 100644 index 000000000..e8aa5cab3 --- /dev/null +++ b/packages/cache/src/adapters/base-key-value.ts @@ -0,0 +1,87 @@ +import type { Promisable, Public } from '@orpc/shared' +import type { CacheEntry, CacheFetchOptions, CacheRevalidateOptions, CacheStore } from '../types' +import { RPCJsonSerializer } from '@orpc/client' +import { encodeCacheKey, isCacheEntryStale } from '../utils' + +export interface BaseKeyValueCacheStoreOptions { + /** + * Serializer for keys, and for cached outputs where the backend stores + * them serialized. + * + * @default RPCJsonSerializer + */ + serializer?: undefined | Public +} + +/** + * Cache store over a key-value backend without an atomic primitive, so + * concurrent callers of one key are coalesced within the process. Subclasses + * read entries by their encoded key and fill the missing ones. + * + * @see {@link https://orpc.dev/docs/helpers/cache#adapters | Cache Helpers - Adapters} + */ +export abstract class BaseKeyValueCacheStore implements CacheStore { + private readonly pending = new Map>() + protected readonly serializer: Public + + constructor(options: BaseKeyValueCacheStoreOptions = {}) { + this.serializer = options.serializer ?? new RPCJsonSerializer() + } + + async fetch(key: unknown, fill: () => Promise, options: CacheFetchOptions = {}): Promise { + const encodedKey = encodeCacheKey(key, this.serializer) + const entry = await this.read(encodedKey) + + if (entry === undefined) { + return this.coalesce(encodedKey, async (waited) => { + const current = waited ? await this.read(encodedKey) : undefined + return current ?? this.fill(encodedKey, fill, options) + }) + } + + if (isCacheEntryStale(entry)) { + const refresh = this.coalesce(encodedKey, async (waited) => { + const current = waited ? await this.read(encodedKey) : undefined + + if (current === undefined || isCacheEntryStale(current)) { + await this.fill(encodedKey, fill, options) + } + }) + + options.waitUntil?.(refresh) + } + + return entry + } + + abstract revalidate(options: CacheRevalidateOptions): Promise + + protected abstract read(encodedKey: string): Promisable + + /** + * Runs `fill` and stores its output. Tag state captured before `fill` runs + * lets a revalidation that lands during it still invalidate the entry. + */ + protected abstract fill(encodedKey: string, fill: () => Promise, options: CacheFetchOptions): Promise + + /** + * Runs `fn` once the key is free, in call order. `waited` is `true` when + * another caller held it first. + */ + private async coalesce(encodedKey: string, fn: (waited: boolean) => Promise): Promise { + const previous = this.pending.get(encodedKey) + const run = () => fn(previous !== undefined) + const current = previous?.then(run, run) ?? run() + + this.pending.set(encodedKey, current) + + try { + return await current + } + finally { + if (this.pending.get(encodedKey) === current) { + this.pending.delete(encodedKey) + } + } + } +} diff --git a/packages/cache/src/adapters/base-redis.ts b/packages/cache/src/adapters/base-redis.ts new file mode 100644 index 000000000..e2b6ce174 --- /dev/null +++ b/packages/cache/src/adapters/base-redis.ts @@ -0,0 +1,244 @@ +import type { RPCJsonSerialization } from '@orpc/client' +import type { Public } from '@orpc/shared' +import type { CacheEntry, CacheFetchOptions, CacheRevalidateOptions, CacheStore } from '../types' +import { RPCJsonSerializer } from '@orpc/client' +import { nowInSeconds, sleep, stringifyJSON } from '@orpc/shared' +import { encodeCacheKey, resolveCacheExpiry } from '../utils' + +/** + * Reads the entry as `[output, tags, expiresAt, evictAt, shouldFill, snapshot]`, + * dropping it when a tag was revalidated since it was stored. A missing or + * stale entry also takes the lock; `shouldFill` reports whether this caller + * got it, and `snapshot` then carries the versions of the tags it will fill + * with, captured before the fill so a revalidation during it still counts. + */ +const FETCH_SCRIPT = ` +local token, lockPx, tagPrefix, now, fillTags = ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5] +local fields = redis.call('HMGET', KEYS[1], 'output', 'tags', 'tagVersions', 'expiresAt', 'evictAt') +local output, tags, versions, expiresAt, evictAt = fields[1], fields[2], fields[3], fields[4], fields[5] + +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] = tagPrefix .. 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(now) +local acquired = false +local snapshot = false +if not output or stale then + acquired = redis.call('SET', KEYS[2], token, 'NX', 'PX', lockPx) and true or false + if acquired and fillTags ~= '' then + local names = cjson.decode(fillTags) + local keys = {} + for i, name in ipairs(names) do + keys[i] = tagPrefix .. name + end + local live = redis.call('MGET', unpack(keys)) + local captured = {} + for i, name in ipairs(names) do + captured[name] = tonumber(live[i] or 0) + end + snapshot = cjson.encode(captured) + end +end + +return { output or false, tags or false, expiresAt or false, evictAt or false, acquired, snapshot } +` + +/** + * Stores the entry with the tag versions captured when its fill started, + * then releases the caller's lock. + */ +const STORE_SCRIPT = ` +local token, output, tags, tagVersions, expiresAt, evictAt = ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5], ARGV[6] +local fields = { 'output', output } + +if tags ~= '' then + fields[#fields + 1] = 'tags' + fields[#fields + 1] = tags + fields[#fields + 1] = 'tagVersions' + fields[#fields + 1] = tagVersions +end + +if expiresAt ~= '' then + fields[#fields + 1] = 'expiresAt' + fields[#fields + 1] = expiresAt + fields[#fields + 1] = 'evictAt' + fields[#fields + 1] = evictAt +end + +redis.call('DEL', KEYS[1]) +redis.call('HSET', KEYS[1], unpack(fields)) + +if evictAt ~= '' then + redis.call('PEXPIREAT', KEYS[1], tonumber(evictAt) * 1000) +end + +if redis.call('GET', KEYS[2]) == token 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 + redis.call('DEL', KEYS[1]) +end +` + +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 keys and cached outputs. + * + * @default RPCJsonSerializer + */ + 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 entryPrefix: string + private readonly lockPrefix: string + private readonly tagPrefix: string + private readonly lockPx: string + private readonly serializer: Public + + constructor(options: BaseRedisCacheStoreOptions = {}) { + const prefix = options.prefix ?? '' + this.entryPrefix = `${prefix}e:` + this.lockPrefix = `${prefix}l:` + this.tagPrefix = `${prefix}t:` + this.lockPx = String((options.lockTtl ?? 10) * 1000) + this.serializer = options.serializer ?? new RPCJsonSerializer() + } + + async fetch(key: unknown, fill: () => Promise, options: CacheFetchOptions = {}): Promise { + const encodedKey = encodeCacheKey(key, this.serializer) + const entryKey = this.entryPrefix + encodedKey + const lockKey = this.lockPrefix + encodedKey + const token = crypto.randomUUID() + const fillTags = options.tags?.length ? stringifyJSON(options.tags) : '' + + while (true) { + const [output, tags, expiresAt, evictAt, shouldFill, snapshot] = await this.run( + FETCH_SCRIPT, + [entryKey, lockKey], + [token, this.lockPx, this.tagPrefix, String(nowInSeconds()), fillTags], + ) as [unknown, unknown, unknown, unknown, unknown, unknown] + + if (output !== null) { + const entry: CacheEntry = { + output: this.serializer.deserialize(parseReply(output) as RPCJsonSerialization), + tags: tags === null ? undefined : parseReply(tags) as string[], + expiresAt: expiresAt === null ? undefined : Number(expiresAt), + evictAt: evictAt === null ? undefined : Number(evictAt), + } + + if (shouldFill) { + const refresh = this.store(entryKey, lockKey, token, fill, options, snapshot) + options.waitUntil?.(refresh) + } + + return entry + } + + if (shouldFill) { + return this.store(entryKey, lockKey, token, fill, options, snapshot) + } + + 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 supports + * it, reloading the script when the server dropped it. + */ + protected abstract run(script: string, keys: string[], args: string[]): Promise + + private async store(entryKey: string, lockKey: string, token: string, fill: () => Promise, options: CacheFetchOptions, snapshot: unknown): Promise { + let output: unknown + let serialized: string + + try { + output = await fill() + const { json, meta } = this.serializer.serialize(output) + serialized = stringifyJSON({ json, meta }) + } + catch (error) { + await this.run(RELEASE_LOCK_SCRIPT, [lockKey], [token]) + throw error + } + + const tags = options.tags?.length ? options.tags : undefined + const { expiresAt, evictAt } = resolveCacheExpiry(options) + + await this.run(STORE_SCRIPT, [entryKey, lockKey], [ + token, + serialized, + tags !== undefined ? stringifyJSON(tags) : '', + snapshot === null ? '' : typeof snapshot === 'string' ? snapshot : stringifyJSON(snapshot as object), + expiresAt !== undefined ? String(expiresAt) : '', + evictAt !== undefined ? String(evictAt) : '', + ]) + + return { output, tags, expiresAt, evictAt } + } +} diff --git a/packages/cache/src/adapters/memory.test.ts b/packages/cache/src/adapters/memory.test.ts new file mode 100644 index 000000000..87cf68b91 --- /dev/null +++ b/packages/cache/src/adapters/memory.test.ts @@ -0,0 +1,183 @@ +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, evictAt: 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, evictAt: 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, evictAt: 2 }) + await expect(store.fetch('k', fill, { ttl: 1, swr: 1, waitUntil })).resolves.toEqual({ output: 'v', tags: undefined, expiresAt: 1, evictAt: 2 }) + 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, evictAt: 3 }) + }) + + 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('drops output computed before a revalidation that landed during its fill', async () => { + const store = new MemoryCacheStore() + let finish!: (output: string) => void + let started!: () => void + const filling = new Promise((resolve) => { + started = resolve + }) + + const first = store.fetch('k', () => { + started() + return new Promise((resolve) => { + finish = resolve + }) + }, { tags: ['t'] }) + await filling + await store.revalidate({ tags: ['t'] }) + finish('outdated') + + await expect(first).resolves.toMatchObject({ output: 'outdated' }) + await expect(store.fetch('k', async () => 'fresh', { tags: ['t'] })).resolves.toMatchObject({ output: 'fresh' }) + }) + + it('drops a refresh computed before a revalidation that landed during it', async () => { + const store = new MemoryCacheStore() + await store.fetch('k', async () => 'v', { tags: ['t'], ttl: 1, swr: 10 }) + + vi.setSystemTime(1500) + let finish!: (output: string) => void + const waitUntil = vi.fn() + + await store.fetch('k', () => new Promise((resolve) => { + finish = resolve + }), { tags: ['t'], ttl: 1, swr: 10, waitUntil }) + await store.revalidate({ tags: ['t'] }) + finish('outdated') + await waitUntil.mock.calls[0]![0] + + await expect(store.fetch('k', async () => 'fresh', { tags: ['t'] })).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..9f608b120 --- /dev/null +++ b/packages/cache/src/adapters/memory.ts @@ -0,0 +1,81 @@ +import type { CacheEntry, CacheFetchOptions, CacheRevalidateOptions } from '../types' +import type { BaseKeyValueCacheStoreOptions } from './base-key-value' +import { nowInSeconds } from '@orpc/shared' +import { resolveCacheExpiry } from '../utils' +import { BaseKeyValueCacheStore } from './base-key-value' + +export type MemoryCacheStoreOptions = BaseKeyValueCacheStoreOptions + +interface MemoryCacheStoreEntry { + output: unknown + /** + * The tags, and the version counter each had when the fill started, 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. + * + * @see {@link https://orpc.dev/docs/helpers/cache#adapters | Cache Helpers - Adapters} + */ +export class MemoryCacheStore extends BaseKeyValueCacheStore { + private readonly entries = new Map() + private readonly tagVersions = new Map() + + constructor(options: MemoryCacheStoreOptions = {}) { + super(options) + } + + async revalidate({ tags }: CacheRevalidateOptions): Promise { + for (const tag of tags) { + this.tagVersions.set(tag, (this.tagVersions.get(tag) ?? 0) + 1) + } + } + + protected 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, + evictAt: entry.evictAt, + } + } + + protected async fill(encodedKey: string, fill: () => Promise, options: CacheFetchOptions): Promise { + const tags = options.tags + const tagVersions = tags?.map(tag => this.tagVersions.get(tag) ?? 0) + const output = await fill() + const { expiresAt, evictAt } = resolveCacheExpiry(options) + + this.entries.set(encodedKey, { output, tags, tagVersions, expiresAt, evictAt }) + + return { output, tags, expiresAt, evictAt } + } +} diff --git a/packages/cache/src/adapters/redis.test.ts b/packages/cache/src/adapters/redis.test.ts new file mode 100644 index 000000000..b21904b11 --- /dev/null +++ b/packages/cache/src/adapters/redis.test.ts @@ -0,0 +1,58 @@ +import { createClient } from 'redis' +import { describeRedisCacheStoreContract } from '../../tests/__shared__/redis-store-contract' +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] = {}) { + const prefix = options.prefix ?? `orpc-redis-cache-store-${crypto.randomUUID()}:` + return { store: new RedisCacheStore(redis, { ...options, prefix }), prefix } + } + + describeCacheStoreContract(() => createTestingStore().store) + describeRedisCacheStoreContract(createTestingStore, { + exists: key => redis.exists(key), + type: key => redis.type(key), + hset: (key, fields) => redis.hSet(key, fields), + scriptFlush: () => redis.scriptFlush(), + }) + + it('reloads a script once the server answers NOSCRIPT for its cached sha', async () => { + const { store } = createTestingStore() + const scriptShas = Reflect.get(store, 'scriptShas') as Map + const unknownSha = '0'.repeat(40) + + await store.fetch('k', async () => 'v') + for (const script of scriptShas.keys()) { + scriptShas.set(script, unknownSha) + } + + await expect(store.fetch('k', async () => 'other')).resolves.toMatchObject({ output: 'v' }) + await expect(store.fetch('k2', async () => 'w')).resolves.toMatchObject({ output: 'w' }) + expect([...scriptShas.values()]).not.toContain(unknownSha) + }) + + it('lazily connects a closed client', async () => { + const lazyRedis = createClient({ url: REDIS_URL }) + const store = new RedisCacheStore(lazyRedis) + + expect(lazyRedis.isOpen).toBe(false) + await expect(store.fetch(crypto.randomUUID(), async () => 'v')).resolves.toMatchObject({ output: 'v' }) + expect(lazyRedis.isOpen).toBe(true) + + await lazyRedis.destroy() + }) +}) diff --git a/packages/cache/src/adapters/redis.ts b/packages/cache/src/adapters/redis.ts new file mode 100644 index 000000000..18b2a796b --- /dev/null +++ b/packages/cache/src/adapters/redis.ts @@ -0,0 +1,52 @@ +import type { RedisClientType } from 'redis' +import type { BaseRedisCacheStoreOptions } from './base-redis' +import { BaseRedisCacheStore } from './base-redis' + +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 once more when + * 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[]): Promise { + if (!this.redis.isOpen) { + await this.redis.connect() + } + + try { + return await this.evalSha(script, keys, args) + } + catch (error) { + if (error instanceof Error && error.message.startsWith('NOSCRIPT')) { + this.scriptShas.delete(script) + return this.evalSha(script, keys, args) + } + + throw error + } + } + + private async evalSha(script: string, keys: string[], args: string[]): Promise { + let sha = this.scriptShas.get(script) + + if (sha === undefined) { + sha = String(await this.redis.scriptLoad(script)) + this.scriptShas.set(script, sha) + } + + return this.redis.evalSha(sha, { keys, arguments: args }) + } +} diff --git a/packages/cache/src/adapters/upstash.test.ts b/packages/cache/src/adapters/upstash.test.ts new file mode 100644 index 000000000..a7927b963 --- /dev/null +++ b/packages/cache/src/adapters/upstash.test.ts @@ -0,0 +1,55 @@ +import { nowInSeconds } from '@orpc/shared' +import { Redis } from '@upstash/redis' +import { describeRedisCacheStoreContract } from '../../tests/__shared__/redis-store-contract' +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 = options.prefix ?? `orpc-upstash-cache-store-${crypto.randomUUID()}:` + return { store: new UpstashCacheStore(client, { ...options, prefix }), prefix } + } + + describeCacheStoreContract(() => createTestingStore().store) + describeRedisCacheStoreContract(createTestingStore, { + exists: key => redis.exists(key), + type: key => redis.type(key), + hset: (key, fields) => redis.hset(key, fields), + scriptFlush: () => redis.scriptFlush(), + }) + + 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' }) + }) +}) diff --git a/packages/cache/src/adapters/upstash.ts b/packages/cache/src/adapters/upstash.ts new file mode 100644 index 000000000..9b4115cd2 --- /dev/null +++ b/packages/cache/src/adapters/upstash.ts @@ -0,0 +1,34 @@ +import type { Redis } from '@upstash/redis' +import type { BaseRedisCacheStoreOptions } from './base-redis' +import { BaseRedisCacheStore } from './base-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..2fb54f62f --- /dev/null +++ b/packages/cache/src/adapters/vercel.test.ts @@ -0,0 +1,151 @@ +import type { RuntimeCache } from '@vercel/functions' +import { RPCJsonSerializer } 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, evictAt: 2 }) + 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 RPCJsonSerializer() + 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..96b70ab65 --- /dev/null +++ b/packages/cache/src/adapters/vercel.ts @@ -0,0 +1,88 @@ +import type { RPCJsonSerialization } from '@orpc/client' +import type { RuntimeCache } from '@vercel/functions' +import type { CacheEntry, CacheFetchOptions, CacheRevalidateOptions } from '../types' +import type { BaseKeyValueCacheStoreOptions } from './base-key-value' +import { nowInSeconds } from '@orpc/shared' +import { getCache } from '@vercel/functions' +import { resolveCacheExpiry } from '../utils' +import { BaseKeyValueCacheStore } from './base-key-value' + +interface VercelCacheStoreEnvelope { + output: RPCJsonSerialization + tags?: readonly string[] + expiresAt?: number | undefined + evictAt?: number | undefined +} + +export interface VercelCacheStoreOptions extends BaseKeyValueCacheStoreOptions { + /** + * The Vercel Runtime Cache to use. + * + * @default getCache() + */ + cache?: RuntimeCache +} + +/** + * 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; for the same reason, a + * revalidation landing while a fill runs is not detected. + * + * @see {@link https://orpc.dev/docs/helpers/cache#adapters | Cache Helpers - Adapters} + */ +export class VercelCacheStore extends BaseKeyValueCacheStore { + private readonly cache: RuntimeCache + + constructor(options: VercelCacheStoreOptions = {}) { + super(options) + this.cache = options.cache ?? getCache() + } + + async revalidate({ tags }: CacheRevalidateOptions): Promise { + await this.cache.expireTag([...tags]) + } + + protected 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), + tags: envelope.tags, + expiresAt: envelope.expiresAt, + evictAt: envelope.evictAt, + } + } + + protected async fill(encodedKey: string, fill: () => Promise, options: CacheFetchOptions): Promise { + const output = await fill() + const tags = options.tags + const { expiresAt, evictAt, retention } = resolveCacheExpiry(options) + const { json, meta } = this.serializer.serialize(output) + + const envelope: VercelCacheStoreEnvelope = { + output: { json, meta }, + tags, + expiresAt, + evictAt, + } + + await this.cache.set(encodedKey, envelope, { + ...(tags?.length ? { tags: [...tags] } : {}), + ...(retention !== undefined ? { ttl: retention } : {}), + }) + + return { output, tags, expiresAt, evictAt } + } +} diff --git a/packages/cache/src/handler-plugin.test.ts b/packages/cache/src/handler-plugin.test.ts new file mode 100644 index 000000000..f600c6398 --- /dev/null +++ b/packages/cache/src/handler-plugin.test.ts @@ -0,0 +1,253 @@ +import type { AnyProcedure } from '@orpc/server' +import type { StandardHandlerPlugin } from '@orpc/server/standard' +import type { StandardHeaders } from '@standard-server/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('sets its headers over ones already on the response', async () => { + const { record, handle } = createTestingHandler(headers, { 'cache-control': 'private, no-store', 'cache-tag': 'stale' }) + record({ caches: [{ tags: ['planets'], ttl: 2 }] }) + + const response = await handle() + + expect(response.headers.get('cache-control')).toBe('public, max-age=2') + expect(response.headers.get('cache-tag')).toBe('planets') + }) + + 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..1beeb380d --- /dev/null +++ b/packages/cache/src/handler-plugin.ts @@ -0,0 +1,134 @@ +import type { AnyProcedure, Context } from '@orpc/server' +import type { StandardHandlerInterceptor, StandardHandlerOptions, StandardHandlerPlugin } from '@orpc/server/standard' +import type { StandardHeaders } from '@standard-server/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. + */ + 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) + + if (rootCache === undefined && rootRevalidation === undefined) { + return response + } + + 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..ca763f803 --- /dev/null +++ b/packages/cache/src/middleware.test.ts @@ -0,0 +1,347 @@ +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, + evictAt: options?.ttl !== undefined ? nowInSeconds() + options.ttl + (options.swr ?? 0) : 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 and swr', async () => { + const now = nowInSeconds() + const stale = createStore({ output: 'stale', tags: ['stored'], expiresAt: now - 10, evictAt: now + 20 }) + const fresh = createStore({ output: 'fresh', tags: ['stored'], expiresAt: now + 60, evictAt: now + 90 }) + const pluginContext: Exclude = { caches: [], revalidations: [] } + const procedure = os + .$context() + .use(cache({ key: 'k', tags: ['t'], swr: 30 })) + .handler(() => 'filled') + 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': fresh, ...context }, path: ['__path__'] }) + await call(procedure, undefined, { context: { 'cache/store': createStore(), ...context }, path: ['__path__'] }) + + // Only what is left of each window is reflected, so headers never outlive the entry. + expect(pluginContext.caches.map(({ ttl, swr }) => ({ ttl, swr }))).toEqual([ + { ttl: 0, swr: expect.closeTo(20, -1) }, + { ttl: expect.closeTo(60, -1), swr: 30 }, + { ttl: undefined, swr: undefined }, + ]) + expect(pluginContext.caches.map(({ tags }) => tags)).toEqual([['stored'], ['stored'], ['t']]) + }) + + 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('never serves an entry whose own fill revalidated one of its tags', 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 tag was captured before the fill and bumped during it, so the entry is invalid on arrival. + await expect(run()).resolves.toBe('fresh') + await expect(run()).resolves.toBe('fresh') + + expect(handlerFn).toHaveBeenCalledTimes(2) + expect(revalidateSpy).toHaveBeenCalledTimes(2) + }) +}) diff --git a/packages/cache/src/middleware.ts b/packages/cache/src/middleware.ts new file mode 100644 index 000000000..f6da2b489 --- /dev/null +++ b/packages/cache/src/middleware.ts @@ -0,0 +1,144 @@ +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; + * `undefined` falls back to the default. + * + * @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) { + if (await value(options.enabled, middlewareOptions, input) === false) { + return middlewareOptions.next() + } + + const [key = [middlewareOptions.path, input], tags, ttl, swr] = await Promise.all([ + value(options.key, middlewareOptions, input), + value(options.tags, middlewareOptions, input), + value(options.ttl, middlewareOptions, input), + value(options.swr, middlewareOptions, 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'], + }) + + const now = nowInSeconds() + + pluginContext?.caches.push({ + procedure: middlewareOptions.procedure, + path: middlewareOptions.path, + tags: entry.tags, + ttl: entry.expiresAt === undefined ? undefined : Math.max(0, entry.expiresAt - now), + swr: entry.expiresAt === undefined || entry.evictAt === undefined + ? undefined + : Math.max(0, entry.evictAt - Math.max(now, entry.expiresAt)), + }) + + 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..6a19a097a --- /dev/null +++ b/packages/cache/src/types.ts @@ -0,0 +1,107 @@ +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 + + /** + * The time (unix timestamp in seconds) after which the entry is no longer + * served, once its stale-while-revalidate window has passed. `undefined` + * means the entry never expires. + */ + evictAt?: 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..61b90e6c6 --- /dev/null +++ b/packages/cache/src/utils.ts @@ -0,0 +1,49 @@ +import type { RPCJsonSerializer } from '@orpc/client' +import type { Public } from '@orpc/shared' +import type { CacheEntry, CacheFetchOptions } 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 +} + +/** + * The entry lifetime an option set describes: when it stops being fresh, + * when it may be evicted (both unix timestamps in seconds), and how long it + * is retained in seconds. All `undefined` when it never expires. + * + * @see {@link https://orpc.dev/docs/helpers/cache#adapters | Cache Helpers - Adapters} + */ +export function resolveCacheExpiry({ ttl, swr }: CacheFetchOptions): { expiresAt: number | undefined, evictAt: number | undefined, retention: number | undefined } { + if (ttl === undefined) { + return { expiresAt: undefined, evictAt: undefined, retention: undefined } + } + + const expiresAt = nowInSeconds() + ttl + + return { expiresAt, evictAt: expiresAt + (swr ?? 0), retention: ttl + (swr ?? 0) } +} diff --git a/packages/cache/tests/__shared__/redis-store-contract.ts b/packages/cache/tests/__shared__/redis-store-contract.ts new file mode 100644 index 000000000..3554017e0 --- /dev/null +++ b/packages/cache/tests/__shared__/redis-store-contract.ts @@ -0,0 +1,169 @@ +import type { CacheStore } from '../../src' +import type { BaseRedisCacheStoreOptions } from '../../src/adapters/base-redis' +import { RPCJsonSerializer } from '@orpc/client' +import { nowInSeconds, sleep, stringifyJSON } from '@orpc/shared' +import { expect, it, vi } from 'vitest' + +export interface RedisCacheStoreContractClient { + exists: (key: string) => Promise + type: (key: string) => Promise + hset: (key: string, fields: Record) => Promise + scriptFlush: () => Promise +} + +/** + * The behavior every Redis-backed store shares, run against one adapter and + * a client that can inspect the keys it writes. `createStore` applies the + * given options over a fresh prefix, or the given one. + */ +export function describeRedisCacheStoreContract( + createStore: (options?: BaseRedisCacheStoreOptions) => { store: CacheStore, prefix: string }, + redis: RedisCacheStoreContractClient, +): void { + it('supports a custom serializer', async () => { + const serializer = new RPCJsonSerializer() + const serializeSpy = vi.spyOn(serializer, 'serialize') + const deserializeSpy = vi.spyOn(serializer, 'deserialize') + const { store } = createStore({ 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 } = createStore() + + 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 } = createStore() + + 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 } = createStore({ prefix: '' }) + 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 } = createStore() + + await redis.hset(`${prefix}e:k`, { output: stringifyJSON({ 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 } = createStore() + + 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('drops output computed before a revalidation that landed during its fill', async () => { + const { store } = createStore() + let finish!: (output: string) => void + let started!: () => void + const filling = new Promise((resolve) => { + started = resolve + }) + + const first = store.fetch('k', () => { + started() + return new Promise((resolve) => { + finish = resolve + }) + }, { tags: ['t'] }) + await filling + await store.revalidate({ tags: ['t'] }) + finish('outdated') + + await expect(first).resolves.toMatchObject({ output: 'outdated' }) + await expect(store.fetch('k', async () => 'fresh', { tags: ['t'] })).resolves.toMatchObject({ output: 'fresh' }) + }) + + it('stays consistent under concurrent fetches and a revalidation on a shared tag', async () => { + const { store } = createStore() + 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 } = createStore({ lockTtl: 1 }) + const { store: waiterStore } = createStore({ 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/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 244fba805..7231455f7 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 9869e6545..4bc1d4f27 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..7f7a7337a --- /dev/null +++ b/packages/cloudflare/src/workers-cache.ts @@ -0,0 +1,46 @@ +import type { CacheEntry, CacheFetchOptions, CacheRevalidateOptions, CacheStore } from '@orpc/experimental-cache' +import { resolveCacheExpiry } from '@orpc/experimental-cache' +import { encodeCacheTag, 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 { + const { expiresAt, evictAt } = resolveCacheExpiry(options) + + return { output: await fill(), tags: options.tags, expiresAt, evictAt } + } + + 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 9f733b023..8003891af 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 21c3c5afb..49342bc0a 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 8e3b0ebb1..7d12914ab 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 0e2251cc7..096081808 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 5c4002aca..0d8268460 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 d4aea2ae4..1c4edb6a8 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 91f43fcfb..ffd43762d 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 2d205c479..923b79f8d 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 1a514038c..26ec5e8f5 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 c962915e4..1c003117d 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 c301e4952..e2ff14dcf 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 79de35582..4e41267af 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 c00a94124..5302585d3 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 28e10f0cd..f3d8febbf 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 c9ca79c37..4c67aadd0 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 9b759fd8b..83d330dbc 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 5bbb46fc4..b35f9969a 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..12fd99a67 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -16,6 +16,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/object.test.ts b/packages/shared/src/object.test.ts index 83639b88e..cc7bbfc00 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,29 @@ 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('keeps an own __proto__ key as data', () => { + const sorted = deepSortKeys(JSON.parse('{"b": 1, "__proto__": {"x": 1}}')) as Record + + expect(Object.keys(sorted)).toEqual(['__proto__', 'b']) + expect(JSON.stringify(sorted)).toBe('{"__proto__":{"x":1},"b":1}') + }) + + 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..8c06475e1 100644 --- a/packages/shared/src/object.ts +++ b/packages/shared/src/object.ts @@ -282,3 +282,19 @@ export function bindMethods( return methods as any } + +export function deepSortKeys(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map(deepSortKeys) + } + + if (isPlainObject(value)) { + const sorted: Record = new NullProtoObj() + for (const key of Object.keys(value).sort()) { + sorted[key] = deepSortKeys(value[key]) + } + return sorted + } + + return value +} 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 b9a02e4e8..aeaecbd06 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 231073c10..322381780 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 35ea17c57..812217360 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 1b88191b2..82688037f 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 83a505ffb..8ae08da47 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..b79c23c7b 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 + '@standard-server/core': + specifier: ~0.9.0 + version: 0.9.0 + 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(),