From acd0fbcf3daea2e7137a4310694c7b0e646ad966 Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Sat, 25 Jul 2026 09:51:34 +0700 Subject: [PATCH 1/8] feat(react-swr): React SWR integration --- README.md | 1 + apps/content/.vitepress/config.ts | 1 + apps/content/docs/integrations/react-swr.md | 181 ++++++++++++ packages/ai-sdk/README.md | 1 + packages/arktype/README.md | 1 + packages/bun/README.md | 1 + packages/client/README.md | 1 + packages/cloudflare/README.md | 1 + packages/contract/README.md | 1 + packages/effect/README.md | 1 + packages/evlog/README.md | 1 + packages/json-schema/README.md | 1 + packages/nest/README.md | 1 + packages/next/README.md | 1 + packages/openapi/README.md | 1 + packages/opentelemetry/README.md | 1 + packages/pinia-colada/README.md | 1 + packages/pino/README.md | 1 + packages/publisher/README.md | 1 + packages/ratelimit/README.md | 1 + packages/react-swr/.gitignore | 26 ++ packages/react-swr/README.md | 199 +++++++++++++ packages/react-swr/package.json | 50 ++++ packages/react-swr/src/index.test.ts | 9 + packages/react-swr/src/index.ts | 13 + packages/react-swr/src/key.test.ts | 13 + packages/react-swr/src/key.ts | 35 +++ .../react-swr/src/procedure-utils.test-d.ts | 221 ++++++++++++++ .../react-swr/src/procedure-utils.test.ts | 271 ++++++++++++++++++ packages/react-swr/src/procedure-utils.ts | 215 ++++++++++++++ packages/react-swr/src/router-utils.test-d.ts | 64 +++++ packages/react-swr/src/router-utils.test.ts | 97 +++++++ packages/react-swr/src/router-utils.ts | 69 +++++ packages/react-swr/src/shared-utils.test-d.ts | 36 +++ packages/react-swr/src/shared-utils.test.ts | 33 +++ packages/react-swr/src/shared-utils.ts | 38 +++ packages/react-swr/src/types.ts | 67 +++++ packages/react-swr/src/utils.test.ts | 43 +++ packages/react-swr/src/utils.ts | 13 + packages/react-swr/tests/__shared__/orpc.ts | 42 +++ packages/react-swr/tests/e2e.test-d.ts | 82 ++++++ packages/react-swr/tests/e2e.test.tsx | 118 ++++++++ packages/react-swr/tsconfig.json | 16 ++ packages/server/README.md | 1 + packages/shared/README.md | 1 + packages/tanstack-query/README.md | 1 + packages/trpc/README.md | 1 + packages/valibot/README.md | 1 + packages/zod/README.md | 1 + pnpm-lock.yaml | 27 ++ vitest.config.ts | 1 + 51 files changed, 2004 insertions(+) create mode 100644 apps/content/docs/integrations/react-swr.md create mode 100644 packages/react-swr/.gitignore create mode 100644 packages/react-swr/README.md create mode 100644 packages/react-swr/package.json create mode 100644 packages/react-swr/src/index.test.ts create mode 100644 packages/react-swr/src/index.ts create mode 100644 packages/react-swr/src/key.test.ts create mode 100644 packages/react-swr/src/key.ts create mode 100644 packages/react-swr/src/procedure-utils.test-d.ts create mode 100644 packages/react-swr/src/procedure-utils.test.ts create mode 100644 packages/react-swr/src/procedure-utils.ts create mode 100644 packages/react-swr/src/router-utils.test-d.ts create mode 100644 packages/react-swr/src/router-utils.test.ts create mode 100644 packages/react-swr/src/router-utils.ts create mode 100644 packages/react-swr/src/shared-utils.test-d.ts create mode 100644 packages/react-swr/src/shared-utils.test.ts create mode 100644 packages/react-swr/src/shared-utils.ts create mode 100644 packages/react-swr/src/types.ts create mode 100644 packages/react-swr/src/utils.test.ts create mode 100644 packages/react-swr/src/utils.ts create mode 100644 packages/react-swr/tests/__shared__/orpc.ts create mode 100644 packages/react-swr/tests/e2e.test-d.ts create mode 100644 packages/react-swr/tests/e2e.test.tsx create mode 100644 packages/react-swr/tsconfig.json diff --git a/README.md b/README.md index dc7a2c230..1747fe54d 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). +- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/apps/content/.vitepress/config.ts b/apps/content/.vitepress/config.ts index 5e8c4e69d..d35719c7c 100644 --- a/apps/content/.vitepress/config.ts +++ b/apps/content/.vitepress/config.ts @@ -196,6 +196,7 @@ export default withMermaid(defineConfig({ { text: 'OpenTelemetry', link: '/docs/integrations/opentelemetry' }, { text: 'Pinia Colada', link: '/docs/integrations/pinia-colada' }, { text: 'Pino', link: '/docs/integrations/pino' }, + { text: 'React SWR', link: '/docs/integrations/react-swr' }, { text: 'Standard Schema', link: '/docs/integrations/standard-schema' }, { text: 'Tanstack Query', link: '/docs/integrations/tanstack-query' }, { text: 'tRPC', link: '/docs/integrations/trpc' }, diff --git a/apps/content/docs/integrations/react-swr.md b/apps/content/docs/integrations/react-swr.md new file mode 100644 index 000000000..dda1bfe93 --- /dev/null +++ b/apps/content/docs/integrations/react-swr.md @@ -0,0 +1,181 @@ +# React SWR Integration + +[SWR](https://swr.vercel.app/) is a React Hooks library for data fetching that provides features like caching, revalidation, and more. oRPC SWR integration is very lightweight and straightforward. There is no extra overhead. + +::: warning +This guide assumes you are already familiar with [SWR](https://swr.vercel.app/). If you need a refresher, review the official SWR documentation before continuing. +::: + +## Installation + +::: code-group + +```sh [npm] +npm install @orpc/react-swr@beta +``` + +```sh [yarn] +yarn add @orpc/react-swr@beta +``` + +```sh [pnpm] +pnpm add @orpc/react-swr@beta +``` + +```sh [bun] +bun add @orpc/react-swr@beta +``` + +```sh [deno] +deno add npm:@orpc/react-swr@beta +``` + +::: + +## Setup + +Before you begin, set up either a [server-side client](/docs/client/server-side) or a [client-side client](/docs/client/client-side). + +```ts +import { createSWRUtils } from '@orpc/react-swr' + +export const orpc = createSWRUtils(client) + +orpc.planet.find.key({ input: { id: 123 } }) +``` + +::: details Avoiding Key Conflicts? + +You can avoid key conflicts by passing a unique prefix when creating your utils: + +```ts +const userORPC = createSWRUtils(userClient, { + prefix: 'user' +}) + +const postORPC = createSWRUtils(postClient, { + prefix: 'post' +}) +``` + +::: + +## Data Fetching + +Use `.key` and `.fetcher` methods to configure `useSWR` for data fetching: + +```ts +import useSWR from 'swr' + +const { data, error, isLoading } = useSWR( + orpc.planet.find.key({ input: { id: 123 } }), + orpc.planet.find.fetcher({ context: { cache: true } }), // Provide client context if needed +) +``` + +## Infinite Queries + +Use `.key` and `.fetcher` methods to configure `useSWRInfinite` for infinite queries: + +```ts +import useSWRInfinite from 'swr/infinite' + +const { data, error, isLoading, size, setSize } = useSWRInfinite( + (index, previousPageData) => { + if (previousPageData && !previousPageData.nextCursor) { + return null // reached the end + } + + return orpc.planet.list.key({ input: { cursor: previousPageData?.nextCursor } }) + }, + orpc.planet.list.fetcher({ context: { cache: true } }), // Provide client context if needed +) +``` + +## Subscriptions + +Use `.key` and `.subscriber` methods to configure `useSWRSubscription` to subscribe to an [AsyncIteratorObject](/docs/async-iterator-object): + +```ts +import useSWRSubscription from 'swr/subscription' + +const { data, error } = useSWRSubscription( + orpc.streamed.key({ input: { id: 3 } }), + orpc.streamed.subscriber({ context: { cache: true }, maxChunks: 10 }), // Provide client context if needed +) +``` + +Use `.liveSubscriber` to subscribe to the latest events without chunking: + +```ts +import useSWRSubscription from 'swr/subscription' + +const { data, error } = useSWRSubscription( + orpc.streamed.key({ input: { id: 3 } }), + orpc.streamed.liveSubscriber({ context: { cache: true } }), // Provide client context if needed +) +``` + +## Mutations + +Use `.key` and `.mutator` methods to configure `useSWRMutation` for mutations with automatic revalidation on success: + +```ts +import useSWRMutation from 'swr/mutation' + +const { trigger, isMutating } = useSWRMutation( + orpc.planet.list.key(), + orpc.planet.create.mutator({ context: { cache: true } }), // Provide client context if needed +) + +trigger({ name: 'New Planet' }) // auto revalidate orpc.planet.list.key() on success +``` + +## Manual Revalidation + +Use `.matcher` to invalidate data manually: + +```ts +import { mutate } from 'swr' + +mutate(orpc.matcher()) // invalidate all orpc data +mutate(orpc.planet.matcher()) // invalidate all planet data +mutate(orpc.planet.find.matcher({ input: { id: 123 }, strategy: 'exact' })) // invalidate specific planet data +``` + +## Calling Clients + +Use `.call` to call a procedure client directly. It's an alias for corresponding procedure client. + +```ts +const planet = await orpc.planet.find.call({ id: 123 }) +``` + +## Operation Context + +When clients are invoked through the SWR integration, an **operation context** is automatically added to the [client context](/docs/client/rpc-link#using-client-context). This context can be used to configure the request behavior, like setting the HTTP method. + +```ts +import { + SWR_OPERATION_CONTEXT_SYMBOL, + SWROperationContext, +} from '@orpc/react-swr' + +interface ClientContext extends SWROperationContext { +} + +const GET_OPERATION_TYPE = new Set(['fetcher', 'subscriber', 'liveSubscriber']) + +const link = new RPCLink({ + url: 'http://localhost:3000/rpc', + method: ({ context }, path) => { + const operationType = context[SWR_OPERATION_CONTEXT_SYMBOL]?.type + + if (operationType && GET_OPERATION_TYPE.has(operationType)) { + return 'GET' + } + + return 'POST' + }, +}) +``` diff --git a/packages/ai-sdk/README.md b/packages/ai-sdk/README.md index 459e7de60..832c41be1 100644 --- a/packages/ai-sdk/README.md +++ b/packages/ai-sdk/README.md @@ -52,6 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). +- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/arktype/README.md b/packages/arktype/README.md index b45ff9e9f..f369500b7 100644 --- a/packages/arktype/README.md +++ b/packages/arktype/README.md @@ -52,6 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). +- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/bun/README.md b/packages/bun/README.md index 96ff840a7..f8499d60c 100644 --- a/packages/bun/README.md +++ b/packages/bun/README.md @@ -49,6 +49,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). +- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/client/README.md b/packages/client/README.md index dc7a2c230..1747fe54d 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -52,6 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). +- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/cloudflare/README.md b/packages/cloudflare/README.md index b89ee75df..2fc40a407 100644 --- a/packages/cloudflare/README.md +++ b/packages/cloudflare/README.md @@ -52,6 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). +- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/contract/README.md b/packages/contract/README.md index eba7d1cc3..c3bc94345 100644 --- a/packages/contract/README.md +++ b/packages/contract/README.md @@ -52,6 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). +- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/effect/README.md b/packages/effect/README.md index 122799365..6e7736c5f 100644 --- a/packages/effect/README.md +++ b/packages/effect/README.md @@ -52,6 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). +- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/evlog/README.md b/packages/evlog/README.md index ec42d1a10..ec350c98f 100644 --- a/packages/evlog/README.md +++ b/packages/evlog/README.md @@ -52,6 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). +- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/json-schema/README.md b/packages/json-schema/README.md index 405f75889..1cbc19696 100644 --- a/packages/json-schema/README.md +++ b/packages/json-schema/README.md @@ -52,6 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). +- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/nest/README.md b/packages/nest/README.md index 3fba87e46..b30b992cf 100644 --- a/packages/nest/README.md +++ b/packages/nest/README.md @@ -52,6 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). +- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/next/README.md b/packages/next/README.md index 0e0c4b7e8..a356e0d29 100644 --- a/packages/next/README.md +++ b/packages/next/README.md @@ -52,6 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). +- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/openapi/README.md b/packages/openapi/README.md index a6310065d..1bc46bad5 100644 --- a/packages/openapi/README.md +++ b/packages/openapi/README.md @@ -52,6 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). +- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/opentelemetry/README.md b/packages/opentelemetry/README.md index 70f261235..01c1b8eba 100644 --- a/packages/opentelemetry/README.md +++ b/packages/opentelemetry/README.md @@ -52,6 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). +- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/pinia-colada/README.md b/packages/pinia-colada/README.md index 93b577342..51c0233b5 100644 --- a/packages/pinia-colada/README.md +++ b/packages/pinia-colada/README.md @@ -52,6 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). +- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/pino/README.md b/packages/pino/README.md index fc189d4d7..2308d925f 100644 --- a/packages/pino/README.md +++ b/packages/pino/README.md @@ -52,6 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). +- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/publisher/README.md b/packages/publisher/README.md index 55c916cd8..95968def9 100644 --- a/packages/publisher/README.md +++ b/packages/publisher/README.md @@ -52,6 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). +- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/ratelimit/README.md b/packages/ratelimit/README.md index 012d54d29..0ad607b87 100644 --- a/packages/ratelimit/README.md +++ b/packages/ratelimit/README.md @@ -52,6 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). +- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/react-swr/.gitignore b/packages/react-swr/.gitignore new file mode 100644 index 000000000..f3620b55e --- /dev/null +++ b/packages/react-swr/.gitignore @@ -0,0 +1,26 @@ +# Hidden folders and files +.* +!.gitignore +!.*.example + +# Common generated folders +logs/ +node_modules/ +out/ +dist/ +dist-ssr/ +build/ +coverage/ +temp/ + +# Common generated files +*.log +*.log.* +*.tsbuildinfo +*.vitest-temp.json +vite.config.ts.timestamp-* +vitest.config.ts.timestamp-* + +# Common manual ignore files +*.local +*.pem \ No newline at end of file diff --git a/packages/react-swr/README.md b/packages/react-swr/README.md new file mode 100644 index 000000000..f4de0ad28 --- /dev/null +++ b/packages/react-swr/README.md @@ -0,0 +1,199 @@ +

oRPC - Typesafe APIs Made Simple 🪄

+ +
+ + codecov + + + weekly downloads + + + CodSpeed + + + MIT License + + + Discord + + + Ask DeepWiki + +
+ +## Documentation + +You can read the documentation [here](https://orpc.dev). + +## Packages + +**Core** + +- [@orpc/contract](https://www.npmjs.com/package/@orpc/contract): Define API contract as the single source of truth. +- [@orpc/server](https://www.npmjs.com/package/@orpc/server): Build APIs or implement contracts. +- [@orpc/client](https://www.npmjs.com/package/@orpc/client): Consume APIs with end-to-end type safety. +- [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi): Add OpenAPI compatibility to APIs. + +**Schema validation** + +- [@orpc/zod](https://www.npmjs.com/package/@orpc/zod): Integrate with [Zod](https://zod.dev/). +- [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot): Integrate with [Valibot](https://valibot.dev/). +- [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype): Integrate with [ArkType](https://arktype.io/). + +**Built-in features** + +- [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher): Pub/Sub with memory, Redis, and Upstash adapters. +- [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit): Rate limiting with memory, Redis, and Upstash adapters. +- [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema): Smart coercion for OpenAPI requests. + +**Framework & ecosystem integrations** + +- [@orpc/next](https://www.npmjs.com/package/@orpc/next): Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data). +- [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. +- [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). +- [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). +- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). +- [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). +- [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). +- [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). +- [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare): Adapters for [Cloudflare's RateLimit and Durable Objects](https://developers.cloudflare.com/workers/). +- [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc): Reuse existing [tRPC](https://trpc.io/) routers within oRPC. + +**Observability** + +- [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry): Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. +- [@orpc/pino](https://www.npmjs.com/package/@orpc/pino): Integrate with [Pino](https://getpino.io/) for logging. +- [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog): Integrate with [Evlog](https://evlog.dev/) for logging. + +## Sponsors + +Like what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going here: [GitHub Sponsors](https://github.com/sponsors/dinwwwh). Every bit helps! 🚀 + +### 🏆 Platinum Sponsor + + + + + +
ScreenshotOne.com
ScreenshotOne.com
+ +### 🥈 Silver Sponsor + + + + + +
村上さん
村上さん
+ +### Generous 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
Ryan Vogel
Ryan Vogel
christ12938
christ12938
Ryan Soderberg
Ryan Soderberg
shota
shota
+ +### Backers + + + + + + + + + + + + + + + + + + + + + + + + + +
David Walsh
David Walsh
Nicholas
Nicholas
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
Alex
Alex
Andrey Gubanov
Andrey Gubanov
+ +### Past Sponsors + +

+ Maxie + Stijn Timmer + あわわわとーにゅ + Zuplo + motopods + Francisco Hermida + Théo LUDWIG + Abhay Ramesh + shr.ink oü + 0x4e32 + Ryuz + happyboy + yicchi + Saksham + Roman Hrynevych + rokitg + Omar Khatib + Yu-Sabo + Bapusaheb Patil + grim + Nelson Lai + Lê Cao Nguyên + Robert Soriano + Andrew Peters + SKostyukovich + Peter Adam + Fabworks + Novak Antonijevic + Laduni Estu Syalwa + Chen, Zhi-Yuan + Illarion Koperski + Anees Iqbal + Sefa Eyeoglu + natt + Adam Tkaczyk + plancraft +

+ +## 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/react-swr/package.json b/packages/react-swr/package.json new file mode 100644 index 000000000..0e8a0d744 --- /dev/null +++ b/packages/react-swr/package.json @@ -0,0 +1,50 @@ +{ + "name": "@orpc/react-swr", + "type": "module", + "version": "2.0.0-beta.19", + "license": "MIT", + "homepage": "https://orpc.dev", + "repository": { + "type": "git", + "url": "git+https://github.com/middleapi/orpc.git", + "directory": "packages/react-swr" + }, + "keywords": [ + "orpc", + "react", + "swr" + ], + "sideEffects": false, + "publishConfig": { + "exports": { + "./package.json": "./package.json", + ".": { + "types": "./dist/index.d.mts", + "import": "./dist/index.mjs", + "default": "./dist/index.mjs" + } + } + }, + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "unbuild", + "type:check": "tsc -b" + }, + "peerDependencies": { + "swr": ">=2.3.6" + }, + "dependencies": { + "@orpc/client": "workspace:*", + "@orpc/shared": "workspace:*" + }, + "devDependencies": { + "swr": "^2.4.1", + "zod": "^4.4.3" + } +} diff --git a/packages/react-swr/src/index.test.ts b/packages/react-swr/src/index.test.ts new file mode 100644 index 000000000..5bc06787c --- /dev/null +++ b/packages/react-swr/src/index.test.ts @@ -0,0 +1,9 @@ +it('exports createSWRUtils, SWR_OPERATION_CONTEXT_SYMBOL', async () => { + await expect(import('./index')).resolves.toMatchObject({ + createSWRUtils: expect.any(Function), + createRouterUtils: expect.any(Function), + generateOperationKey: expect.any(Function), + isSubsetOf: expect.any(Function), + SWR_OPERATION_CONTEXT_SYMBOL: expect.any(Symbol), + }) +}) diff --git a/packages/react-swr/src/index.ts b/packages/react-swr/src/index.ts new file mode 100644 index 000000000..5f752068d --- /dev/null +++ b/packages/react-swr/src/index.ts @@ -0,0 +1,13 @@ +export * from './key' +export * from './procedure-utils' +export * from './router-utils' +export { + createRouterUtils as createSWRUtils, +} from './router-utils' +export * from './shared-utils' +export * from './types' +export { + OPERATION_CONTEXT_SYMBOL as SWR_OPERATION_CONTEXT_SYMBOL, + type OperationContext as SWROperationContext, +} from './types' +export * from './utils' diff --git a/packages/react-swr/src/key.test.ts b/packages/react-swr/src/key.test.ts new file mode 100644 index 000000000..8a5aa6804 --- /dev/null +++ b/packages/react-swr/src/key.test.ts @@ -0,0 +1,13 @@ +import { generateOperationKey } from './key' + +describe('generateOperationKey', () => { + it('without prefix', () => { + expect(generateOperationKey(['ping'])).toEqual([['ping'], {}]) + expect(generateOperationKey(['ping'], { input: { search: '__search__' } })).toEqual([['ping'], { input: { search: '__search__' } }]) + }) + + it('with prefix', () => { + expect(generateOperationKey(['ping'], { prefix: '__prefix__' })).toEqual(['__prefix__', ['ping'], {}]) + expect(generateOperationKey(['ping'], { prefix: '__prefix__', input: { search: '__search__' } })).toEqual(['__prefix__', ['ping'], { input: { search: '__search__' } }]) + }) +}) diff --git a/packages/react-swr/src/key.ts b/packages/react-swr/src/key.ts new file mode 100644 index 000000000..7626e5306 --- /dev/null +++ b/packages/react-swr/src/key.ts @@ -0,0 +1,35 @@ +import type { PartialDeep } from '@orpc/shared' + +export interface OperationKeyPrefixOptions { + /** + * Prepended as the first element of the key when present. + * Use this to avoid key conflicts when multiple router utils share the same client. + */ + prefix?: string +} + +export type OperationKeyOptions = OperationKeyPrefixOptions & { + input?: PartialDeep +} + +export type OperationKey + = | [path: string[], options: Omit, 'prefix'>] + | [prefix: string, path: string[], options: Omit, 'prefix'>] + +/** + * Generate a **full matching** key for SWR operations. + * + * @see {@link https://orpc.dev/docs/integrations/react-swr#data-fetching React SWR Key Docs} + */ +export function generateOperationKey( + path: string[], + options: OperationKeyOptions = {}, +): OperationKey { + return [ + ...options.prefix !== undefined ? [options.prefix] : [], + path, + { + ...options.input !== undefined ? { input: options.input } : {}, + }, + ] as OperationKey +} diff --git a/packages/react-swr/src/procedure-utils.test-d.ts b/packages/react-swr/src/procedure-utils.test-d.ts new file mode 100644 index 000000000..1a77ba19b --- /dev/null +++ b/packages/react-swr/src/procedure-utils.test-d.ts @@ -0,0 +1,221 @@ +import type { Client, ORPCError } from '@orpc/client' +import type { Public } from '@orpc/shared' +import type { OperationKey } from './key' +import type { ProcedureUtils } from './procedure-utils' +import useSWR from 'swr' +import useSWRInfinite from 'swr/infinite' +import useSWRMutation from 'swr/mutation' +import useSWRSubscription from 'swr/subscription' + +describe('procedureUtils', () => { + type UtilsInput = { search?: string, cursor?: number } | undefined + type UtilsOutput = { title: string }[] + type UtilsError = ORPCError<'BASE', { output: string }> | Error + const condition = {} as boolean + + const optionalUtils = {} as Public> + + const requiredUtils = {} as Public> + + const streamUtils = {} as Public, + UtilsError + >> + + it('.call', () => { + expectTypeOf(optionalUtils.call).toEqualTypeOf< + Client<{ batch?: boolean }, UtilsInput, UtilsOutput, UtilsError> + >() + }) + + describe('.key', () => { + it('should handle optional `input` correctly', () => { + optionalUtils.key() + optionalUtils.key({ }) + optionalUtils.key({ input: { search: 'search' } }) + }) + + it('should handle required `input` correctly', () => { + // @ts-expect-error - `input` is required + requiredUtils.key() + }) + + it('should infer types for `input` correctly', () => { + optionalUtils.key({ input: { cursor: 1 } }) + // @ts-expect-error - Should error on invalid input type + optionalUtils.key({ input: { cursor: 'invalid' } }) + // @ts-expect-error - Should error on non-existent input property + optionalUtils.key({ input: { cursor: 1, nonExistent: true } }) + + requiredUtils.key({ input: 'input' }) + // @ts-expect-error - Should error on invalid input type + requiredUtils.key({ input: 123 }) + }) + + it('return valid key', () => { + expectTypeOf(optionalUtils.key()).toEqualTypeOf>() + expectTypeOf(optionalUtils.key({ input: { search: 'search' } })).toEqualTypeOf>() + }) + }) + + describe('.fetcher & useSWR & useSWRInfinite', () => { + it('require matching key', () => { + useSWR(optionalUtils.key(), optionalUtils.fetcher()) + // FIXME: This should be an error, but SWR does not enforce key type + useSWR('invalid', optionalUtils.fetcher()) + + useSWRInfinite((index, previousPage) => { + // FIXME: This should be typed + // expectTypeOf(previousPage).toEqualTypeOf() + + return optionalUtils.key() + }, optionalUtils.fetcher()) + // FIXME: This should be an error, but SWR does not enforce key type + useSWRInfinite(() => 'invalid', optionalUtils.fetcher()) + }) + + it('allow use `null` as key', () => { + useSWR(condition ? null : optionalUtils.key(), optionalUtils.fetcher()) + useSWRInfinite(() => condition ? null : optionalUtils.key(), optionalUtils.fetcher()) + }) + + it('should infer types for `context` correctly', () => { + optionalUtils.fetcher() + optionalUtils.fetcher({ context: { batch: true } }) + // @ts-expect-error - Should error on non-existent context property + optionalUtils.fetcher({ context: { batch: true, nonExistent: true } }) + // @ts-expect-error - Should error on invalid context type + optionalUtils.fetcher({ context: { batch: 'invalid' } }) + + // @ts-expect-error - should require provided context if contains non-optional fields + requiredUtils.fetcher() + requiredUtils.fetcher({ context: { batch: true } }) + // @ts-expect-error - Should error on non-existent context property + requiredUtils.fetcher({ context: { batch: true, nonExistent: true } }) + // @ts-expect-error - Should error on invalid context type + requiredUtils.fetcher({ context: { batch: 'invalid' } }) + }) + + it('should infer types for `output` and `error` correctly', () => { + const swr = useSWR(optionalUtils.key(), optionalUtils.fetcher()) + expectTypeOf(swr.data).toEqualTypeOf() + // FIXME: this should be typed + // expectTypeOf(swr.error).toEqualTypeOf() + + const infinite = useSWRInfinite((index, previousPage) => optionalUtils.key(), optionalUtils.fetcher()) + expectTypeOf(infinite.data).toEqualTypeOf() + // FIXME: this should be typed + // expectTypeOf(infinite.error).toEqualTypeOf() + }) + }) + + describe('.subscriber & useSWRSubscription', () => { + it('require matching key', () => { + useSWRSubscription(optionalUtils.key(), optionalUtils.subscriber()) + // @ts-expect-error - Should error on invalid key type + useSWRSubscription('invalid', optionalUtils.subscriber()) + }) + + it('allow use `null` as key', () => { + useSWRSubscription(condition ? null : optionalUtils.key(), optionalUtils.subscriber()) + }) + + it('should infer types for `context` correctly', () => { + optionalUtils.subscriber() + optionalUtils.subscriber({ context: { batch: true } }) + // @ts-expect-error - Should error on non-existent context property + optionalUtils.subscriber({ context: { batch: true, nonExistent: true } }) + // @ts-expect-error - Should error on invalid context type + optionalUtils.subscriber({ context: { batch: 'invalid' } }) + + // @ts-expect-error - should require provided context if contains non-optional fields + requiredUtils.subscriber() + requiredUtils.subscriber({ context: { batch: true } }) + // @ts-expect-error - Should error on non-existent context property + requiredUtils.subscriber({ context: { batch: true, nonExistent: true } }) + // @ts-expect-error - Should error on invalid context type + requiredUtils.subscriber({ context: { batch: 'invalid' } }) + }) + + it('should infer types for `output` and `error` correctly', () => { + const subscription = useSWRSubscription(streamUtils.key(), streamUtils.subscriber({ maxChunks: 5 })) + expectTypeOf(subscription.data).toEqualTypeOf() + expectTypeOf(subscription.error).toEqualTypeOf() + }) + }) + + describe('.liveSubscriber & useSWRSubscription', () => { + it('require matching key', () => { + useSWRSubscription(optionalUtils.key(), optionalUtils.liveSubscriber()) + // @ts-expect-error - Should error on invalid key type + useSWRSubscription('invalid', optionalUtils.liveSubscriber()) + }) + + it('allow use `null` as key', () => { + useSWRSubscription(condition ? null : optionalUtils.key(), optionalUtils.liveSubscriber()) + }) + + it('should infer types for `context` correctly', () => { + optionalUtils.liveSubscriber() + optionalUtils.liveSubscriber({ context: { batch: true } }) + // @ts-expect-error - Should error on non-existent context property + optionalUtils.liveSubscriber({ context: { batch: true, nonExistent: true } }) + // @ts-expect-error - Should error on invalid context type + optionalUtils.liveSubscriber({ context: { batch: 'invalid' } }) + + // @ts-expect-error - should require provided context if contains non-optional fields + requiredUtils.liveSubscriber() + requiredUtils.liveSubscriber({ context: { batch: true } }) + // @ts-expect-error - Should error on non-existent context property + requiredUtils.liveSubscriber({ context: { batch: true, nonExistent: true } }) + // @ts-expect-error - Should error on invalid context type + requiredUtils.liveSubscriber({ context: { batch: 'invalid' } }) + }) + + it('should infer types for `output` and `error` correctly', () => { + const subscription = useSWRSubscription(streamUtils.key(), streamUtils.liveSubscriber()) + expectTypeOf(subscription.data).toEqualTypeOf() + expectTypeOf(subscription.error).toEqualTypeOf() + }) + }) + + describe('.mutator & useSWRMutation', () => { + it('should infer types for `context` correctly', () => { + optionalUtils.mutator() + optionalUtils.mutator({ context: { batch: true } }) + // @ts-expect-error - Should error on non-existent context property + optionalUtils.mutator({ context: { batch: true, nonExistent: true } }) + // @ts-expect-error - Should error on invalid context type + optionalUtils.mutator({ context: { batch: 'invalid' } }) + + // @ts-expect-error - should require provided context if contains non-optional fields + requiredUtils.mutator() + requiredUtils.mutator({ context: { batch: true } }) + // @ts-expect-error - Should error on non-existent context property + requiredUtils.mutator({ context: { batch: true, nonExistent: true } }) + // @ts-expect-error - Should error on invalid context type + requiredUtils.mutator({ context: { batch: 'invalid' } }) + }) + + it('should infer types for `output` & `input` & `error` correctly', () => { + const mutation = useSWRMutation('some-key', optionalUtils.mutator()) + + expectTypeOf[0]>().toEqualTypeOf() + expectTypeOf(mutation.data).toEqualTypeOf() + // FIXME: this should be typed + // expectTypeOf(mutation.error).toEqualTypeOf() + }) + }) +}) diff --git a/packages/react-swr/src/procedure-utils.test.ts b/packages/react-swr/src/procedure-utils.test.ts new file mode 100644 index 000000000..99b6d45db --- /dev/null +++ b/packages/react-swr/src/procedure-utils.test.ts @@ -0,0 +1,271 @@ +import type { OperationKey } from './key' +import { ProcedureUtils } from './procedure-utils' +import { OPERATION_CONTEXT_SYMBOL } from './types' + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('procedureUtils', () => { + const client = vi.fn() + const utils = new ProcedureUtils(['ping'], client, {}) + + const key: OperationKey = [['ping'], { input: { search: '__search__' } }] + + it('.call', () => { + expect(utils.call).toBe(client) + }) + + it('.key', () => { + expect(utils.key()).toEqual([['ping'], {}]) + expect(utils.key({ input: { search: '__search__' } })).toEqual([['ping'], { input: { search: '__search__' } }]) + }) + + it('.key with prefix', () => { + const prefixedUtils = new ProcedureUtils(['ping'], client, { prefix: '__prefix__' }) + + expect(prefixedUtils.key()).toEqual(['__prefix__', ['ping'], {}]) + expect(prefixedUtils.key({ input: { search: '__search__' } })).toEqual(['__prefix__', ['ping'], { input: { search: '__search__' } }]) + }) + + it('.fetcher', async () => { + client.mockResolvedValueOnce('__output__') + const fetcher = utils.fetcher({ context: { batch: true } }) + + await expect(fetcher(key)).resolves.toEqual('__output__') + expect(client).toHaveBeenCalledTimes(1) + expect(client).toHaveBeenCalledWith({ search: '__search__' }, { context: { batch: true, [OPERATION_CONTEXT_SYMBOL]: { key, type: 'fetcher' } } }) + }) + + it('.fetcher with prefixed key', async () => { + client.mockResolvedValueOnce('__output__') + const fetcher = utils.fetcher({ context: { batch: true } }) + + const prefixedKey: OperationKey = ['__prefix__', ['ping'], { input: { search: '__search__' } }] + + await expect(fetcher(prefixedKey)).resolves.toEqual('__output__') + expect(client).toHaveBeenCalledTimes(1) + expect(client).toHaveBeenCalledWith({ search: '__search__' }, { context: { batch: true, [OPERATION_CONTEXT_SYMBOL]: { key: prefixedKey, type: 'fetcher' } } }) + }) + + describe('.subscriber', async () => { + it('on success', async () => { + client.mockImplementationOnce(async function* () { + await new Promise(resolve => setTimeout(resolve, 10)) + yield '__event__1' + yield '__event__2' + yield '__event__3' + }) + const subscriber = utils.subscriber({ context: { batch: true }, maxChunks: 2 }) + + const next = vi.fn() + const unsubscribe = subscriber(key, { next }) + + expect(unsubscribe).toBeInstanceOf(Function) + + await new Promise(resolve => setTimeout(resolve, 20)) + + expect(client).toHaveBeenCalledTimes(1) + expect(client).toHaveBeenCalledWith({ search: '__search__' }, { context: { batch: true, [OPERATION_CONTEXT_SYMBOL]: { key, type: 'subscriber' } }, signal: expect.any(AbortSignal) }) + + expect(next).toHaveBeenCalledTimes(4) + expect(next).toHaveBeenCalledWith(undefined, expect.any(Function)) + + expect(next).toHaveBeenNthCalledWith(1, undefined, undefined) // reset mode + expect(next.mock.calls[1]![1](undefined)).toEqual(['__event__1']) + expect(next.mock.calls[2]![1](['1'])).toEqual(['1', '__event__2']) + // exceeds maxChunks, so it should only return the last 2 events + expect(next.mock.calls[3]![1](['1', '2'])).toEqual(['2', '__event__3']) + }) + + it('on success refetchMode=append', async () => { + client.mockImplementationOnce(async function* () { + await new Promise(resolve => setTimeout(resolve, 10)) + yield '__event__1' + yield '__event__2' + yield '__event__3' + }) + const subscriber = utils.subscriber({ context: { batch: true }, maxChunks: 2, refetchMode: 'append' }) + + const next = vi.fn() + const unsubscribe = subscriber(key, { next }) + + expect(unsubscribe).toBeInstanceOf(Function) + + await new Promise(resolve => setTimeout(resolve, 20)) + + expect(client).toHaveBeenCalledTimes(1) + expect(client).toHaveBeenCalledWith({ search: '__search__' }, { context: { batch: true, [OPERATION_CONTEXT_SYMBOL]: { key, type: 'subscriber' } }, signal: expect.any(AbortSignal) }) + + expect(next).toHaveBeenCalledTimes(3) + expect(next).toHaveBeenCalledWith(undefined, expect.any(Function)) + + expect(next.mock.calls[0]![1](undefined)).toEqual(['__event__1']) + expect(next.mock.calls[1]![1](['1'])).toEqual(['1', '__event__2']) + // exceeds maxChunks, so it should only return the last 2 events + expect(next.mock.calls[2]![1](['1', '2'])).toEqual(['2', '__event__3']) + }) + + it('on unsubscribe', async () => { + client.mockImplementationOnce(async function* () { + await new Promise(resolve => setTimeout(resolve, 100)) + yield '__event__1' + yield '__event__2' + yield '__event__3' + }) + const subscriber = utils.subscriber({ context: { batch: true }, maxChunks: 2 }) + + const next = vi.fn() + const unsubscribe = subscriber(key, { next }) + await new Promise(resolve => setTimeout(resolve, 10)) + unsubscribe() + + expect(client).toHaveBeenCalledTimes(1) + expect(client).toHaveBeenCalledWith({ search: '__search__' }, { context: { batch: true, [OPERATION_CONTEXT_SYMBOL]: { key, type: 'subscriber' } }, signal: expect.any(AbortSignal) }) + expect(client.mock.calls[0]![1].signal.aborted).toBe(true) + }) + + it('on error while yielding', async () => { + client.mockImplementationOnce(async function* () { + await new Promise(resolve => setTimeout(resolve, 10)) + throw new Error('__error__') + }) + const subscriber = utils.subscriber({ context: { batch: true }, maxChunks: 2 }) + + const next = vi.fn() + const unsubscribe = subscriber(key, { next }) + + expect(unsubscribe).toBeInstanceOf(Function) + + await new Promise(resolve => setTimeout(resolve, 20)) + + expect(next).toHaveBeenCalledTimes(2) + expect(next).toHaveBeenCalledWith(new Error('__error__')) + }) + + it('on error after unsubscribe', async () => { + client.mockImplementationOnce(async function* () { + await new Promise(resolve => setTimeout(resolve, 10)) + throw new Error('__error__') + }) + const subscriber = utils.subscriber({ context: { batch: true }, maxChunks: 2 }) + + const next = vi.fn() + const unsubscribe = subscriber(key, { next }) + unsubscribe() + await new Promise(resolve => setTimeout(resolve, 20)) + expect(next).toHaveBeenCalledTimes(1) + }) + + it('on non-AsyncIteratorObject output', async () => { + client.mockResolvedValueOnce('__output__') + const subscriber = utils.subscriber({ context: { batch: true } }) + + const next = vi.fn() + subscriber(key, { next }) + + await new Promise(resolve => setTimeout(resolve, 10)) + + expect(next).toHaveBeenCalledTimes(1) + expect(next).toHaveBeenCalledWith(new TypeError('.subscriber requires an AsyncIteratorObject output')) + }) + }) + + describe('.liveSubscriber', async () => { + it('on success', async () => { + client.mockImplementationOnce(async function* () { + await new Promise(resolve => setTimeout(resolve, 10)) + yield '__event__1' + yield '__event__2' + yield '__event__3' + }) + const subscriber = utils.liveSubscriber({ context: { batch: true } }) + + const next = vi.fn() + const unsubscribe = subscriber(key, { next }) + + expect(unsubscribe).toBeInstanceOf(Function) + + await new Promise(resolve => setTimeout(resolve, 20)) + + expect(client).toHaveBeenCalledTimes(1) + expect(client).toHaveBeenCalledWith({ search: '__search__' }, { context: { batch: true, [OPERATION_CONTEXT_SYMBOL]: { key, type: 'liveSubscriber' } }, signal: expect.any(AbortSignal) }) + + expect(next).toHaveBeenCalledTimes(3) + expect(next).toHaveBeenNthCalledWith(1, undefined, '__event__1') + expect(next).toHaveBeenNthCalledWith(2, undefined, '__event__2') + expect(next).toHaveBeenNthCalledWith(3, undefined, '__event__3') + }) + + it('on unsubscribe', async () => { + client.mockImplementationOnce(async function* () { + await new Promise(resolve => setTimeout(resolve, 10)) + yield '__event__1' + yield '__event__2' + yield '__event__3' + }) + const subscriber = utils.liveSubscriber({ context: { batch: true } }) + + const next = vi.fn() + const unsubscribe = subscriber(key, { next }) + await new Promise(resolve => setTimeout(resolve, 20)) + unsubscribe() + + expect(client).toHaveBeenCalledTimes(1) + expect(client).toHaveBeenCalledWith({ search: '__search__' }, { context: { batch: true, [OPERATION_CONTEXT_SYMBOL]: { key, type: 'liveSubscriber' } }, signal: expect.any(AbortSignal) }) + expect(client.mock.calls[0]![1].signal.aborted).toBe(true) + }) + + it('on error while yielding', async () => { + client.mockImplementationOnce(async function* () { + await new Promise(resolve => setTimeout(resolve, 10)) + throw new Error('__error__') + }) + const subscriber = utils.liveSubscriber({ context: { batch: true } }) + + const next = vi.fn() + const unsubscribe = subscriber(key, { next }) + + expect(unsubscribe).toBeInstanceOf(Function) + + await new Promise(resolve => setTimeout(resolve, 20)) + + expect(next).toHaveBeenCalledTimes(1) + expect(next).toHaveBeenCalledWith(new Error('__error__')) + }) + + it('on error after unsubscribe', async () => { + client.mockImplementationOnce(async function* () { + await new Promise(resolve => setTimeout(resolve, 10)) + throw new Error('__error__') + }) + const subscriber = utils.liveSubscriber({ context: { batch: true } }) + + const next = vi.fn() + const unsubscribe = subscriber(key, { next }) + unsubscribe() + await new Promise(resolve => setTimeout(resolve, 20)) + expect(next).toHaveBeenCalledTimes(0) + }) + + it('on non-AsyncIteratorObject output', async () => { + client.mockResolvedValueOnce('__output__') + const subscriber = utils.liveSubscriber({ context: { batch: true } }) + + const next = vi.fn() + subscriber(key, { next }) + await new Promise(resolve => setTimeout(resolve, 10)) + expect(next).toHaveBeenCalledTimes(1) + expect(next).toHaveBeenCalledWith(new TypeError('.liveSubscriber requires an AsyncIteratorObject output')) + }) + }) + + it('.mutator', async () => { + client.mockResolvedValueOnce('__output__') + const mutator = utils.mutator({ context: { batch: true } }) + + await expect(mutator('key', { arg: { search: '__search__' } })).resolves.toEqual('__output__') + expect(client).toHaveBeenCalledTimes(1) + expect(client).toHaveBeenCalledWith({ search: '__search__' }, { context: { batch: true, [OPERATION_CONTEXT_SYMBOL]: { key: 'key', type: 'mutator' } } }) + }) +}) diff --git a/packages/react-swr/src/procedure-utils.ts b/packages/react-swr/src/procedure-utils.ts new file mode 100644 index 000000000..3f733b00d --- /dev/null +++ b/packages/react-swr/src/procedure-utils.ts @@ -0,0 +1,215 @@ +import type { Client, ClientContext } from '@orpc/client' +import type { MaybeOptionalOptions } from '@orpc/shared' +import type { OperationKey, OperationKeyOptions, OperationKeyPrefixOptions } from './key' +import type { + Fetcher, + FetcherOptions, + InferLiveSubscriberOutput, + InferSubscriberOutput, + KeyOptions, + Mutator, + MutatorOptions, + OperationContext, + Subscriber, + SubscriberOptions, +} from './types' +import { isAsyncIteratorObject, resolveMaybeOptionalOptions } from '@orpc/shared' +import { generateOperationKey } from './key' +import { SharedUtils } from './shared-utils' +import { OPERATION_CONTEXT_SYMBOL } from './types' + +export interface ProcedureUtilsOptions extends OperationKeyPrefixOptions { +} + +export class ProcedureUtils extends SharedUtils { + /** + * Calling corresponding procedure client + * + * @see {@link https://orpc.dev/docs/integrations/react-swr#calling-clients React SWR Calling Procedure Client Docs} + */ + call: Client + + constructor( + path: string[], + client: Client, + options: ProcedureUtilsOptions = {}, + ) { + super(path, options) + this.call = client + } + + /** + * Generate a **full matching** key for SWR operations. + * + * @see {@link https://orpc.dev/docs/integrations/react-swr#data-fetching React SWR Key Docs} + */ + key( + ...rest: MaybeOptionalOptions> + ): OperationKey { + const options = resolveMaybeOptionalOptions(rest) + + return generateOperationKey(this.path, { ...options, prefix: this.options.prefix } as OperationKeyOptions) + } + + /** + * Generate a fetcher function for use with useSWR, useSWRInfinite, and other SWR hooks. + * + * @see {@link https://orpc.dev/docs/integrations/react-swr#data-fetching React SWR Data Fetching Docs} + */ + fetcher( + ...rest: MaybeOptionalOptions> + ): Fetcher { + const options = resolveMaybeOptionalOptions(rest) + + return async (key) => { + /** + * Input can be omitted from the key when it is undefined, + * so we can safely cast it to TInput with an undefined fallback. + */ + const { input } = key[key.length - 1] as { input: TInput } + + return this.call(input, { + context: { + [OPERATION_CONTEXT_SYMBOL]: { key, type: 'fetcher' }, + ...options.context, + } satisfies OperationContext as any, + }) + } + } + + /** + * Generate a subscriber function that subscribes to an [AsyncIteratorObject](https://orpc.dev/docs/async-iterator-object) for use with useSWRSubscription, etc. + * + * @see {@link https://orpc.dev/docs/integrations/react-swr#subscriptions React SWR Subscriptions Docs} + */ + subscriber( + ...rest: MaybeOptionalOptions> + ): Subscriber, TError> { + const { maxChunks, refetchMode = 'reset', ...options } = resolveMaybeOptionalOptions(rest) + + return (key, { next }) => { + const controller = new AbortController() + + void (async () => { + try { + /** + * Input can be omitted from the key when it is undefined, + * so we can safely cast it to TInput with an undefined fallback. + */ + const { input } = key[key.length - 1] as { input: TInput } + + const iterator = await this.call(input, { + context: { + [OPERATION_CONTEXT_SYMBOL]: { key, type: 'subscriber' }, + ...options.context, + } satisfies OperationContext as any, + signal: controller.signal, + }) + + if (!isAsyncIteratorObject(iterator)) { + throw new TypeError('.subscriber requires an AsyncIteratorObject output') + } + + if (refetchMode === 'reset') { + next(undefined, undefined) + } + + for await (const event of iterator) { + next(undefined, (old) => { + const newData = Array.isArray(old) ? [...old, event] : [event] + + if (typeof maxChunks === 'number' && newData.length > maxChunks) { + return newData.slice(newData.length - maxChunks) as InferSubscriberOutput + } + + return newData as InferSubscriberOutput + }) + } + } + catch (error) { + /** + * Only report the error if the controller is not aborted. + */ + if (!controller.signal.aborted) { + next(error as TError) + } + } + })() + + return () => { + controller.abort() + } + } + } + + /** + * Generate a live subscriber that subscribes to the latest events from an [AsyncIteratorObject](https://orpc.dev/docs/async-iterator-object) for use with useSWRSubscription, etc. + * + * @see {@link https://orpc.dev/docs/integrations/react-swr#subscriptions React SWR Subscriptions Docs} + */ + liveSubscriber( + ...rest: MaybeOptionalOptions> + ): Subscriber, TError> { + const options = resolveMaybeOptionalOptions(rest) + + return (key, { next }) => { + const controller = new AbortController() + + void (async () => { + try { + /** + * Input can be omitted from the key when it is undefined, + * so we can safely cast it to TInput with an undefined fallback. + */ + const { input } = key[key.length - 1] as { input: TInput } + + const iterator = await this.call(input, { + context: { + [OPERATION_CONTEXT_SYMBOL]: { key, type: 'liveSubscriber' }, + ...options.context, + } satisfies OperationContext as any, + signal: controller.signal, + }) + + if (!isAsyncIteratorObject(iterator)) { + throw new TypeError('.liveSubscriber requires an AsyncIteratorObject output') + } + + for await (const event of iterator) { + next(undefined, event as InferLiveSubscriberOutput) + } + } + catch (error) { + /** + * Only report the error if the controller is not aborted. + */ + if (!controller.signal.aborted) { + next(error as TError) + } + } + })() + + return () => { + controller.abort() + } + } + } + + /** + * Generate a mutator function for use with useSWRMutation, etc. + * + * @see {@link https://orpc.dev/docs/integrations/react-swr#mutations React SWR Mutations Docs} + */ + mutator( + ...rest: MaybeOptionalOptions> + ): Mutator { + const options = resolveMaybeOptionalOptions(rest) + + return (key, { arg }) => this.call(arg, { + context: { + [OPERATION_CONTEXT_SYMBOL]: { key, type: 'mutator' }, + ...options.context, + } satisfies OperationContext as any, + }) + } +} diff --git a/packages/react-swr/src/router-utils.test-d.ts b/packages/react-swr/src/router-utils.test-d.ts new file mode 100644 index 000000000..73e1a0dfc --- /dev/null +++ b/packages/react-swr/src/router-utils.test-d.ts @@ -0,0 +1,64 @@ +import type { ORPCErrorFromErrorMap } from '@orpc/contract' +import type { RouterClient } from '@orpc/server' +import type { Public } from '@orpc/shared' +import type { ProcedureUtils } from './procedure-utils' +import type { RouterUtils } from './router-utils' +import type { SharedUtils } from './shared-utils' +import { os } from '@orpc/server' +import z from 'zod' +import { createRouterUtils } from './router-utils' + +const inputSchema = z.object({ input: z.number().transform(n => `${n}`) }) +const outputSchema = z.object({ output: z.number().transform(n => `${n}`) }) +const unknownSchema = z.unknown() + +const baseErrorMap = { + BASE: { + data: outputSchema, + }, + OVERRIDE: {}, +} + +const ping = os.input(inputSchema).output(outputSchema).errors(baseErrorMap).handler(() => ({ output: 1 })) +const pong = os.input(unknownSchema).output(unknownSchema).handler(() => 'pong') + +const router = { + ping, + pong, + nested: os.router({ + ping, + pong, + }), +} + +it('routerUtils', () => { + const utils = {} as RouterUtils> + + expectTypeOf(utils).toExtend>>() + expectTypeOf(utils.nested).toExtend>>() + + expectTypeOf(utils.ping).toExtend>>() + expectTypeOf(utils.nested.ping).toExtend>>() + + expectTypeOf(utils.ping).toExtend< + Public | Error>> + >() + expectTypeOf(utils.nested.ping).toExtend< + Public | Error>> + >() + + expectTypeOf(utils.pong).toExtend< + Public> + >() + expectTypeOf(utils.nested.pong).toExtend< + Public> + >() +}) + +it('createRouterUtils', () => { + const utils = createRouterUtils({} as RouterClient, { + prefix: '__prefix__', + }) + + expectTypeOf(utils).toEqualTypeOf>>() +}) diff --git a/packages/react-swr/src/router-utils.test.ts b/packages/react-swr/src/router-utils.test.ts new file mode 100644 index 000000000..5a98cd5f4 --- /dev/null +++ b/packages/react-swr/src/router-utils.test.ts @@ -0,0 +1,97 @@ +import { ProcedureUtils } from './procedure-utils' +import { createRouterUtils } from './router-utils' + +vi.mock('./procedure-utils', async (importOriginal) => { + const { SharedUtils } = await import('./shared-utils') + + return { + ...await importOriginal(), + ProcedureUtils: vi.fn(class extends SharedUtils { + call = vi.fn() + override matcher = SharedUtils.prototype.matcher + key = vi.fn(() => ({ key: true })) + fetcher = vi.fn(() => ({ fetcher: true })) + + constructor(path: string[], _client: unknown, options: any = {}) { + super(path, options) + } + }), + } +}) + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('createRouterUtils', () => { + const client = vi.fn() as any + client.key = vi.fn() // "key" mean client can handle when client and method is conflict + client.key.pong = vi.fn() + + it('creates nested procedure & shared utils', () => { + const utils = createRouterUtils(client, { + prefix: '__prefix__', + }) as any + + expect(ProcedureUtils).toHaveBeenCalledTimes(1) + expect(ProcedureUtils).toHaveBeenCalledWith([], client, { prefix: '__prefix__' }) + expect(utils.matcher()(['__prefix__', [], {}])).toBe(true) + expect(utils.matcher()([[], {}])).toBe(false) + expect(utils.fetcher()).toBe(vi.mocked(ProcedureUtils).mock.results[0]?.value.fetcher.mock.results[0]?.value) + + vi.clearAllMocks() + const keyUtils = utils.key + + expect(ProcedureUtils).toHaveBeenCalledTimes(1) + expect(ProcedureUtils).toHaveBeenCalledWith(['key'], client.key, { prefix: '__prefix__' }) + expect(keyUtils.matcher()(['__prefix__', ['key'], {}])).toBe(true) + expect(keyUtils.fetcher()).toBe(vi.mocked(ProcedureUtils).mock.results[0]?.value.fetcher.mock.results[0]?.value) + + vi.clearAllMocks() + const pongUtils = keyUtils.pong + + expect(ProcedureUtils).toHaveBeenCalledTimes(1) + expect(ProcedureUtils).toHaveBeenCalledWith(['key', 'pong'], client.key.pong, { prefix: '__prefix__' }) + expect(pongUtils.matcher()(['__prefix__', ['key', 'pong'], {}])).toBe(true) + expect(pongUtils.fetcher()).toBe(vi.mocked(ProcedureUtils).mock.results[0]?.value.fetcher.mock.results[0]?.value) + }) + + it('works with plain object routers', () => { + const router = { nested: { ping: vi.fn() } } as any + const utils = createRouterUtils(router) as any + + expect(ProcedureUtils).toHaveBeenCalledTimes(0) + expect(utils.fetcher).toBeUndefined() + expect(utils.matcher()([[], {}])).toBe(true) + + const nestedUtils = utils.nested + + expect(ProcedureUtils).toHaveBeenCalledTimes(0) + expect(nestedUtils.matcher()([['nested'], {}])).toBe(true) + + const pingUtils = nestedUtils.ping + + expect(ProcedureUtils).toHaveBeenCalledTimes(1) + expect(ProcedureUtils).toHaveBeenCalledWith(['nested', 'ping'], router.nested.ping, { prefix: undefined }) + expect(pingUtils.fetcher()).toBe(vi.mocked(ProcedureUtils).mock.results[0]?.value.fetcher.mock.results[0]?.value) + }) + + it('stops recursive on symbol', () => { + const utils = createRouterUtils(client) as any + expect(utils[Symbol.for('a')]).toBe(undefined) + }) + + it('stops recursive on RECURSIVE_CLIENT_UNWRAP_KEYS', () => { + client.then = vi.fn() + + try { + const utils = createRouterUtils(client) as any + + expect(utils.then).toBe(undefined) + expect(ProcedureUtils).toHaveBeenCalledTimes(1) // only the root utils + } + finally { + delete client.then + } + }) +}) diff --git a/packages/react-swr/src/router-utils.ts b/packages/react-swr/src/router-utils.ts new file mode 100644 index 000000000..5e88aa0f7 --- /dev/null +++ b/packages/react-swr/src/router-utils.ts @@ -0,0 +1,69 @@ +import type { AnyNestedClient, Client } from '@orpc/client' +import type { Public } from '@orpc/shared' +import type { OperationKeyPrefixOptions } from './key' +import { RECURSIVE_CLIENT_UNWRAP_KEYS } from '@orpc/client' +import { bindMethods, get, getOrBind, isTypescriptObject, toArray } from '@orpc/shared' +import { ProcedureUtils } from './procedure-utils' +import { SharedUtils } from './shared-utils' + +export type RouterUtils + = T extends Client + ? Public> + : { + [K in keyof T]: T[K] extends AnyNestedClient ? RouterUtils : never + } & Public> + +export interface RouterUtilsOptions extends OperationKeyPrefixOptions { + /** + * Base path for all keys. + * + * @internal + */ + path?: string[] | undefined +} + +/** + * Create a swr router utils from a client. + * + * @info Both client-side and server-side clients are supported. + * @see {@link https://orpc.dev/docs/integrations/react-swr React SWR Integration} + */ +export function createRouterUtils( + client: T, + options: RouterUtilsOptions = {}, +): RouterUtils { + const path = toArray(options.path) + + const utils = typeof client === 'function' + ? bindMethods(new ProcedureUtils(path, client as any, { prefix: options.prefix })) + : bindMethods(new SharedUtils(path, { prefix: options.prefix })) + + const recursive = new Proxy(utils, { + get(target, prop) { + const value = getOrBind(target, prop) + const nextClient = get(client, [prop]) + + if (typeof prop !== 'string' || RECURSIVE_CLIENT_UNWRAP_KEYS.has(prop) || !isTypescriptObject(nextClient)) { + return value + } + + const nextUtils = createRouterUtils(nextClient as any, { ...options, path: [...path, prop] }) + + if (typeof value !== 'function') { + return nextUtils + } + + return new Proxy(value, { + get(target, prop) { + if (typeof prop !== 'string' || RECURSIVE_CLIENT_UNWRAP_KEYS.has(prop)) { + return getOrBind(target, prop) + } + + return getOrBind(nextUtils, prop) + }, + }) + }, + }) + + return recursive as any +} diff --git a/packages/react-swr/src/shared-utils.test-d.ts b/packages/react-swr/src/shared-utils.test-d.ts new file mode 100644 index 000000000..8d466ba97 --- /dev/null +++ b/packages/react-swr/src/shared-utils.test-d.ts @@ -0,0 +1,36 @@ +import type { Public } from '@orpc/shared' +import type { SharedUtils } from './shared-utils' +import { mutate } from 'swr' + +describe('sharedUtils', () => { + const optionalUtils = {} as Public> + const requiredUtils = {} as Public> + + describe('.matcher & mutate', () => { + it('support partial input if strategy=partial', () => { + optionalUtils.matcher({ input: { nested: { value1: 'test' } } }) + optionalUtils.matcher({ input: { nested: {} } }) + optionalUtils.matcher({ input: { } }) + + requiredUtils.matcher({ input: { nested: { value1: 'test' } } }) + requiredUtils.matcher({ input: { nested: {} } }) + requiredUtils.matcher({ input: {} }) + }) + + it('require exact input if strategy=exact', () => { + optionalUtils.matcher({ strategy: 'exact' }) + optionalUtils.matcher({ strategy: 'exact', input: { nested: { value1: 'test' } } }) + + requiredUtils.matcher({ strategy: 'exact', input: { nested: { value1: 'test' } } }) + // @ts-expect-error - missing nested field + requiredUtils.matcher({ strategy: 'exact', input: { } }) + // @ts-expect-error - missing input field + requiredUtils.matcher({ strategy: 'exact' }) + }) + + it('can be used for mutating', () => { + mutate(optionalUtils.matcher()) + mutate(requiredUtils.matcher({ strategy: 'exact', input: { nested: { value1: 'test' } } })) + }) + }) +}) diff --git a/packages/react-swr/src/shared-utils.test.ts b/packages/react-swr/src/shared-utils.test.ts new file mode 100644 index 000000000..ad49e1f09 --- /dev/null +++ b/packages/react-swr/src/shared-utils.test.ts @@ -0,0 +1,33 @@ +import { SharedUtils } from './shared-utils' + +describe('sharedUtils', () => { + const utils = new SharedUtils(['test', 'path'], {}) + + describe('.matcher', () => { + it('strategy=partial', () => { + expect(utils.matcher()([['test', 'path'], { input: { value1: 'test' } }])).toBe(true) + expect(utils.matcher()([['test', 'path'], {}])).toBe(true) + expect(utils.matcher()([['invalid'], { input: { value1: 'test' } }])).toBe(false) + + expect(utils.matcher({ input: { value1: true } })([['test', 'path'], { input: { value1: true, value2: true } }])).toBe(true) + expect(utils.matcher({ input: { value1: true } })([['test', 'path'], { input: { value1: false, value2: true } }])).toBe(false) + }) + + it('strategy=exact', () => { + expect(utils.matcher({ strategy: 'exact', input: { value1: 'test' } })([['test', 'path'], { input: { value1: 'test' } }])).toBe(true) + expect(utils.matcher({ strategy: 'exact' })([['test', 'path'], { input: { value1: 'test' } }])).toBe(false) + expect(utils.matcher({ strategy: 'exact', input: { value1: 'test' } })([['invalid'], { input: { value1: 'test' } }])).toBe(false) + }) + + it('with prefix', () => { + const prefixedUtils = new SharedUtils(['test', 'path'], { prefix: '__prefix__' }) + + expect(prefixedUtils.matcher()(['__prefix__', ['test', 'path'], { input: { value1: 'test' } }])).toBe(true) + expect(prefixedUtils.matcher()([['test', 'path'], { input: { value1: 'test' } }])).toBe(false) + expect(prefixedUtils.matcher()(['__invalid__', ['test', 'path'], { input: { value1: 'test' } }])).toBe(false) + + expect(prefixedUtils.matcher({ strategy: 'exact', input: { value1: 'test' } })(['__prefix__', ['test', 'path'], { input: { value1: 'test' } }])).toBe(true) + expect(prefixedUtils.matcher({ strategy: 'exact' })(['__prefix__', ['test', 'path'], { input: { value1: 'test' } }])).toBe(false) + }) + }) +}) diff --git a/packages/react-swr/src/shared-utils.ts b/packages/react-swr/src/shared-utils.ts new file mode 100644 index 000000000..8fd7be0b8 --- /dev/null +++ b/packages/react-swr/src/shared-utils.ts @@ -0,0 +1,38 @@ +import type { MaybeOptionalOptions, PartialDeep } from '@orpc/shared' +import type { OperationKeyPrefixOptions } from './key' +import type { Matcher, MatcherOptions, MatcherStrategy } from './types' +import { resolveMaybeOptionalOptions } from '@orpc/shared' +import { generateOperationKey } from './key' +import { isSubsetOf } from './utils' + +export class SharedUtils { + constructor( + protected readonly path: string[], + protected readonly options: OperationKeyPrefixOptions, + ) {} + + /** + * Generate a matcher function that returns `true` if the key matches the specified conditions. + * + * @see {@link https://orpc.dev/docs/integrations/react-swr#manual-revalidation React SWR Manual Revalidation Docs} + */ + matcher( + ...rest: MaybeOptionalOptions> + ): Matcher { + const { input, strategy = 'partial' } = resolveMaybeOptionalOptions(rest) + + const expectedKey = generateOperationKey(this.path, { prefix: this.options.prefix, input: input as PartialDeep }) + + return (key) => { + if (!isSubsetOf(expectedKey, key)) { + return false + } + + if (strategy === 'exact' && !isSubsetOf(key, expectedKey)) { + return false + } + + return true + } + } +} diff --git a/packages/react-swr/src/types.ts b/packages/react-swr/src/types.ts new file mode 100644 index 000000000..3a7cdca2a --- /dev/null +++ b/packages/react-swr/src/types.ts @@ -0,0 +1,67 @@ +import type { ClientContext } from '@orpc/client' +import type { PartialDeep } from '@orpc/shared' +import type { SWRSubscriptionOptions } from 'swr/subscription' +import type { OperationKey } from './key' + +export type InferSubscriberOutput = TOutput extends AsyncIterable ? U[] : never +export type InferLiveSubscriberOutput = TOutput extends AsyncIterable ? U : never + +export type OperationType = 'fetcher' | 'mutator' | 'subscriber' | 'liveSubscriber' + +export const OPERATION_CONTEXT_SYMBOL: unique symbol = Symbol.for('ORPC_SWR_OPERATION_CONTEXT') as any + +export interface OperationContext { + [OPERATION_CONTEXT_SYMBOL]?: { + key: unknown + type: OperationType + } +} + +export type KeyOptions + = undefined extends TInput ? { input?: TInput } : { input: TInput } + +export type MatcherStrategy = 'exact' | 'partial' + +export type MatcherOptions + = ( + 'partial' extends TStrategy + ? { input?: PartialDeep } + : undefined extends TInput + ? { input?: TInput } + : { input: TInput } + ) & { + strategy?: TStrategy + } + +export type Matcher = (key?: unknown) => boolean + +export type FetcherOptions + = object extends TClientContext ? { context?: TClientContext } : { context: TClientContext } + +export type SubscriberOptions = FetcherOptions & { + /** + * Determines how data is handled when the subscription refetches. + * - `reset`: Replace existing data with new data + * - `append`: Add new data to existing data + * + * @default 'reset' + */ + refetchMode?: 'reset' | 'append' + + /** + * Maximum number of chunks to store. + * When exceeded, the oldest chunks will be removed. + */ + maxChunks?: number +} + +export type MutatorOptions = FetcherOptions + +export type Fetcher = (key: OperationKey) => Promise + +export type Mutator = (key: unknown, options: Readonly<{ arg: TInput }>) => Promise + +export type Subscriber = ( + key: OperationKey, + options: SWRSubscriptionOptions, +) => (() => void) diff --git a/packages/react-swr/src/utils.test.ts b/packages/react-swr/src/utils.test.ts new file mode 100644 index 000000000..04ee5a89f --- /dev/null +++ b/packages/react-swr/src/utils.test.ts @@ -0,0 +1,43 @@ +import { isSubsetOf } from './utils' + +describe('isSubsetOf', () => { + it('returns true for identical values and valid subsets', () => { + // Identical values + expect(isSubsetOf([1, 2], [1, 2])).toBe(true) + expect(isSubsetOf({}, {})).toBe(true) + expect(isSubsetOf([], [])).toBe(true) + + // Object subsets + expect(isSubsetOf({ a: 1 }, { a: 1, b: 2 })).toBe(true) + expect(isSubsetOf({}, { a: 1 })).toBe(true) + expect(isSubsetOf({ a: undefined }, { a: 1 })).toBe(true) + + // Array subsets + expect(isSubsetOf([1, 2], [1, 2, 3])).toBe(true) + expect(isSubsetOf([], [1])).toBe(true) + + // Nested structures + expect(isSubsetOf({ a: { b: 2 } }, { a: { b: 2, c: 3 } })).toBe(true) + expect(isSubsetOf({ a: { b: [1, 2] } }, { a: { b: [1, 2, 3] } })).toBe(true) + }) + + it('returns false for type mismatches and non-subsets', () => { + // Type mismatches + expect(isSubsetOf([1, 2], 'string')).toBe(false) + expect(isSubsetOf({ a: 1 }, [1, 2])).toBe(false) + + // Non-subset objects + expect(isSubsetOf({ a: 1 }, { a: 2 })).toBe(false) + expect(isSubsetOf({ a: { b: 2 } }, { a: { b: 3 } })).toBe(false) + expect(isSubsetOf({ a: { b: 2 } }, { a: undefined })).toBe(false) + + // Non-subset arrays + expect(isSubsetOf([1, 2], [2, 3])).toBe(false) + expect(isSubsetOf([[1]], [[2]])).toBe(false) + + // Different instances + const date1 = new Date() + const date2 = new Date(date1) + expect(isSubsetOf(date1, date2)).toBe(false) + }) +}) diff --git a/packages/react-swr/src/utils.ts b/packages/react-swr/src/utils.ts new file mode 100644 index 000000000..4800dcb2e --- /dev/null +++ b/packages/react-swr/src/utils.ts @@ -0,0 +1,13 @@ +import { isPlainObject } from '@orpc/shared' + +export function isSubsetOf(subsetKey: unknown, fullKey: unknown): boolean { + return subsetKey === fullKey + ? true + : typeof subsetKey !== typeof fullKey + ? false + : isPlainObject(subsetKey) && isPlainObject(fullKey) + ? Object.keys(subsetKey).every(key => subsetKey[key] === undefined || isSubsetOf(subsetKey[key], fullKey[key])) + : Array.isArray(subsetKey) && Array.isArray(fullKey) + ? subsetKey.every((value, index) => isSubsetOf(value, fullKey[index])) + : false +} diff --git a/packages/react-swr/tests/__shared__/orpc.ts b/packages/react-swr/tests/__shared__/orpc.ts new file mode 100644 index 000000000..3db4b1c2d --- /dev/null +++ b/packages/react-swr/tests/__shared__/orpc.ts @@ -0,0 +1,42 @@ +import type { RouterClient } from '@orpc/server' +import { createORPCClient } from '@orpc/client' +import { RPCLink } from '@orpc/client/fetch' +import { asyncIteratorObject, os } from '@orpc/server' +import { RPCHandler } from '@orpc/server/fetch' +import z from 'zod' +import { createSWRUtils } from '../../src' + +const staticProcedure = os + .errors({ STATIC_ERROR: { data: z.object({ static: z.string() }) } }) + .input(z.object({ input: z.number() })) + .output(z.object({ output: z.string() })) + .handler(vi.fn(({ input }) => ({ output: input.input.toString() }))) + +export const router = { + static: staticProcedure, + stream: os + .errors({ STREAM_ERROR: { data: z.object({ stream: z.string() }) } }) + .input(z.object({ input: z.number() }).optional()) + .output(asyncIteratorObject(z.object({ output: z.string() }))) + .handler(vi.fn(async function* ({ input }) { + for (let i = 0; i < (input?.input ?? 0); i++) { + yield { output: i.toString() } + } + })), + nested: { + static: staticProcedure, + }, +} + +const handler = new RPCHandler(router) + +// prefer createORPCClient over createRouterClient for more close realistic +export const client: RouterClient = createORPCClient(new RPCLink({ + origin: 'http://localhost', + fetch: async (url, init) => { + const { response } = await handler.handle(new Request(url, init)) + return response ?? new Response('Not Found', { status: 404 }) + }, +})) + +export const orpc = createSWRUtils(client) diff --git a/packages/react-swr/tests/e2e.test-d.ts b/packages/react-swr/tests/e2e.test-d.ts new file mode 100644 index 000000000..40bfa6160 --- /dev/null +++ b/packages/react-swr/tests/e2e.test-d.ts @@ -0,0 +1,82 @@ +import { isInferableError } from '@orpc/client' +import useSWR from 'swr' +import useSWRInfinite from 'swr/infinite' +import useSWRMutation from 'swr/mutation' +import useSWRSubscription from 'swr/subscription' +import { client, orpc } from './__shared__/orpc' + +it('.call', () => { + expectTypeOf(orpc.static.call).toEqualTypeOf(client.static) +}) + +it('useSWR', () => { + const swr = useSWR( + orpc.nested.static.key({ input: { input: 123 } }), + orpc.static.fetcher(), + ) + + expectTypeOf(swr.data).toEqualTypeOf<{ output: string } | undefined>() + + // FIXME: this should be an error because invalid key + useSWR( + 'invalid', + orpc.static.fetcher(), + ) +}) + +it('useSWRMutation', () => { + const mutation = useSWRMutation( + orpc.nested.static.key({ input: { input: 123 } }), + orpc.static.mutator(), + ) + + expectTypeOf[0]>().toEqualTypeOf<{ input: number }>() + expectTypeOf(mutation.data).toEqualTypeOf<{ output: string } | undefined>() +}) + +it('useSWRInfinite', () => { + const swr = useSWRInfinite( + index => orpc.nested.static.key({ input: { input: index + 1 } }), + orpc.static.fetcher(), + ) + + expectTypeOf(swr.data).toEqualTypeOf | undefined>() +}) + +it('useSWRSubscription with subscriber', () => { + const subscription = useSWRSubscription( + orpc.stream.key({ input: { input: 3 } }), + orpc.stream.subscriber({ maxChunks: 2 }), + ) + + expectTypeOf(subscription.data).toEqualTypeOf | undefined>() + + if (subscription.error && isInferableError(subscription.error) && subscription.error.code === 'STREAM_ERROR') { + expectTypeOf(subscription.error.data).toEqualTypeOf<{ stream: string }>() + } + + useSWRSubscription( + 'invalid', + // @ts-expect-error: invalid key + orpc.stream.subscriber({ maxChunks: 2 }), + ) +}) + +it('useSWRSubscription with liveSubscriber', () => { + const subscription = useSWRSubscription( + orpc.stream.key({ input: { input: 3 } }), + orpc.stream.liveSubscriber(), + ) + + expectTypeOf(subscription.data).toEqualTypeOf<{ output: string } | undefined>() + + if (subscription.error && isInferableError(subscription.error) && subscription.error.code === 'STREAM_ERROR') { + expectTypeOf(subscription.error.data).toEqualTypeOf<{ stream: string }>() + } + + useSWRSubscription( + 'invalid', + // @ts-expect-error: invalid key + orpc.stream.liveSubscriber(), + ) +}) diff --git a/packages/react-swr/tests/e2e.test.tsx b/packages/react-swr/tests/e2e.test.tsx new file mode 100644 index 000000000..af895a0ad --- /dev/null +++ b/packages/react-swr/tests/e2e.test.tsx @@ -0,0 +1,118 @@ +import { act, renderHook } from '@testing-library/react' +import useSWR, { mutate } from 'swr' +import useSWRInfinite from 'swr/infinite' +import useSWRMutation from 'swr/mutation' +import useSWRSubscription from 'swr/subscription' +import { orpc } from './__shared__/orpc' + +beforeEach(async () => { + vi.clearAllMocks() + // Clear SWR's global cache to avoid carrying data between tests + await mutate(() => true, undefined, { revalidate: false }) +}) + +it('case: useSWR & mutate & useSWRMutation', async () => { + const fetcher = vi.fn(orpc.static.fetcher()) + + const { result } = renderHook(() => { + const swr = useSWR( + orpc.nested.static.key({ input: { input: 123 } }), + fetcher, + ) + const mutation = useSWRMutation( + orpc.nested.static.key({ input: { input: 123 } }), + orpc.static.mutator(), + ) + + return { swr, mutation } + }) + + expect(result.current.swr.isLoading).toBe(true) + + await act(async () => { + await vi.waitFor(() => expect(result.current.swr.data).toEqual({ output: '123' })) + }) + expect(fetcher).toHaveBeenCalledTimes(1) + + await act(async () => { + await result.current.mutation.trigger({ input: 456 }) + }) + expect(fetcher).toHaveBeenCalledTimes(2) + + await act(async () => { + await mutate(orpc.matcher()) + }) + expect(fetcher).toHaveBeenCalledTimes(3) + + await act(async () => { + await mutate(orpc.nested.static.matcher({ input: { input: 123 }, strategy: 'exact' })) + }) + expect(fetcher).toHaveBeenCalledTimes(4) + + await act(async () => { + await mutate(orpc.stream.matcher()) + }) + // not matched - no invalidate happens + expect(fetcher).toHaveBeenCalledTimes(4) +}) + +it('case: useSWRInfinite', async () => { + const { result } = renderHook(() => { + const swr = useSWRInfinite( + index => orpc.nested.static.key({ input: { input: index + 1 } }), + orpc.static.fetcher(), + ) + + return { swr } + }) + + expect(result.current.swr.isLoading).toBe(true) + + await act(async () => { + await vi.waitFor(() => expect(result.current.swr.data).toEqual([{ output: '1' }])) + result.current.swr.setSize(2) + }) + + await act(async () => { + await vi.waitFor(() => expect(result.current.swr.data).toEqual([{ output: '1' }, { output: '2' }])) + result.current.swr.setSize(3) + }) + + await act(async () => { + await vi.waitFor(() => expect(result.current.swr.data).toEqual([{ output: '1' }, { output: '2' }, { output: '3' }])) + }) +}) + +it('case: useSWRSubscription & .subscriber', async () => { + const { result } = renderHook(() => { + const subscription = useSWRSubscription( + orpc.stream.key({ input: { input: 3 } }), + orpc.stream.subscriber({ maxChunks: 2 }), + ) + + return { subscription } + }) + + expect(result.current.subscription.data).toBeUndefined() + + await act(async () => { + await vi.waitFor(() => expect(result.current.subscription.data).toEqual([{ output: '1' }, { output: '2' }])) + }) +}) + +it('case: useSWRSubscription & .liveSubscriber', async () => { + const { result } = renderHook(() => { + const subscription = useSWRSubscription( + orpc.stream.key({ input: { input: 4 } }), + orpc.stream.liveSubscriber(), + ) + + return { subscription } + }) + + expect(result.current.subscription.data).toBeUndefined() + + await act(async () => { + await vi.waitFor(() => expect(result.current.subscription.data).toEqual({ output: '3' })) + }) +}) diff --git a/packages/react-swr/tsconfig.json b/packages/react-swr/tsconfig.json new file mode 100644 index 000000000..b6ee325dd --- /dev/null +++ b/packages/react-swr/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.lib.json", + "references": [ + { "path": "../client" }, + { "path": "../shared" } + ], + "include": ["package.json", "src"], + "exclude": [ + "**/*.test.*", + "**/*.test-d.ts", + "**/*.bench.*", + "**/__tests__/**", + "**/__mocks__/**", + "**/__snapshots__/**" + ] +} diff --git a/packages/server/README.md b/packages/server/README.md index 901bf7228..ac77a6078 100644 --- a/packages/server/README.md +++ b/packages/server/README.md @@ -52,6 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). +- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/shared/README.md b/packages/shared/README.md index e9e44a02a..a4077cd5a 100644 --- a/packages/shared/README.md +++ b/packages/shared/README.md @@ -52,6 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). +- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/tanstack-query/README.md b/packages/tanstack-query/README.md index 2dc339da7..234cf4c63 100644 --- a/packages/tanstack-query/README.md +++ b/packages/tanstack-query/README.md @@ -52,6 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). +- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/trpc/README.md b/packages/trpc/README.md index df2f7ba21..7111a3a17 100644 --- a/packages/trpc/README.md +++ b/packages/trpc/README.md @@ -52,6 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). +- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/valibot/README.md b/packages/valibot/README.md index 9061eedb1..76a06e0e2 100644 --- a/packages/valibot/README.md +++ b/packages/valibot/README.md @@ -52,6 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). +- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/zod/README.md b/packages/zod/README.md index b72653ec8..dbb2b27f2 100644 --- a/packages/zod/README.md +++ b/packages/zod/README.md @@ -52,6 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). +- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0f14ee9a6..be28275bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -728,6 +728,22 @@ importers: specifier: ^6.0.1 version: 6.1.0(@opentelemetry/api@1.9.1) + packages/react-swr: + dependencies: + '@orpc/client': + specifier: workspace:* + version: link:../client + '@orpc/shared': + specifier: workspace:* + version: link:../shared + devDependencies: + swr: + specifier: ^2.4.1 + version: 2.4.2(react@19.2.7) + zod: + specifier: ^4.4.3 + version: 4.4.3 + packages/server: dependencies: '@orpc/client': @@ -9399,6 +9415,11 @@ packages: swagger-ui@5.32.9: resolution: {integrity: sha512-F+RBm5bswwJ3oxlYkcyH5EkM6hHZsJEiSPi8zWw66ncBpuue3sz9dmiXepWYv8BiHqnK82TAHyjoyYLHkEVm+w==} + swr@2.4.2: + resolution: {integrity: sha512-ej644Y2bvkIajfR32KGeSSdBXQW+ScjGjkybZgSE7kFpk9eGnV44XY9FJylXi+W75pavSX1PVNB57W5EbhGIYw==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + swrv@1.2.0: resolution: {integrity: sha512-lH/g4UcNyj+7lzK4eRGT4C68Q4EhQ6JtM9otPRIASfhhzfLWtbZPHcMuhuba7S9YVYuxkMUGImwMyGpfbkH07A==} peerDependencies: @@ -19678,6 +19699,12 @@ snapshots: - debug - supports-color + swr@2.4.2(react@19.2.7): + dependencies: + dequal: 2.0.3 + react: 19.2.7 + use-sync-external-store: 1.6.0(react@19.2.7) + swrv@1.2.0(vue@3.5.40(typescript@6.0.3)): dependencies: vue: 3.5.40(typescript@6.0.3) diff --git a/vitest.config.ts b/vitest.config.ts index 8f4cc3351..2e572ce37 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -48,6 +48,7 @@ export default defineConfig(({ mode }) => ({ './packages/next/**/*.test.tsx', './packages/tanstack-query/**/*.test.tsx', './packages/pinia-colada/**/*.test.tsx', + './packages/react-swr/**/*.test.tsx', ], benchmark: { exclude: ['**/**'], From d5590c11aa44de299b7a13b553fc0a5f80134a06 Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Sat, 25 Jul 2026 09:59:17 +0700 Subject: [PATCH 2/8] fix example --- apps/content/docs/integrations/react-swr.md | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/content/docs/integrations/react-swr.md b/apps/content/docs/integrations/react-swr.md index dda1bfe93..d6b067ed1 100644 --- a/apps/content/docs/integrations/react-swr.md +++ b/apps/content/docs/integrations/react-swr.md @@ -167,7 +167,6 @@ interface ClientContext extends SWROperationContext { const GET_OPERATION_TYPE = new Set(['fetcher', 'subscriber', 'liveSubscriber']) const link = new RPCLink({ - url: 'http://localhost:3000/rpc', method: ({ context }, path) => { const operationType = context[SWR_OPERATION_CONTEXT_SYMBOL]?.type From 664a1569b4011bed23a79827a5163cf2469d5f30 Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Sat, 25 Jul 2026 10:08:20 +0700 Subject: [PATCH 3/8] test(react-swr): cover unwrap keys on conflicting method proxies --- packages/react-swr/src/router-utils.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/react-swr/src/router-utils.test.ts b/packages/react-swr/src/router-utils.test.ts index 5a98cd5f4..bbb39ab41 100644 --- a/packages/react-swr/src/router-utils.test.ts +++ b/packages/react-swr/src/router-utils.test.ts @@ -81,6 +81,14 @@ describe('createRouterUtils', () => { expect(utils[Symbol.for('a')]).toBe(undefined) }) + it('stops recursive on symbol and RECURSIVE_CLIENT_UNWRAP_KEYS of conflicting method proxies', () => { + const utils = createRouterUtils(client) as any + const keyUtils = utils.key // "key" is both a util method and a nested client + + expect(keyUtils[Symbol.for('a')]).toBe(undefined) + expect(keyUtils.bind).toBeInstanceOf(Function) // resolved from the underlying method, not the nested utils + }) + it('stops recursive on RECURSIVE_CLIENT_UNWRAP_KEYS', () => { client.then = vi.fn() From 1cb6ec06a662542b00032594e74ccf9418a74829 Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Sat, 25 Jul 2026 10:14:10 +0700 Subject: [PATCH 4/8] repo:fix --- packages/react-swr/package.json | 4 ++-- pnpm-lock.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-swr/package.json b/packages/react-swr/package.json index 0e8a0d744..5a4033cf4 100644 --- a/packages/react-swr/package.json +++ b/packages/react-swr/package.json @@ -37,14 +37,14 @@ "type:check": "tsc -b" }, "peerDependencies": { - "swr": ">=2.3.6" + "swr": ">=2.4.2" }, "dependencies": { "@orpc/client": "workspace:*", "@orpc/shared": "workspace:*" }, "devDependencies": { - "swr": "^2.4.1", + "swr": "^2.4.2", "zod": "^4.4.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be28275bb..75441da08 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -738,7 +738,7 @@ importers: version: link:../shared devDependencies: swr: - specifier: ^2.4.1 + specifier: ^2.4.2 version: 2.4.2(react@19.2.7) zod: specifier: ^4.4.3 From eafd38c5b8ec8c75cc732fb16e91599dc8985a2f Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Sat, 25 Jul 2026 10:18:30 +0700 Subject: [PATCH 5/8] refactor(swr): rename package to @orpc/swr --- README.md | 2 +- apps/content/.vitepress/config.ts | 2 +- .../integrations/{react-swr.md => swr.md} | 16 +++++----- packages/ai-sdk/README.md | 2 +- packages/arktype/README.md | 2 +- packages/bun/README.md | 2 +- packages/client/README.md | 2 +- packages/cloudflare/README.md | 2 +- packages/contract/README.md | 2 +- packages/effect/README.md | 2 +- packages/evlog/README.md | 2 +- packages/json-schema/README.md | 2 +- packages/nest/README.md | 2 +- packages/next/README.md | 2 +- packages/openapi/README.md | 2 +- packages/opentelemetry/README.md | 2 +- packages/pinia-colada/README.md | 2 +- packages/pino/README.md | 2 +- packages/publisher/README.md | 2 +- packages/ratelimit/README.md | 2 +- packages/server/README.md | 2 +- packages/shared/README.md | 2 +- packages/{react-swr => swr}/.gitignore | 0 packages/{react-swr => swr}/README.md | 6 ++-- packages/{react-swr => swr}/package.json | 4 +-- packages/{react-swr => swr}/src/index.test.ts | 0 packages/{react-swr => swr}/src/index.ts | 0 packages/{react-swr => swr}/src/key.test.ts | 0 packages/{react-swr => swr}/src/key.ts | 2 +- .../src/procedure-utils.test-d.ts | 0 .../src/procedure-utils.test.ts | 0 .../{react-swr => swr}/src/procedure-utils.ts | 12 +++---- .../src/router-utils.test-d.ts | 0 .../src/router-utils.test.ts | 0 .../{react-swr => swr}/src/router-utils.ts | 2 +- .../src/shared-utils.test-d.ts | 0 .../src/shared-utils.test.ts | 0 .../{react-swr => swr}/src/shared-utils.ts | 2 +- packages/{react-swr => swr}/src/types.ts | 0 packages/{react-swr => swr}/src/utils.test.ts | 0 packages/{react-swr => swr}/src/utils.ts | 0 .../tests/__shared__/orpc.ts | 0 .../{react-swr => swr}/tests/e2e.test-d.ts | 0 .../{react-swr => swr}/tests/e2e.test.tsx | 0 packages/{react-swr => swr}/tsconfig.json | 0 packages/tanstack-query/README.md | 2 +- packages/trpc/README.md | 2 +- packages/valibot/README.md | 2 +- packages/zod/README.md | 2 +- pnpm-lock.yaml | 32 +++++++++---------- vitest.config.ts | 2 +- 51 files changed, 64 insertions(+), 64 deletions(-) rename apps/content/docs/integrations/{react-swr.md => swr.md} (94%) rename packages/{react-swr => swr}/.gitignore (100%) rename packages/{react-swr => swr}/README.md (99%) rename packages/{react-swr => swr}/package.json (93%) rename packages/{react-swr => swr}/src/index.test.ts (100%) rename packages/{react-swr => swr}/src/index.ts (100%) rename packages/{react-swr => swr}/src/key.test.ts (100%) rename packages/{react-swr => swr}/src/key.ts (91%) rename packages/{react-swr => swr}/src/procedure-utils.test-d.ts (100%) rename packages/{react-swr => swr}/src/procedure-utils.test.ts (100%) rename packages/{react-swr => swr}/src/procedure-utils.ts (91%) rename packages/{react-swr => swr}/src/router-utils.test-d.ts (100%) rename packages/{react-swr => swr}/src/router-utils.test.ts (100%) rename packages/{react-swr => swr}/src/router-utils.ts (96%) rename packages/{react-swr => swr}/src/shared-utils.test-d.ts (100%) rename packages/{react-swr => swr}/src/shared-utils.test.ts (100%) rename packages/{react-swr => swr}/src/shared-utils.ts (90%) rename packages/{react-swr => swr}/src/types.ts (100%) rename packages/{react-swr => swr}/src/utils.test.ts (100%) rename packages/{react-swr => swr}/src/utils.ts (100%) rename packages/{react-swr => swr}/tests/__shared__/orpc.ts (100%) rename packages/{react-swr => swr}/tests/e2e.test-d.ts (100%) rename packages/{react-swr => swr}/tests/e2e.test.tsx (100%) rename packages/{react-swr => swr}/tsconfig.json (100%) diff --git a/README.md b/README.md index 1747fe54d..f99660aa5 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). -- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). +- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/apps/content/.vitepress/config.ts b/apps/content/.vitepress/config.ts index d35719c7c..123ef4b9c 100644 --- a/apps/content/.vitepress/config.ts +++ b/apps/content/.vitepress/config.ts @@ -196,7 +196,7 @@ export default withMermaid(defineConfig({ { text: 'OpenTelemetry', link: '/docs/integrations/opentelemetry' }, { text: 'Pinia Colada', link: '/docs/integrations/pinia-colada' }, { text: 'Pino', link: '/docs/integrations/pino' }, - { text: 'React SWR', link: '/docs/integrations/react-swr' }, + { text: 'SWR', link: '/docs/integrations/swr' }, { text: 'Standard Schema', link: '/docs/integrations/standard-schema' }, { text: 'Tanstack Query', link: '/docs/integrations/tanstack-query' }, { text: 'tRPC', link: '/docs/integrations/trpc' }, diff --git a/apps/content/docs/integrations/react-swr.md b/apps/content/docs/integrations/swr.md similarity index 94% rename from apps/content/docs/integrations/react-swr.md rename to apps/content/docs/integrations/swr.md index d6b067ed1..85b980f3f 100644 --- a/apps/content/docs/integrations/react-swr.md +++ b/apps/content/docs/integrations/swr.md @@ -1,4 +1,4 @@ -# React SWR Integration +# SWR Integration [SWR](https://swr.vercel.app/) is a React Hooks library for data fetching that provides features like caching, revalidation, and more. oRPC SWR integration is very lightweight and straightforward. There is no extra overhead. @@ -11,23 +11,23 @@ This guide assumes you are already familiar with [SWR](https://swr.vercel.app/). ::: code-group ```sh [npm] -npm install @orpc/react-swr@beta +npm install @orpc/swr@beta ``` ```sh [yarn] -yarn add @orpc/react-swr@beta +yarn add @orpc/swr@beta ``` ```sh [pnpm] -pnpm add @orpc/react-swr@beta +pnpm add @orpc/swr@beta ``` ```sh [bun] -bun add @orpc/react-swr@beta +bun add @orpc/swr@beta ``` ```sh [deno] -deno add npm:@orpc/react-swr@beta +deno add npm:@orpc/swr@beta ``` ::: @@ -37,7 +37,7 @@ deno add npm:@orpc/react-swr@beta Before you begin, set up either a [server-side client](/docs/client/server-side) or a [client-side client](/docs/client/client-side). ```ts -import { createSWRUtils } from '@orpc/react-swr' +import { createSWRUtils } from '@orpc/swr' export const orpc = createSWRUtils(client) @@ -159,7 +159,7 @@ When clients are invoked through the SWR integration, an **operation context** i import { SWR_OPERATION_CONTEXT_SYMBOL, SWROperationContext, -} from '@orpc/react-swr' +} from '@orpc/swr' interface ClientContext extends SWROperationContext { } diff --git a/packages/ai-sdk/README.md b/packages/ai-sdk/README.md index 832c41be1..a62a73306 100644 --- a/packages/ai-sdk/README.md +++ b/packages/ai-sdk/README.md @@ -52,7 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). -- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). +- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/arktype/README.md b/packages/arktype/README.md index f369500b7..882660b69 100644 --- a/packages/arktype/README.md +++ b/packages/arktype/README.md @@ -52,7 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). -- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). +- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/bun/README.md b/packages/bun/README.md index f8499d60c..7d9e0b719 100644 --- a/packages/bun/README.md +++ b/packages/bun/README.md @@ -49,7 +49,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). -- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). +- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/client/README.md b/packages/client/README.md index 1747fe54d..f99660aa5 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -52,7 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). -- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). +- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/cloudflare/README.md b/packages/cloudflare/README.md index 2fc40a407..f64589fdc 100644 --- a/packages/cloudflare/README.md +++ b/packages/cloudflare/README.md @@ -52,7 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). -- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). +- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/contract/README.md b/packages/contract/README.md index c3bc94345..8b8594e50 100644 --- a/packages/contract/README.md +++ b/packages/contract/README.md @@ -52,7 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). -- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). +- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/effect/README.md b/packages/effect/README.md index 6e7736c5f..5bfb63bd5 100644 --- a/packages/effect/README.md +++ b/packages/effect/README.md @@ -52,7 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). -- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). +- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/evlog/README.md b/packages/evlog/README.md index ec350c98f..ba01a8b6f 100644 --- a/packages/evlog/README.md +++ b/packages/evlog/README.md @@ -52,7 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). -- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). +- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/json-schema/README.md b/packages/json-schema/README.md index 1cbc19696..7a4da3313 100644 --- a/packages/json-schema/README.md +++ b/packages/json-schema/README.md @@ -52,7 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). -- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). +- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/nest/README.md b/packages/nest/README.md index b30b992cf..d7c30214d 100644 --- a/packages/nest/README.md +++ b/packages/nest/README.md @@ -52,7 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). -- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). +- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/next/README.md b/packages/next/README.md index a356e0d29..52e9f2f4c 100644 --- a/packages/next/README.md +++ b/packages/next/README.md @@ -52,7 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). -- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). +- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/openapi/README.md b/packages/openapi/README.md index 1bc46bad5..029f318a5 100644 --- a/packages/openapi/README.md +++ b/packages/openapi/README.md @@ -52,7 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). -- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). +- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/opentelemetry/README.md b/packages/opentelemetry/README.md index 01c1b8eba..c448ccbb9 100644 --- a/packages/opentelemetry/README.md +++ b/packages/opentelemetry/README.md @@ -52,7 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). -- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). +- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/pinia-colada/README.md b/packages/pinia-colada/README.md index 51c0233b5..af7b84e5e 100644 --- a/packages/pinia-colada/README.md +++ b/packages/pinia-colada/README.md @@ -52,7 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). -- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). +- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/pino/README.md b/packages/pino/README.md index 2308d925f..b3c871081 100644 --- a/packages/pino/README.md +++ b/packages/pino/README.md @@ -52,7 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). -- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). +- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/publisher/README.md b/packages/publisher/README.md index 95968def9..0ad90c7b5 100644 --- a/packages/publisher/README.md +++ b/packages/publisher/README.md @@ -52,7 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). -- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). +- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/ratelimit/README.md b/packages/ratelimit/README.md index 0ad607b87..ede390f87 100644 --- a/packages/ratelimit/README.md +++ b/packages/ratelimit/README.md @@ -52,7 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). -- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). +- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/server/README.md b/packages/server/README.md index ac77a6078..9b0f87ce4 100644 --- a/packages/server/README.md +++ b/packages/server/README.md @@ -52,7 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). -- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). +- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/shared/README.md b/packages/shared/README.md index a4077cd5a..8c8a4bf4a 100644 --- a/packages/shared/README.md +++ b/packages/shared/README.md @@ -52,7 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). -- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). +- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/react-swr/.gitignore b/packages/swr/.gitignore similarity index 100% rename from packages/react-swr/.gitignore rename to packages/swr/.gitignore diff --git a/packages/react-swr/README.md b/packages/swr/README.md similarity index 99% rename from packages/react-swr/README.md rename to packages/swr/README.md index f4de0ad28..c8ed8ccbd 100644 --- a/packages/react-swr/README.md +++ b/packages/swr/README.md @@ -4,8 +4,8 @@ codecov - - weekly downloads + + weekly downloads CodSpeed @@ -52,7 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). -- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). +- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/react-swr/package.json b/packages/swr/package.json similarity index 93% rename from packages/react-swr/package.json rename to packages/swr/package.json index 5a4033cf4..044450737 100644 --- a/packages/react-swr/package.json +++ b/packages/swr/package.json @@ -1,5 +1,5 @@ { - "name": "@orpc/react-swr", + "name": "@orpc/swr", "type": "module", "version": "2.0.0-beta.19", "license": "MIT", @@ -7,7 +7,7 @@ "repository": { "type": "git", "url": "git+https://github.com/middleapi/orpc.git", - "directory": "packages/react-swr" + "directory": "packages/swr" }, "keywords": [ "orpc", diff --git a/packages/react-swr/src/index.test.ts b/packages/swr/src/index.test.ts similarity index 100% rename from packages/react-swr/src/index.test.ts rename to packages/swr/src/index.test.ts diff --git a/packages/react-swr/src/index.ts b/packages/swr/src/index.ts similarity index 100% rename from packages/react-swr/src/index.ts rename to packages/swr/src/index.ts diff --git a/packages/react-swr/src/key.test.ts b/packages/swr/src/key.test.ts similarity index 100% rename from packages/react-swr/src/key.test.ts rename to packages/swr/src/key.test.ts diff --git a/packages/react-swr/src/key.ts b/packages/swr/src/key.ts similarity index 91% rename from packages/react-swr/src/key.ts rename to packages/swr/src/key.ts index 7626e5306..d83fb353b 100644 --- a/packages/react-swr/src/key.ts +++ b/packages/swr/src/key.ts @@ -19,7 +19,7 @@ export type OperationKey /** * Generate a **full matching** key for SWR operations. * - * @see {@link https://orpc.dev/docs/integrations/react-swr#data-fetching React SWR Key Docs} + * @see {@link https://orpc.dev/docs/integrations/swr#data-fetching SWR Key Docs} */ export function generateOperationKey( path: string[], diff --git a/packages/react-swr/src/procedure-utils.test-d.ts b/packages/swr/src/procedure-utils.test-d.ts similarity index 100% rename from packages/react-swr/src/procedure-utils.test-d.ts rename to packages/swr/src/procedure-utils.test-d.ts diff --git a/packages/react-swr/src/procedure-utils.test.ts b/packages/swr/src/procedure-utils.test.ts similarity index 100% rename from packages/react-swr/src/procedure-utils.test.ts rename to packages/swr/src/procedure-utils.test.ts diff --git a/packages/react-swr/src/procedure-utils.ts b/packages/swr/src/procedure-utils.ts similarity index 91% rename from packages/react-swr/src/procedure-utils.ts rename to packages/swr/src/procedure-utils.ts index 3f733b00d..51bc77d3a 100644 --- a/packages/react-swr/src/procedure-utils.ts +++ b/packages/swr/src/procedure-utils.ts @@ -25,7 +25,7 @@ export class ProcedureUtils @@ -41,7 +41,7 @@ export class ProcedureUtils> @@ -54,7 +54,7 @@ export class ProcedureUtils> @@ -80,7 +80,7 @@ export class ProcedureUtils> @@ -145,7 +145,7 @@ export class ProcedureUtils> @@ -198,7 +198,7 @@ export class ProcedureUtils> diff --git a/packages/react-swr/src/router-utils.test-d.ts b/packages/swr/src/router-utils.test-d.ts similarity index 100% rename from packages/react-swr/src/router-utils.test-d.ts rename to packages/swr/src/router-utils.test-d.ts diff --git a/packages/react-swr/src/router-utils.test.ts b/packages/swr/src/router-utils.test.ts similarity index 100% rename from packages/react-swr/src/router-utils.test.ts rename to packages/swr/src/router-utils.test.ts diff --git a/packages/react-swr/src/router-utils.ts b/packages/swr/src/router-utils.ts similarity index 96% rename from packages/react-swr/src/router-utils.ts rename to packages/swr/src/router-utils.ts index 5e88aa0f7..ba1de4c08 100644 --- a/packages/react-swr/src/router-utils.ts +++ b/packages/swr/src/router-utils.ts @@ -26,7 +26,7 @@ export interface RouterUtilsOptions extends OperationKeyPrefixOptions { * Create a swr router utils from a client. * * @info Both client-side and server-side clients are supported. - * @see {@link https://orpc.dev/docs/integrations/react-swr React SWR Integration} + * @see {@link https://orpc.dev/docs/integrations/swr SWR Integration} */ export function createRouterUtils( client: T, diff --git a/packages/react-swr/src/shared-utils.test-d.ts b/packages/swr/src/shared-utils.test-d.ts similarity index 100% rename from packages/react-swr/src/shared-utils.test-d.ts rename to packages/swr/src/shared-utils.test-d.ts diff --git a/packages/react-swr/src/shared-utils.test.ts b/packages/swr/src/shared-utils.test.ts similarity index 100% rename from packages/react-swr/src/shared-utils.test.ts rename to packages/swr/src/shared-utils.test.ts diff --git a/packages/react-swr/src/shared-utils.ts b/packages/swr/src/shared-utils.ts similarity index 90% rename from packages/react-swr/src/shared-utils.ts rename to packages/swr/src/shared-utils.ts index 8fd7be0b8..eab0f87f6 100644 --- a/packages/react-swr/src/shared-utils.ts +++ b/packages/swr/src/shared-utils.ts @@ -14,7 +14,7 @@ export class SharedUtils { /** * Generate a matcher function that returns `true` if the key matches the specified conditions. * - * @see {@link https://orpc.dev/docs/integrations/react-swr#manual-revalidation React SWR Manual Revalidation Docs} + * @see {@link https://orpc.dev/docs/integrations/swr#manual-revalidation SWR Manual Revalidation Docs} */ matcher( ...rest: MaybeOptionalOptions> diff --git a/packages/react-swr/src/types.ts b/packages/swr/src/types.ts similarity index 100% rename from packages/react-swr/src/types.ts rename to packages/swr/src/types.ts diff --git a/packages/react-swr/src/utils.test.ts b/packages/swr/src/utils.test.ts similarity index 100% rename from packages/react-swr/src/utils.test.ts rename to packages/swr/src/utils.test.ts diff --git a/packages/react-swr/src/utils.ts b/packages/swr/src/utils.ts similarity index 100% rename from packages/react-swr/src/utils.ts rename to packages/swr/src/utils.ts diff --git a/packages/react-swr/tests/__shared__/orpc.ts b/packages/swr/tests/__shared__/orpc.ts similarity index 100% rename from packages/react-swr/tests/__shared__/orpc.ts rename to packages/swr/tests/__shared__/orpc.ts diff --git a/packages/react-swr/tests/e2e.test-d.ts b/packages/swr/tests/e2e.test-d.ts similarity index 100% rename from packages/react-swr/tests/e2e.test-d.ts rename to packages/swr/tests/e2e.test-d.ts diff --git a/packages/react-swr/tests/e2e.test.tsx b/packages/swr/tests/e2e.test.tsx similarity index 100% rename from packages/react-swr/tests/e2e.test.tsx rename to packages/swr/tests/e2e.test.tsx diff --git a/packages/react-swr/tsconfig.json b/packages/swr/tsconfig.json similarity index 100% rename from packages/react-swr/tsconfig.json rename to packages/swr/tsconfig.json diff --git a/packages/tanstack-query/README.md b/packages/tanstack-query/README.md index 234cf4c63..c9e3e749a 100644 --- a/packages/tanstack-query/README.md +++ b/packages/tanstack-query/README.md @@ -52,7 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). -- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). +- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/trpc/README.md b/packages/trpc/README.md index 7111a3a17..dec87d26e 100644 --- a/packages/trpc/README.md +++ b/packages/trpc/README.md @@ -52,7 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). -- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). +- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/valibot/README.md b/packages/valibot/README.md index 76a06e0e2..4eb35a969 100644 --- a/packages/valibot/README.md +++ b/packages/valibot/README.md @@ -52,7 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). -- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). +- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/packages/zod/README.md b/packages/zod/README.md index dbb2b27f2..d7fa3282c 100644 --- a/packages/zod/README.md +++ b/packages/zod/README.md @@ -52,7 +52,7 @@ You can read the documentation [here](https://orpc.dev). - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools. - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest). - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/). -- [@orpc/react-swr](https://www.npmjs.com/package/@orpc/react-swr): Integrate with [SWR](https://swr.vercel.app/). +- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/). - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/). - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/). - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/). diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 75441da08..21d919253 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -728,22 +728,6 @@ importers: specifier: ^6.0.1 version: 6.1.0(@opentelemetry/api@1.9.1) - packages/react-swr: - dependencies: - '@orpc/client': - specifier: workspace:* - version: link:../client - '@orpc/shared': - specifier: workspace:* - version: link:../shared - devDependencies: - swr: - specifier: ^2.4.2 - version: 2.4.2(react@19.2.7) - zod: - specifier: ^4.4.3 - version: 4.4.3 - packages/server: dependencies: '@orpc/client': @@ -806,6 +790,22 @@ importers: specifier: ^4.4.3 version: 4.4.3 + packages/swr: + dependencies: + '@orpc/client': + specifier: workspace:* + version: link:../client + '@orpc/shared': + specifier: workspace:* + version: link:../shared + devDependencies: + swr: + specifier: ^2.4.2 + version: 2.4.2(react@19.2.7) + zod: + specifier: ^4.4.3 + version: 4.4.3 + packages/tanstack-query: dependencies: '@orpc/client': diff --git a/vitest.config.ts b/vitest.config.ts index 2e572ce37..fb0e5d228 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -48,7 +48,7 @@ export default defineConfig(({ mode }) => ({ './packages/next/**/*.test.tsx', './packages/tanstack-query/**/*.test.tsx', './packages/pinia-colada/**/*.test.tsx', - './packages/react-swr/**/*.test.tsx', + './packages/swr/**/*.test.tsx', ], benchmark: { exclude: ['**/**'], From 04dff80c8f7bf0a614c332926d7d86b62c047246 Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Sat, 25 Jul 2026 15:49:57 +0700 Subject: [PATCH 6/8] feat(swr): support refetchMode 'replace' in subscriber --- packages/swr/src/procedure-utils.test.ts | 55 ++++++++++++++++++++++++ packages/swr/src/procedure-utils.ts | 42 +++++++++++++++--- packages/swr/src/types.ts | 10 +++-- packages/swr/tests/e2e.test.tsx | 28 ++++++++++++ 4 files changed, 124 insertions(+), 11 deletions(-) diff --git a/packages/swr/src/procedure-utils.test.ts b/packages/swr/src/procedure-utils.test.ts index 99b6d45db..cdc1e07bc 100644 --- a/packages/swr/src/procedure-utils.test.ts +++ b/packages/swr/src/procedure-utils.test.ts @@ -106,6 +106,61 @@ describe('procedureUtils', () => { expect(next.mock.calls[2]![1](['1', '2'])).toEqual(['2', '__event__3']) }) + it('on success refetchMode=replace with previous data', async () => { + client.mockImplementationOnce(async function* () { + await new Promise(resolve => setTimeout(resolve, 10)) + yield '__event__1' + yield '__event__2' + yield '__event__3' + }) + const subscriber = utils.subscriber({ context: { batch: true }, maxChunks: 2, refetchMode: 'replace' }) + + // simulate SWR: updaters are invoked synchronously with the current data + let data: unknown = ['__previous__'] + const next = vi.fn((error, update?) => { + if (error === undefined) { + data = typeof update === 'function' ? update(data) : update + } + }) + + subscriber(key, { next }) + + await new Promise(resolve => setTimeout(resolve, 20)) + + // one probe call + one final replace call, no updates during the stream + expect(next).toHaveBeenCalledTimes(2) + expect(next).toHaveBeenNthCalledWith(1, undefined, expect.any(Function)) + // exceeds maxChunks, so it should only keep the last 2 events + expect(next).toHaveBeenNthCalledWith(2, undefined, ['__event__2', '__event__3']) + expect(data).toEqual(['__event__2', '__event__3']) + }) + + it('on success refetchMode=replace without previous data', async () => { + client.mockImplementationOnce(async function* () { + await new Promise(resolve => setTimeout(resolve, 10)) + yield '__event__1' + yield '__event__2' + yield '__event__3' + }) + const subscriber = utils.subscriber({ context: { batch: true }, refetchMode: 'replace' }) + + // simulate SWR: updaters are invoked synchronously with the current data + let data: unknown + const next = vi.fn((error, update?) => { + if (error === undefined) { + data = typeof update === 'function' ? update(data) : update + } + }) + + subscriber(key, { next }) + + await new Promise(resolve => setTimeout(resolve, 20)) + + // one probe call + one update per event, streamed live like append mode + expect(next).toHaveBeenCalledTimes(4) + expect(data).toEqual(['__event__1', '__event__2', '__event__3']) + }) + it('on unsubscribe', async () => { client.mockImplementationOnce(async function* () { await new Promise(resolve => setTimeout(resolve, 100)) diff --git a/packages/swr/src/procedure-utils.ts b/packages/swr/src/procedure-utils.ts index 51bc77d3a..17a0742e3 100644 --- a/packages/swr/src/procedure-utils.ts +++ b/packages/swr/src/procedure-utils.ts @@ -110,20 +110,48 @@ export class ProcedureUtils { - const newData = Array.isArray(old) ? [...old, event] : [event] + hasPreviousData = old !== undefined + return old as InferSubscriberOutput + }) + } - if (typeof maxChunks === 'number' && newData.length > maxChunks) { - return newData.slice(newData.length - maxChunks) as InferSubscriberOutput + const shouldUpdateDataDuringStream = refetchMode !== 'replace' || !hasPreviousData + let buffer: unknown[] = [] + + for await (const event of iterator) { + if (shouldUpdateDataDuringStream) { + next(undefined, (old) => { + const newData = Array.isArray(old) ? [...old, event] : [event] + + if (typeof maxChunks === 'number' && newData.length > maxChunks) { + return newData.slice(newData.length - maxChunks) as InferSubscriberOutput + } + + return newData as InferSubscriberOutput + }) + } + else { + buffer = [...buffer, event] + + if (typeof maxChunks === 'number' && buffer.length > maxChunks) { + buffer = buffer.slice(buffer.length - maxChunks) } + } + } - return newData as InferSubscriberOutput - }) + if (!shouldUpdateDataDuringStream) { + next(undefined, buffer as InferSubscriberOutput) } } catch (error) { diff --git a/packages/swr/src/types.ts b/packages/swr/src/types.ts index 3a7cdca2a..6232832dc 100644 --- a/packages/swr/src/types.ts +++ b/packages/swr/src/types.ts @@ -40,13 +40,15 @@ export type FetcherOptions export type SubscriberOptions = FetcherOptions & { /** - * Determines how data is handled when the subscription refetches. - * - `reset`: Replace existing data with new data - * - `append`: Add new data to existing data + * Determines how data is handled when the subscription is subscribed again + * + * - `'reset'`: Clears existing data before streaming new data. + * - `'append'`: Adds new streamed chunks to the existing data. + * - `'replace'`: Buffers streamed data and replaces existing data after the stream completes. * * @default 'reset' */ - refetchMode?: 'reset' | 'append' + refetchMode?: 'append' | 'reset' | 'replace' /** * Maximum number of chunks to store. diff --git a/packages/swr/tests/e2e.test.tsx b/packages/swr/tests/e2e.test.tsx index af895a0ad..343239311 100644 --- a/packages/swr/tests/e2e.test.tsx +++ b/packages/swr/tests/e2e.test.tsx @@ -100,6 +100,34 @@ it('case: useSWRSubscription & .subscriber', async () => { }) }) +it('case: useSWRSubscription & .subscriber refetchMode=replace', async () => { + const first = renderHook(() => useSWRSubscription( + orpc.stream.key({ input: { input: 3 } }), + orpc.stream.subscriber({ refetchMode: 'replace' }), + )) + + await act(async () => { + await vi.waitFor(() => expect(first.result.current.data).toEqual([{ output: '0' }, { output: '1' }, { output: '2' }])) + }) + + first.unmount() + + const second = renderHook(() => useSWRSubscription( + orpc.stream.key({ input: { input: 3 } }), + orpc.stream.subscriber({ refetchMode: 'replace' }), + )) + + // previous data is preserved while the new stream is buffering + expect(second.result.current.data).toEqual([{ output: '0' }, { output: '1' }, { output: '2' }]) + + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 50)) + }) + + // append mode would end with 6 events here, replace keeps only the new stream's events + expect(second.result.current.data).toEqual([{ output: '0' }, { output: '1' }, { output: '2' }]) +}) + it('case: useSWRSubscription & .liveSubscriber', async () => { const { result } = renderHook(() => { const subscription = useSWRSubscription( From 3a47337ca0cb0c067bfa43a161a540b6c95df034 Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Sat, 25 Jul 2026 15:58:21 +0700 Subject: [PATCH 7/8] test(swr): make replaced data distinct in refetchMode=replace e2e --- packages/swr/tests/__shared__/orpc.ts | 12 +++++++----- packages/swr/tests/e2e.test.tsx | 12 +++++++----- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/swr/tests/__shared__/orpc.ts b/packages/swr/tests/__shared__/orpc.ts index 3db4b1c2d..867780fbd 100644 --- a/packages/swr/tests/__shared__/orpc.ts +++ b/packages/swr/tests/__shared__/orpc.ts @@ -12,17 +12,19 @@ const staticProcedure = os .output(z.object({ output: z.string() })) .handler(vi.fn(({ input }) => ({ output: input.input.toString() }))) +export const streamHandler = vi.fn(async function* ({ input }: { input?: { input: number } }) { + for (let i = 0; i < (input?.input ?? 0); i++) { + yield { output: i.toString() } + } +}) + export const router = { static: staticProcedure, stream: os .errors({ STREAM_ERROR: { data: z.object({ stream: z.string() }) } }) .input(z.object({ input: z.number() }).optional()) .output(asyncIteratorObject(z.object({ output: z.string() }))) - .handler(vi.fn(async function* ({ input }) { - for (let i = 0; i < (input?.input ?? 0); i++) { - yield { output: i.toString() } - } - })), + .handler(streamHandler), nested: { static: staticProcedure, }, diff --git a/packages/swr/tests/e2e.test.tsx b/packages/swr/tests/e2e.test.tsx index 343239311..f318788e0 100644 --- a/packages/swr/tests/e2e.test.tsx +++ b/packages/swr/tests/e2e.test.tsx @@ -3,7 +3,7 @@ import useSWR, { mutate } from 'swr' import useSWRInfinite from 'swr/infinite' import useSWRMutation from 'swr/mutation' import useSWRSubscription from 'swr/subscription' -import { orpc } from './__shared__/orpc' +import { orpc, streamHandler } from './__shared__/orpc' beforeEach(async () => { vi.clearAllMocks() @@ -112,6 +112,10 @@ it('case: useSWRSubscription & .subscriber refetchMode=replace', async () => { first.unmount() + streamHandler.mockImplementationOnce(async function* () { + yield { output: '__replaced__' } + }) + const second = renderHook(() => useSWRSubscription( orpc.stream.key({ input: { input: 3 } }), orpc.stream.subscriber({ refetchMode: 'replace' }), @@ -121,11 +125,9 @@ it('case: useSWRSubscription & .subscriber refetchMode=replace', async () => { expect(second.result.current.data).toEqual([{ output: '0' }, { output: '1' }, { output: '2' }]) await act(async () => { - await new Promise(resolve => setTimeout(resolve, 50)) + // append mode would end with 4 events here, replace keeps only the new stream's events + await vi.waitFor(() => expect(second.result.current.data).toEqual([{ output: '__replaced__' }])) }) - - // append mode would end with 6 events here, replace keeps only the new stream's events - expect(second.result.current.data).toEqual([{ output: '0' }, { output: '1' }, { output: '2' }]) }) it('case: useSWRSubscription & .liveSubscriber', async () => { From bea4be20151ff5bda33ed5024a20502832d7d47f Mon Sep 17 00:00:00 2001 From: Dinh Le Date: Sat, 25 Jul 2026 16:02:51 +0700 Subject: [PATCH 8/8] improve --- packages/swr/src/router-utils.ts | 2 +- packages/swr/src/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/swr/src/router-utils.ts b/packages/swr/src/router-utils.ts index ba1de4c08..834d49d90 100644 --- a/packages/swr/src/router-utils.ts +++ b/packages/swr/src/router-utils.ts @@ -35,7 +35,7 @@ export function createRouterUtils( const path = toArray(options.path) const utils = typeof client === 'function' - ? bindMethods(new ProcedureUtils(path, client as any, { prefix: options.prefix })) + ? bindMethods(new ProcedureUtils(path, client, { prefix: options.prefix })) : bindMethods(new SharedUtils(path, { prefix: options.prefix })) const recursive = new Proxy(utils, { diff --git a/packages/swr/src/types.ts b/packages/swr/src/types.ts index 6232832dc..c2568005b 100644 --- a/packages/swr/src/types.ts +++ b/packages/swr/src/types.ts @@ -8,7 +8,7 @@ export type InferLiveSubscriberOutput = TOutput extends AsyncIterable