-
-
Notifications
You must be signed in to change notification settings - Fork 145
feat(publisher): memory, ioredis, upstash redis publishers #1094
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
6b6167b
init
dinwwwh 766ab34
publisher & memory publisher
dinwwwh 688bff7
improve
dinwwwh 346f80b
redis - wip
dinwwwh ee2fb18
improve
dinwwwh a70f9b4
wip
dinwwwh 46731a2
wip
dinwwwh e2ac5d2
ioredis
dinwwwh 171bb6d
ci: tests with real redis
dinwwwh 0cc63c2
upstash redis
dinwwwh 6eae7c0
docs
dinwwwh d8a69f0
fix & improve
dinwwwh e85a5f5
fix & improve
dinwwwh b855377
Merge branch 'main' into feat/publisher/renew
dinwwwh 48002b5
fix and improve race condition
dinwwwh 7ca7912
version
dinwwwh a971e9e
fix and improve test speed
dinwwwh 0881c35
comment
dinwwwh 0e5fbfe
improve tests coverage
dinwwwh 8c3a72f
fix a test not cover as expected
dinwwwh 4d2db9c
fix
dinwwwh eb574ba
fix concurrent tests
dinwwwh dd63481
Merge branch 'main' into feat/publisher/renew
dinwwwh 5b281c1
handle error
dinwwwh 26c5f19
comment
dinwwwh e76d1e2
fix ioredis cleanup
dinwwwh bc506f7
use array instead of set - because listener can duplicate + fix onError
dinwwwh 7e4012b
cover case unsub use multiple time + same lister
dinwwwh c89872d
improve unsub
dinwwwh 5d9b55c
handles multiple subscribers on same event with race condition
dinwwwh 9cfe343
reorganize
dinwwwh 70f2209
bump version
dinwwwh File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # copy this file and rename it to .env | ||
| # then fill in the appropriate values | ||
|
|
||
| # Some tests in the project depend on Redis or Upstash Redis | ||
| # You can create a free redis instance at upstash and copy the connection details here | ||
| REDIS_URL= | ||
| UPSTASH_REDIS_REST_URL= | ||
| UPSTASH_REDIS_REST_TOKEN= |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,223 @@ | ||
| --- | ||
| title: Publisher | ||
| description: Listen and publish events with resuming support in oRPC | ||
| --- | ||
|
|
||
| # Publisher | ||
|
|
||
| The Publisher is a helper that enables you to listen to and publish events to subscribers. Combined with the [Event Iterator](/docs/client/event-iterator), it allows you to build streaming responses, real-time updates, and server-sent events with minimal requirements. | ||
|
|
||
| ## Installation | ||
|
|
||
| ::: code-group | ||
|
|
||
| ```sh [npm] | ||
| npm install @orpc/experimental-publisher@latest | ||
| ``` | ||
|
|
||
| ```sh [yarn] | ||
| yarn add @orpc/experimental-publisher@latest | ||
| ``` | ||
|
|
||
| ```sh [pnpm] | ||
| pnpm add @orpc/experimental-publisher@latest | ||
| ``` | ||
|
|
||
| ```sh [bun] | ||
| bun add @orpc/experimental-publisher@latest | ||
| ``` | ||
|
|
||
| ```sh [deno] | ||
| deno add npm:@orpc/experimental-publisher@latest | ||
| ``` | ||
|
|
||
| ::: | ||
|
|
||
| ## Basic Usage | ||
|
|
||
| ```ts twoslash | ||
| import { MemoryPublisher } from '@orpc/experimental-publisher/memory' | ||
| import { os } from '@orpc/server' | ||
| import * as z from 'zod' | ||
| // ---cut--- | ||
| const publisher = new MemoryPublisher<{ | ||
| 'something-updated': { | ||
| id: string | ||
| } | ||
| }>() | ||
|
|
||
| const live = os | ||
| .handler(async function* ({ input, signal }) { | ||
| const iterator = publisher.subscribe('something-updated', { signal }) | ||
| for await (const payload of iterator) { | ||
| // Handle payload here or yield directly to client | ||
| yield payload | ||
| } | ||
| }) | ||
|
|
||
| const publish = os | ||
| .input(z.object({ id: z.string() })) | ||
| .handler(async ({ input }) => { | ||
| await publisher.publish('something-updated', { id: input.id }) | ||
| }) | ||
| ``` | ||
|
|
||
| ::: tip | ||
| The publisher supports both static and dynamic event names. | ||
|
|
||
| ```ts | ||
| const publisher = new MemoryPublisher<Record<string, { message: string }>>() | ||
| ``` | ||
|
|
||
| ::: | ||
|
|
||
| ## Resume Feature | ||
|
|
||
| The resume feature uses `lastEventId` to determine where to resume from after a disconnection. | ||
|
|
||
| ::: warning | ||
| By default, most adapters have this feature disabled. | ||
| ::: | ||
|
|
||
| ### Server Implementation | ||
|
|
||
| When subscribing, you must forward the `lastEventId` to the publisher to enable resuming: | ||
|
|
||
| ```ts | ||
| const live = os | ||
| .handler(async function* ({ input, signal, lastEventId }) { | ||
| const iterator = publisher.subscribe('something-updated', { signal, lastEventId }) | ||
| for await (const payload of iterator) { | ||
| yield payload | ||
| } | ||
| }) | ||
| ``` | ||
|
|
||
| ::: warning Event ID Management | ||
| The publisher automatically manages event ids when resume is enabled. This means: | ||
|
|
||
| - Event ids you provide when publishing will be ignored | ||
| - When subscribing, you must forward the event id when yielding custom payloads | ||
|
|
||
| ```ts | ||
| import { getEventMeta, withEventMeta } from '@orpc/server' | ||
|
|
||
| const live = os | ||
| .handler(async function* ({ input, signal, lastEventId }) { | ||
| const iterator = publisher.subscribe('something-updated', { signal, lastEventId }) | ||
| for await (const payload of iterator) { | ||
| // Preserve event id when yielding custom data | ||
| yield withEventMeta({ custom: 'value' }, { ...getEventMeta(payload) }) | ||
| } | ||
| }) | ||
|
|
||
| const publish = os | ||
| .input(z.object({ id: z.string() })) | ||
| .handler(async ({ input }) => { | ||
| // The event id 'this-will-be-ignored' will be replaced by the publisher | ||
| await publisher.publish('something-updated', withEventMeta({ id: input.id }, { id: 'this-will-be-ignored' })) | ||
| }) | ||
| ``` | ||
|
|
||
| ::: | ||
|
|
||
| ### Client Implementation | ||
|
|
||
| On the client, you can use the [Client Retry Plugin](/docs/plugins/client-retry), which automatically controls and passes `lastEventId` to the server when reconnecting. Alternatively, you can manage `lastEventId` manually: | ||
|
|
||
| ```ts | ||
| import { getEventMeta } from '@orpc/client' | ||
|
|
||
| let lastEventId: string | undefined | ||
|
|
||
| while (true) { | ||
| try { | ||
| const iterator = await client.live('input', { lastEventId }) | ||
|
|
||
| for await (const payload of iterator) { | ||
| lastEventId = getEventMeta(payload)?.id // Update lastEventId | ||
|
|
||
| console.log(payload) | ||
| } | ||
| } | ||
| catch { | ||
| await new Promise(resolve => setTimeout(resolve, 1000)) // Wait 1 second before retrying | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Available Adapters | ||
|
|
||
| | Name | Resume Support | Description | | ||
| | ----------------------- | -------------- | ---------------------------------------------------------------- | | ||
| | `MemoryPublisher` | ✅ | A simple in-memory publisher | | ||
| | `IORedisPublisher` | ✅ | Adapter for [ioredis](https://github.com/redis/ioredis) | | ||
| | `UpstashRedisPublisher` | ✅ | Adapter for [Upstash Redis](https://github.com/upstash/redis-js) | | ||
|
|
||
| ::: info | ||
| If you'd like to add a new publisher adapter, please open an issue. | ||
| ::: | ||
|
|
||
| ### Memory Publisher | ||
|
|
||
| ```ts | ||
| import { MemoryPublisher } from '@orpc/experimental-publisher/memory' | ||
|
|
||
| const publisher = new MemoryPublisher<{ | ||
| 'something-updated': { | ||
| id: string | ||
| } | ||
| }>({ | ||
| resumeRetentionSeconds: 60 * 2, // Retain events for 2 minutes to support resume | ||
| }) | ||
| ``` | ||
|
|
||
| ::: info | ||
| Resume support is disabled by default in `MemoryPublisher`. Enable it by setting `resumeRetentionSeconds` to an appropriate value. | ||
| ::: | ||
|
|
||
| ### IORedis Publisher | ||
|
|
||
| ```ts | ||
| import { Redis } from 'ioredis' | ||
| import { IORedisPublisher } from '@orpc/experimental-publisher/ioredis' | ||
|
|
||
| const publisher = new IORedisPublisher<{ | ||
| 'something-updated': { | ||
| id: string | ||
| } | ||
| }>({ | ||
| commander: new Redis(), // For executing short-lived commands | ||
| subscriber: new Redis(), // For subscribing to events | ||
| resumeRetentionSeconds: 60 * 2, // Retain events for 2 minutes to support resume | ||
| prefix: 'orpc:publisher:', // avoid conflict with other keys | ||
| }) | ||
| ``` | ||
|
|
||
| This adapter requires two Redis instances: one for executing short-lived commands and another for subscribing to events. | ||
|
|
||
| ::: info | ||
| Resume support is disabled by default in `IORedisPublisher`. Enable it by setting `resumeRetentionSeconds` to an appropriate value. | ||
| ::: | ||
|
|
||
| ### Upstash Redis Publisher | ||
|
|
||
| ```ts | ||
| import { Redis } from '@upstash/redis' | ||
| import { UpstashRedisPublisher } from '@orpc/experimental-publisher/upstash-redis' | ||
|
|
||
| const redis = Redis.fromEnv() | ||
|
|
||
| const publisher = new UpstashRedisPublisher<{ | ||
| 'something-updated': { | ||
| id: string | ||
| } | ||
| }>(redis, { | ||
| resumeRetentionSeconds: 60 * 2, // Retain events for 2 minutes to support resume | ||
| prefix: 'orpc:publisher:', // avoid conflict with other keys | ||
| }) | ||
| ``` | ||
|
|
||
| ::: info | ||
| Resume support is disabled by default in `UpstashRedisPublisher`. Enable it by setting `resumeRetentionSeconds` to an appropriate value. | ||
| ::: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,5 +40,8 @@ | |
| }, | ||
| "dependencies": { | ||
| "@orpc/openapi": "workspace:*" | ||
| }, | ||
| "devDependencies": { | ||
| "zod": "^4.1.11" | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.