@orpc/publisher 2.0.0-beta.40 → 2.0.0-beta.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/adapters/base-redis.d.mts +7 -25
- package/dist/adapters/base-redis.d.ts +7 -25
- package/dist/adapters/base-redis.mjs +23 -21
- package/dist/adapters/memory.mjs +8 -5
- package/dist/adapters/redis.d.mts +7 -2
- package/dist/adapters/redis.d.ts +7 -2
- package/dist/adapters/redis.mjs +17 -10
- package/dist/adapters/upstash.d.mts +2 -2
- package/dist/adapters/upstash.d.ts +2 -2
- package/dist/adapters/upstash.mjs +3 -7
- package/dist/index.d.mts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.mjs +1 -1
- package/dist/shared/{publisher.BSQf8ybR.mjs → publisher.DcPG7zl6.mjs} +47 -9
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -85,7 +85,7 @@ Like what we build over at [middleapi](https://github.com/middleapi)? You can he
|
|
|
85
85
|
|
|
86
86
|
<table>
|
|
87
87
|
<tr>
|
|
88
|
-
<td width="2000"><a href="https://screenshotone.com/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener" title="The screenshot API for developers"><img src="https://avatars.githubusercontent.com/u/97035603?v=4" width="64" align="left" hspace="12" alt="ScreenshotOne.com"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>
|
|
88
|
+
<td width="2000"><a href="https://screenshotone.com/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="The screenshot API for developers"><img src="https://avatars.githubusercontent.com/u/97035603?v=4" width="64" align="left" hspace="12" alt="ScreenshotOne.com"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>
|
|
89
89
|
</tr>
|
|
90
90
|
<tr>
|
|
91
91
|
<td width="2000"><a href="https://yuzu.health/careers?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="We're hiring NYC based engineers"><img src="https://avatars.githubusercontent.com/u/102488956?v=4" width="64" align="left" hspace="12" alt="Yuzu"/><b>Yuzu</b></a><br /><sub>We're hiring NYC based engineers</sub></td>
|
|
@@ -127,19 +127,18 @@ Like what we build over at [middleapi](https://github.com/middleapi)? You can he
|
|
|
127
127
|
<td align="center"><a href="https://soymilk.party/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="あわわわとーにゅ"><img src="https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&v=4" width="139" alt="あわわわとーにゅ"/><br />あわわわとーにゅ</a></td>
|
|
128
128
|
<td align="center"><a href="https://github.com/nicognaW?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="nk"><img src="https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&v=4" width="139" alt="nk"/><br />nk</a></td>
|
|
129
129
|
<td align="center"><a href="https://supastarter.dev/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="supastarter"><img src="https://avatars.githubusercontent.com/u/110960143?v=4" width="139" alt="supastarter"/><br />supastarter</a></td>
|
|
130
|
-
<td align="center"><a href="https://github.com/divmgl?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Dexter Miguel"><img src="https://avatars.githubusercontent.com/u/5452298?u=645993204be8696c085ecf0d228c3062efe2ed65&v=4" width="139" alt="Dexter Miguel"/><br />Dexter Miguel</a></td>
|
|
131
130
|
<td align="center"><a href="https://github.com/herrfugbaum?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="herrfugbaum"><img src="https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&v=4" width="139" alt="herrfugbaum"/><br />herrfugbaum</a></td>
|
|
131
|
+
<td align="center"><a href="https://laststance.io/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Ryota Murakami"><img src="https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&v=4" width="139" alt="Ryota Murakami"/><br />Ryota Murakami</a></td>
|
|
132
132
|
</tr>
|
|
133
133
|
<tr>
|
|
134
|
-
<td align="center"><a href="https://laststance.io/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Ryota Murakami"><img src="https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&v=4" width="139" alt="Ryota Murakami"/><br />Ryota Murakami</a></td>
|
|
135
134
|
<td align="center"><a href="https://cra.mr/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="David Cramer"><img src="https://avatars.githubusercontent.com/u/23610?v=4" width="139" alt="David Cramer"/><br />David Cramer</a></td>
|
|
136
135
|
<td align="center"><a href="https://valerii15298.github.io/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Valerii Petryniak"><img src="https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&v=4" width="139" alt="Valerii Petryniak"/><br />Valerii Petryniak</a></td>
|
|
137
136
|
<td align="center"><a href="https://letstri.dev/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Valerii Strilets"><img src="https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&v=4" width="139" alt="Valerii Strilets"/><br />Valerii Strilets</a></td>
|
|
138
137
|
<td align="center"><a href="https://blacklight.sh/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Kyle Mistele"><img src="https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&v=4" width="139" alt="Kyle Mistele"/><br />Kyle Mistele</a></td>
|
|
139
138
|
<td align="center"><a href="https://github.com/christ12938?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="christ12938"><img src="https://avatars.githubusercontent.com/u/25758598?v=4" width="139" alt="christ12938"/><br />christ12938</a></td>
|
|
139
|
+
<td align="center"><a href="https://github.com/Ryanjso?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Ryan Soderberg"><img src="https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&v=4" width="139" alt="Ryan Soderberg"/><br />Ryan Soderberg</a></td>
|
|
140
140
|
</tr>
|
|
141
141
|
<tr>
|
|
142
|
-
<td align="center"><a href="https://github.com/Ryanjso?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Ryan Soderberg"><img src="https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&v=4" width="139" alt="Ryan Soderberg"/><br />Ryan Soderberg</a></td>
|
|
143
142
|
<td align="center"><a href="https://github.com/itigoore01?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="shota"><img src="https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&v=4" width="139" alt="shota"/><br />shota</a></td>
|
|
144
143
|
<td align="center"><a href="https://github.com/ellis-driscoll?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Ellis Driscoll"><img src="https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&v=4" width="139" alt="Ellis Driscoll"/><br />Ellis Driscoll</a></td>
|
|
145
144
|
<td align="center"><a href="https://github.com/hoangbn?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Hoang Nguyen"><img src="https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&v=4" width="139" alt="Hoang Nguyen"/><br />Hoang Nguyen</a></td>
|
|
@@ -173,12 +172,13 @@ Like what we build over at [middleapi](https://github.com/middleapi)? You can he
|
|
|
173
172
|
<td align="center"><a href="https://kellychan.im/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Kelly Peilin Chan"><img src="https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&v=4" width="119" alt="Kelly Peilin Chan"/><br />Kelly Peilin Chan</a></td>
|
|
174
173
|
<td align="center"><a href="https://guyariely.com/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Guy Ariely"><img src="https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&v=4" width="119" alt="Guy Ariely"/><br />Guy Ariely</a></td>
|
|
175
174
|
<td align="center"><a href="https://paulsenon.com/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="PaulSenon"><img src="https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&v=4" width="119" alt="PaulSenon"/><br />PaulSenon</a></td>
|
|
175
|
+
<td align="center"><a href="https://nakasyou.how/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Shotaro Nakamura"><img src="https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&v=4" width="119" alt="Shotaro Nakamura"/><br />Shotaro Nakamura</a></td>
|
|
176
176
|
<td align="center"><a href="https://piscis.dev/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Alex"><img src="https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&v=4" width="119" alt="Alex"/><br />Alex</a></td>
|
|
177
177
|
<td align="center"><a href="https://opensource.gubanov.eu/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Andrey Gubanov"><img src="https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&v=4" width="119" alt="Andrey Gubanov"/><br />Andrey Gubanov</a></td>
|
|
178
178
|
</tr>
|
|
179
179
|
</table>
|
|
180
180
|
|
|
181
|
-
With thanks to [
|
|
181
|
+
With thanks to [38 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.
|
|
182
182
|
|
|
183
183
|
## References
|
|
184
184
|
|
|
@@ -9,7 +9,8 @@ import { RPCJsonSerializer } from '@orpc/client';
|
|
|
9
9
|
*/
|
|
10
10
|
interface BaseRedisPublisherOptions extends PublisherOptions {
|
|
11
11
|
/**
|
|
12
|
-
* The prefix to use for Redis keys.
|
|
12
|
+
* The prefix to use for Redis keys and Pub/Sub channels.
|
|
13
|
+
* Set it to isolate events when several apps share one Redis instance.
|
|
13
14
|
*
|
|
14
15
|
* @default ''
|
|
15
16
|
*/
|
|
@@ -64,25 +65,6 @@ interface RedisStreamEntry {
|
|
|
64
65
|
*/
|
|
65
66
|
data: unknown;
|
|
66
67
|
}
|
|
67
|
-
/**
|
|
68
|
-
* Trimming to apply to a Redis Stream while adding an entry.
|
|
69
|
-
*
|
|
70
|
-
* @see {@link https://orpc.dev/docs/helpers/publisher#adapters | Publisher Helpers - Adapters}
|
|
71
|
-
*/
|
|
72
|
-
interface RedisStreamTrimOptions {
|
|
73
|
-
/**
|
|
74
|
-
* Entries with an ID lower than this one are removed (`XTRIM key MINID minId`).
|
|
75
|
-
*/
|
|
76
|
-
minId: string;
|
|
77
|
-
/**
|
|
78
|
-
* Whether trimming is exact (`=`) or approximate (`~`).
|
|
79
|
-
*/
|
|
80
|
-
exactness: '~' | '=';
|
|
81
|
-
/**
|
|
82
|
-
* Time to live to set on the stream key, in seconds (`EXPIRE key seconds`).
|
|
83
|
-
*/
|
|
84
|
-
expireSeconds: number;
|
|
85
|
-
}
|
|
86
68
|
/**
|
|
87
69
|
* Base class for Redis-backed publisher adapters. It owns the key naming, the
|
|
88
70
|
* message format, and the resume logic, so every adapter built on it can exchange
|
|
@@ -119,11 +101,11 @@ declare abstract class BaseRedisPublisher<T extends Record<string, object>> exte
|
|
|
119
101
|
*/
|
|
120
102
|
protected abstract subscribeChannel(channel: string, listener: (message: unknown) => void, onError?: (error: ThrowableError) => void): Promise<() => Promise<void>>;
|
|
121
103
|
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
104
|
+
* Runs a Lua script (`EVAL script numkeys key [key ...] arg [arg ...]`) and resolves with its reply.
|
|
105
|
+
* The publish script also uses its key as the Pub/Sub channel, so a client that prefixes
|
|
106
|
+
* keys must prefix channels the same way.
|
|
125
107
|
*/
|
|
126
|
-
protected abstract
|
|
108
|
+
protected abstract evalScript(script: string, keys: string[], args: string[]): Promise<unknown>;
|
|
127
109
|
/**
|
|
128
110
|
* Reads the entries with an ID greater than `lastId` (`XREAD STREAMS key lastId`), oldest first.
|
|
129
111
|
*/
|
|
@@ -135,4 +117,4 @@ declare abstract class BaseRedisPublisher<T extends Record<string, object>> exte
|
|
|
135
117
|
}
|
|
136
118
|
|
|
137
119
|
export { BaseRedisPublisher };
|
|
138
|
-
export type { BaseRedisPublisherOptions, RedisStreamEntry
|
|
120
|
+
export type { BaseRedisPublisherOptions, RedisStreamEntry };
|
|
@@ -9,7 +9,8 @@ import { RPCJsonSerializer } from '@orpc/client';
|
|
|
9
9
|
*/
|
|
10
10
|
interface BaseRedisPublisherOptions extends PublisherOptions {
|
|
11
11
|
/**
|
|
12
|
-
* The prefix to use for Redis keys.
|
|
12
|
+
* The prefix to use for Redis keys and Pub/Sub channels.
|
|
13
|
+
* Set it to isolate events when several apps share one Redis instance.
|
|
13
14
|
*
|
|
14
15
|
* @default ''
|
|
15
16
|
*/
|
|
@@ -64,25 +65,6 @@ interface RedisStreamEntry {
|
|
|
64
65
|
*/
|
|
65
66
|
data: unknown;
|
|
66
67
|
}
|
|
67
|
-
/**
|
|
68
|
-
* Trimming to apply to a Redis Stream while adding an entry.
|
|
69
|
-
*
|
|
70
|
-
* @see {@link https://orpc.dev/docs/helpers/publisher#adapters | Publisher Helpers - Adapters}
|
|
71
|
-
*/
|
|
72
|
-
interface RedisStreamTrimOptions {
|
|
73
|
-
/**
|
|
74
|
-
* Entries with an ID lower than this one are removed (`XTRIM key MINID minId`).
|
|
75
|
-
*/
|
|
76
|
-
minId: string;
|
|
77
|
-
/**
|
|
78
|
-
* Whether trimming is exact (`=`) or approximate (`~`).
|
|
79
|
-
*/
|
|
80
|
-
exactness: '~' | '=';
|
|
81
|
-
/**
|
|
82
|
-
* Time to live to set on the stream key, in seconds (`EXPIRE key seconds`).
|
|
83
|
-
*/
|
|
84
|
-
expireSeconds: number;
|
|
85
|
-
}
|
|
86
68
|
/**
|
|
87
69
|
* Base class for Redis-backed publisher adapters. It owns the key naming, the
|
|
88
70
|
* message format, and the resume logic, so every adapter built on it can exchange
|
|
@@ -119,11 +101,11 @@ declare abstract class BaseRedisPublisher<T extends Record<string, object>> exte
|
|
|
119
101
|
*/
|
|
120
102
|
protected abstract subscribeChannel(channel: string, listener: (message: unknown) => void, onError?: (error: ThrowableError) => void): Promise<() => Promise<void>>;
|
|
121
103
|
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
104
|
+
* Runs a Lua script (`EVAL script numkeys key [key ...] arg [arg ...]`) and resolves with its reply.
|
|
105
|
+
* The publish script also uses its key as the Pub/Sub channel, so a client that prefixes
|
|
106
|
+
* keys must prefix channels the same way.
|
|
125
107
|
*/
|
|
126
|
-
protected abstract
|
|
108
|
+
protected abstract evalScript(script: string, keys: string[], args: string[]): Promise<unknown>;
|
|
127
109
|
/**
|
|
128
110
|
* Reads the entries with an ID greater than `lastId` (`XREAD STREAMS key lastId`), oldest first.
|
|
129
111
|
*/
|
|
@@ -135,4 +117,4 @@ declare abstract class BaseRedisPublisher<T extends Record<string, object>> exte
|
|
|
135
117
|
}
|
|
136
118
|
|
|
137
119
|
export { BaseRedisPublisher };
|
|
138
|
-
export type { BaseRedisPublisherOptions, RedisStreamEntry
|
|
120
|
+
export type { BaseRedisPublisherOptions, RedisStreamEntry };
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { RPCJsonSerializer } from '@orpc/client';
|
|
2
2
|
import { stringifyJSON, once, parseEmptyableJSON } from '@orpc/shared';
|
|
3
3
|
import { unwrapEvent, withEventMeta, getEventMeta } from '@standard-server/core';
|
|
4
|
-
import { P as Publisher } from '../shared/publisher.
|
|
4
|
+
import { P as Publisher } from '../shared/publisher.DcPG7zl6.mjs';
|
|
5
5
|
|
|
6
|
+
const PUBLISH_SCRIPT = `local id=redis.call('XADD',KEYS[1],'*','data',ARGV[1]) if ARGV[2] then redis.call('XTRIM',KEYS[1],'MINID',ARGV[2],ARGV[3]) redis.call('EXPIRE',KEYS[1],ARGV[4]) end redis.call('PUBLISH',KEYS[1],'{"data":'..ARGV[1]..',"id":"'..id..'"}')`;
|
|
6
7
|
class BaseRedisPublisher extends Publisher {
|
|
7
8
|
prefix;
|
|
8
9
|
serializer;
|
|
@@ -23,28 +24,29 @@ class BaseRedisPublisher extends Publisher {
|
|
|
23
24
|
async publish(event, payload) {
|
|
24
25
|
const channel = `${this.prefix}${event}`;
|
|
25
26
|
const data = this.serializePayload(payload);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (this.lastTrimTimes.has(channel)) {
|
|
36
|
-
id = await this.addStreamEntry(channel, stringifyJSON(data));
|
|
37
|
-
} else {
|
|
38
|
-
this.lastTrimTimes.set(channel, now);
|
|
39
|
-
id = await this.addStreamEntry(channel, stringifyJSON(data), {
|
|
40
|
-
minId: `${now - windowMs}-0`,
|
|
41
|
-
exactness: this.xTrimExactness,
|
|
42
|
-
// 2x so entries added late in a window outlive it until the next trim refreshes the TTL.
|
|
43
|
-
expireSeconds: this.resumeSeconds * 2
|
|
44
|
-
});
|
|
27
|
+
if (!this.resumeEnabled) {
|
|
28
|
+
await this.publishMessage(channel, stringifyJSON({ data }));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
const windowMs = this.resumeSeconds * 1e3;
|
|
33
|
+
for (const [trimmedChannel, trimTime] of this.lastTrimTimes) {
|
|
34
|
+
if (trimTime + windowMs >= now) {
|
|
35
|
+
break;
|
|
45
36
|
}
|
|
37
|
+
this.lastTrimTimes.delete(trimmedChannel);
|
|
38
|
+
}
|
|
39
|
+
const args = [stringifyJSON(data)];
|
|
40
|
+
if (!this.lastTrimTimes.has(channel)) {
|
|
41
|
+
this.lastTrimTimes.set(channel, now);
|
|
42
|
+
args.push(
|
|
43
|
+
this.xTrimExactness,
|
|
44
|
+
`${now - windowMs}-0`,
|
|
45
|
+
// 2x so entries added late in a window outlive it until the next trim refreshes the TTL.
|
|
46
|
+
String(this.resumeSeconds * 2)
|
|
47
|
+
);
|
|
46
48
|
}
|
|
47
|
-
await this.
|
|
49
|
+
await this.evalScript(PUBLISH_SCRIPT, [channel], args);
|
|
48
50
|
}
|
|
49
51
|
async subscribeListener(event, listener, { lastEventId, onError } = {}) {
|
|
50
52
|
const channel = `${this.prefix}${event}`;
|
package/dist/adapters/memory.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SequentialIdGenerator, once, compareSequentialIds } from '@orpc/shared';
|
|
2
2
|
import { unwrapEvent, withEventMeta, getEventMeta } from '@standard-server/core';
|
|
3
|
-
import { P as Publisher } from '../shared/publisher.
|
|
3
|
+
import { P as Publisher } from '../shared/publisher.DcPG7zl6.mjs';
|
|
4
4
|
|
|
5
5
|
class MemoryPublisher extends Publisher {
|
|
6
6
|
listenersMap = /* @__PURE__ */ new Map();
|
|
@@ -25,8 +25,7 @@ class MemoryPublisher extends Publisher {
|
|
|
25
25
|
payload = withEventMeta(original, { ...meta, id: this.idGenerator.generate() });
|
|
26
26
|
bucket.push({ expiresAt, payload });
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
listeners?.forEach((listener) => listener(payload));
|
|
28
|
+
this.listenersMap.get(event)?.forEach((listener) => listener(payload));
|
|
30
29
|
}
|
|
31
30
|
async subscribeListener(event, listener, options) {
|
|
32
31
|
this.cleanup();
|
|
@@ -45,9 +44,13 @@ class MemoryPublisher extends Publisher {
|
|
|
45
44
|
}
|
|
46
45
|
listeners.push(listener);
|
|
47
46
|
return once(async () => {
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
const current = this.listenersMap.get(event);
|
|
48
|
+
const index = current.indexOf(listener);
|
|
49
|
+
const remaining = current.filter((_, i) => i !== index);
|
|
50
|
+
if (remaining.length === 0) {
|
|
50
51
|
this.listenersMap.delete(event);
|
|
52
|
+
} else {
|
|
53
|
+
this.listenersMap.set(event, remaining);
|
|
51
54
|
}
|
|
52
55
|
});
|
|
53
56
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RedisClientType } from 'redis';
|
|
2
|
-
import { BaseRedisPublisher, BaseRedisPublisherOptions,
|
|
2
|
+
import { BaseRedisPublisher, BaseRedisPublisherOptions, RedisStreamEntry } from './base-redis.mjs';
|
|
3
3
|
import '@orpc/shared';
|
|
4
4
|
import '../index.mjs';
|
|
5
5
|
import '@orpc/client';
|
|
@@ -23,10 +23,15 @@ interface RedisPublisherOptions extends BaseRedisPublisherOptions {
|
|
|
23
23
|
declare class RedisPublisher<T extends Record<string, object>> extends BaseRedisPublisher<T> {
|
|
24
24
|
private readonly redis;
|
|
25
25
|
private readonly subscriber;
|
|
26
|
+
/**
|
|
27
|
+
* node-redis applies `keyPrefix` to keys but not channels, while the publish script
|
|
28
|
+
* publishes on its key, so channels need the same prefix.
|
|
29
|
+
*/
|
|
30
|
+
private readonly channelPrefix;
|
|
26
31
|
constructor(redis: RedisClientType<any, any, any, any, any>, { subscriber, ...options }?: RedisPublisherOptions);
|
|
27
32
|
protected publishMessage(channel: string, message: string): Promise<void>;
|
|
28
33
|
protected subscribeChannel(channel: string, listener: (message: unknown) => void): Promise<() => Promise<void>>;
|
|
29
|
-
protected
|
|
34
|
+
protected evalScript(script: string, keys: string[], args: string[]): Promise<unknown>;
|
|
30
35
|
protected readStreamEntries(key: string, lastId: string): Promise<RedisStreamEntry[]>;
|
|
31
36
|
}
|
|
32
37
|
|
package/dist/adapters/redis.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RedisClientType } from 'redis';
|
|
2
|
-
import { BaseRedisPublisher, BaseRedisPublisherOptions,
|
|
2
|
+
import { BaseRedisPublisher, BaseRedisPublisherOptions, RedisStreamEntry } from './base-redis.js';
|
|
3
3
|
import '@orpc/shared';
|
|
4
4
|
import '../index.js';
|
|
5
5
|
import '@orpc/client';
|
|
@@ -23,10 +23,15 @@ interface RedisPublisherOptions extends BaseRedisPublisherOptions {
|
|
|
23
23
|
declare class RedisPublisher<T extends Record<string, object>> extends BaseRedisPublisher<T> {
|
|
24
24
|
private readonly redis;
|
|
25
25
|
private readonly subscriber;
|
|
26
|
+
/**
|
|
27
|
+
* node-redis applies `keyPrefix` to keys but not channels, while the publish script
|
|
28
|
+
* publishes on its key, so channels need the same prefix.
|
|
29
|
+
*/
|
|
30
|
+
private readonly channelPrefix;
|
|
26
31
|
constructor(redis: RedisClientType<any, any, any, any, any>, { subscriber, ...options }?: RedisPublisherOptions);
|
|
27
32
|
protected publishMessage(channel: string, message: string): Promise<void>;
|
|
28
33
|
protected subscribeChannel(channel: string, listener: (message: unknown) => void): Promise<() => Promise<void>>;
|
|
29
|
-
protected
|
|
34
|
+
protected evalScript(script: string, keys: string[], args: string[]): Promise<unknown>;
|
|
30
35
|
protected readStreamEntries(key: string, lastId: string): Promise<RedisStreamEntry[]>;
|
|
31
36
|
}
|
|
32
37
|
|
package/dist/adapters/redis.mjs
CHANGED
|
@@ -2,33 +2,40 @@ import { BaseRedisPublisher } from './base-redis.mjs';
|
|
|
2
2
|
import '@orpc/client';
|
|
3
3
|
import '@orpc/shared';
|
|
4
4
|
import '@standard-server/core';
|
|
5
|
-
import '../shared/publisher.
|
|
5
|
+
import '../shared/publisher.DcPG7zl6.mjs';
|
|
6
6
|
|
|
7
7
|
class RedisPublisher extends BaseRedisPublisher {
|
|
8
8
|
constructor(redis, { subscriber, ...options } = {}) {
|
|
9
9
|
super(options);
|
|
10
10
|
this.redis = redis;
|
|
11
|
+
const keyPrefix = redis.options?.keyPrefix;
|
|
12
|
+
if (keyPrefix !== void 0 && typeof keyPrefix !== "string") {
|
|
13
|
+
throw new TypeError("RedisPublisher only supports a string keyPrefix on the Redis client.");
|
|
14
|
+
}
|
|
11
15
|
this.subscriber = subscriber ?? redis.duplicate();
|
|
16
|
+
this.channelPrefix = keyPrefix ?? "";
|
|
12
17
|
}
|
|
13
18
|
subscriber;
|
|
19
|
+
/**
|
|
20
|
+
* node-redis applies `keyPrefix` to keys but not channels, while the publish script
|
|
21
|
+
* publishes on its key, so channels need the same prefix.
|
|
22
|
+
*/
|
|
23
|
+
channelPrefix;
|
|
14
24
|
async publishMessage(channel, message) {
|
|
15
25
|
await connectIfNeeded(this.redis);
|
|
16
|
-
await this.redis.publish(channel
|
|
26
|
+
await this.redis.publish(`${this.channelPrefix}${channel}`, message);
|
|
17
27
|
}
|
|
18
28
|
async subscribeChannel(channel, listener) {
|
|
29
|
+
const prefixedChannel = `${this.channelPrefix}${channel}`;
|
|
19
30
|
await connectIfNeeded(this.subscriber);
|
|
20
|
-
await this.subscriber.subscribe(
|
|
31
|
+
await this.subscriber.subscribe(prefixedChannel, listener);
|
|
21
32
|
return async () => {
|
|
22
|
-
await this.subscriber.unsubscribe(
|
|
33
|
+
await this.subscriber.unsubscribe(prefixedChannel, listener);
|
|
23
34
|
};
|
|
24
35
|
}
|
|
25
|
-
async
|
|
36
|
+
async evalScript(script, keys, args) {
|
|
26
37
|
await connectIfNeeded(this.redis);
|
|
27
|
-
|
|
28
|
-
return await this.redis.xAdd(key, "*", { data });
|
|
29
|
-
}
|
|
30
|
-
const [id] = await this.redis.multi().xAdd(key, "*", { data }).xTrim(key, "MINID", trim.minId, { strategyModifier: trim.exactness }).expire(key, trim.expireSeconds).exec();
|
|
31
|
-
return id;
|
|
38
|
+
return await this.redis.eval(script, { keys, arguments: args });
|
|
32
39
|
}
|
|
33
40
|
async readStreamEntries(key, lastId) {
|
|
34
41
|
await connectIfNeeded(this.redis);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ThrowableError } from '@orpc/shared';
|
|
2
2
|
import { Redis } from '@upstash/redis';
|
|
3
|
-
import { BaseRedisPublisher, BaseRedisPublisherOptions,
|
|
3
|
+
import { BaseRedisPublisher, BaseRedisPublisherOptions, RedisStreamEntry } from './base-redis.mjs';
|
|
4
4
|
import '../index.mjs';
|
|
5
5
|
import '@orpc/client';
|
|
6
6
|
|
|
@@ -21,7 +21,7 @@ declare class UpstashPublisher<T extends Record<string, object>> extends BaseRed
|
|
|
21
21
|
constructor(redis: Redis, options?: UpstashPublisherOptions);
|
|
22
22
|
protected publishMessage(channel: string, message: string): Promise<void>;
|
|
23
23
|
protected subscribeChannel(channel: string, listener: (message: unknown) => void, onError?: (error: ThrowableError) => void): Promise<() => Promise<void>>;
|
|
24
|
-
protected
|
|
24
|
+
protected evalScript(script: string, keys: string[], args: string[]): Promise<unknown>;
|
|
25
25
|
protected readStreamEntries(key: string, lastId: string): Promise<RedisStreamEntry[]>;
|
|
26
26
|
private subscribeIfNeeded;
|
|
27
27
|
private unsubscribeIfUnused;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ThrowableError } from '@orpc/shared';
|
|
2
2
|
import { Redis } from '@upstash/redis';
|
|
3
|
-
import { BaseRedisPublisher, BaseRedisPublisherOptions,
|
|
3
|
+
import { BaseRedisPublisher, BaseRedisPublisherOptions, RedisStreamEntry } from './base-redis.js';
|
|
4
4
|
import '../index.js';
|
|
5
5
|
import '@orpc/client';
|
|
6
6
|
|
|
@@ -21,7 +21,7 @@ declare class UpstashPublisher<T extends Record<string, object>> extends BaseRed
|
|
|
21
21
|
constructor(redis: Redis, options?: UpstashPublisherOptions);
|
|
22
22
|
protected publishMessage(channel: string, message: string): Promise<void>;
|
|
23
23
|
protected subscribeChannel(channel: string, listener: (message: unknown) => void, onError?: (error: ThrowableError) => void): Promise<() => Promise<void>>;
|
|
24
|
-
protected
|
|
24
|
+
protected evalScript(script: string, keys: string[], args: string[]): Promise<unknown>;
|
|
25
25
|
protected readStreamEntries(key: string, lastId: string): Promise<RedisStreamEntry[]>;
|
|
26
26
|
private subscribeIfNeeded;
|
|
27
27
|
private unsubscribeIfUnused;
|
|
@@ -2,7 +2,7 @@ import { promiseWithResolvers } from '@orpc/shared';
|
|
|
2
2
|
import { BaseRedisPublisher } from './base-redis.mjs';
|
|
3
3
|
import '@orpc/client';
|
|
4
4
|
import '@standard-server/core';
|
|
5
|
-
import '../shared/publisher.
|
|
5
|
+
import '../shared/publisher.DcPG7zl6.mjs';
|
|
6
6
|
|
|
7
7
|
class UpstashPublisher extends BaseRedisPublisher {
|
|
8
8
|
constructor(redis, options = {}) {
|
|
@@ -36,12 +36,8 @@ class UpstashPublisher extends BaseRedisPublisher {
|
|
|
36
36
|
await this.unsubscribeIfUnused(channel);
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
|
-
async
|
|
40
|
-
|
|
41
|
-
return this.redis.xadd(key, "*", { data });
|
|
42
|
-
}
|
|
43
|
-
const [id] = await this.redis.multi().xadd(key, "*", { data }).xtrim(key, { strategy: "MINID", exactness: trim.exactness, threshold: trim.minId }).expire(key, trim.expireSeconds).exec();
|
|
44
|
-
return id;
|
|
39
|
+
async evalScript(script, keys, args) {
|
|
40
|
+
return await this.redis.eval(script, keys, args);
|
|
45
41
|
}
|
|
46
42
|
async readStreamEntries(key, lastId) {
|
|
47
43
|
const results = await this.redis.xread(key, lastId);
|
package/dist/index.d.mts
CHANGED
|
@@ -6,6 +6,8 @@ interface PublisherOptions {
|
|
|
6
6
|
*
|
|
7
7
|
* If the buffer exceeds this limit, the oldest event is dropped.
|
|
8
8
|
* This prevents unbounded memory growth if consumers process events slowly.
|
|
9
|
+
* Missed events the adapter replays for `lastEventId` while subscribing are all kept,
|
|
10
|
+
* and the limit then applies to how much further behind the consumer falls.
|
|
9
11
|
*
|
|
10
12
|
* Set to:
|
|
11
13
|
* - `0`: Disable buffering. Events must be consumed before the next one arrives.
|
|
@@ -42,6 +44,8 @@ declare abstract class Publisher<T extends Record<string, object>> {
|
|
|
42
44
|
/**
|
|
43
45
|
* Subscribes to a specific event using a callback function.
|
|
44
46
|
* Returns an unsubscribe function to remove the listener.
|
|
47
|
+
* Events missed since `lastEventId` should reach the listener before the returned promise
|
|
48
|
+
* resolves, so iterator subscribers keep them all regardless of `maxBufferedEvents`.
|
|
45
49
|
*
|
|
46
50
|
* @remarks
|
|
47
51
|
* This method should be protected to avoid conflicts with `subscribe` method
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ interface PublisherOptions {
|
|
|
6
6
|
*
|
|
7
7
|
* If the buffer exceeds this limit, the oldest event is dropped.
|
|
8
8
|
* This prevents unbounded memory growth if consumers process events slowly.
|
|
9
|
+
* Missed events the adapter replays for `lastEventId` while subscribing are all kept,
|
|
10
|
+
* and the limit then applies to how much further behind the consumer falls.
|
|
9
11
|
*
|
|
10
12
|
* Set to:
|
|
11
13
|
* - `0`: Disable buffering. Events must be consumed before the next one arrives.
|
|
@@ -42,6 +44,8 @@ declare abstract class Publisher<T extends Record<string, object>> {
|
|
|
42
44
|
/**
|
|
43
45
|
* Subscribes to a specific event using a callback function.
|
|
44
46
|
* Returns an unsubscribe function to remove the listener.
|
|
47
|
+
* Events missed since `lastEventId` should reach the listener before the returned promise
|
|
48
|
+
* resolves, so iterator subscribers keep them all regardless of `maxBufferedEvents`.
|
|
45
49
|
*
|
|
46
50
|
* @remarks
|
|
47
51
|
* This method should be protected to avoid conflicts with `subscribe` method
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { P as Publisher } from './shared/publisher.
|
|
1
|
+
export { P as Publisher } from './shared/publisher.DcPG7zl6.mjs';
|
|
2
2
|
import '@orpc/shared';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AsyncIteratorClass } from '@orpc/shared';
|
|
1
|
+
import { throwIfAborted, AsyncIteratorClass } from '@orpc/shared';
|
|
2
2
|
|
|
3
3
|
class Publisher {
|
|
4
4
|
maxBufferedEvents;
|
|
@@ -11,10 +11,13 @@ class Publisher {
|
|
|
11
11
|
}
|
|
12
12
|
const signal = listenerOrOptions?.signal;
|
|
13
13
|
const maxBufferedEvents = listenerOrOptions?.maxBufferedEvents ?? this.maxBufferedEvents;
|
|
14
|
-
|
|
15
|
-
const
|
|
14
|
+
throwIfAborted(signal);
|
|
15
|
+
const resuming = listenerOrOptions?.lastEventId !== void 0;
|
|
16
|
+
const bufferedEvents = new Queue();
|
|
16
17
|
const pullResolvers = [];
|
|
17
|
-
|
|
18
|
+
let bufferFloor = 0;
|
|
19
|
+
let bufferLimit = resuming ? Infinity : maxBufferedEvents;
|
|
20
|
+
const subscription = this.subscribeListener(event, (payload) => {
|
|
18
21
|
if (signal?.aborted) {
|
|
19
22
|
return;
|
|
20
23
|
}
|
|
@@ -23,14 +26,18 @@ class Publisher {
|
|
|
23
26
|
resolver.resolve({ done: false, value: payload });
|
|
24
27
|
} else {
|
|
25
28
|
bufferedEvents.push(payload);
|
|
26
|
-
if (bufferedEvents.length >
|
|
29
|
+
if (bufferedEvents.length > bufferLimit) {
|
|
27
30
|
bufferedEvents.shift();
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
33
|
}, {
|
|
31
34
|
lastEventId: listenerOrOptions?.lastEventId,
|
|
32
35
|
onError: (error) => terminate({ kind: "error", error })
|
|
33
|
-
})
|
|
36
|
+
});
|
|
37
|
+
const unsubscribePromise = (resuming ? subscription.finally(() => {
|
|
38
|
+
bufferFloor = bufferedEvents.length;
|
|
39
|
+
bufferLimit = bufferFloor + maxBufferedEvents;
|
|
40
|
+
}) : subscription).catch((error) => terminate({ kind: "error", error }));
|
|
34
41
|
const abortListener = () => terminate({ kind: "error", error: signal.reason });
|
|
35
42
|
signal?.addEventListener("abort", abortListener);
|
|
36
43
|
let terminalState;
|
|
@@ -47,7 +54,7 @@ class Publisher {
|
|
|
47
54
|
}
|
|
48
55
|
if (signal?.aborted) {
|
|
49
56
|
pullResolvers.length = 0;
|
|
50
|
-
bufferedEvents.
|
|
57
|
+
bufferedEvents.clear();
|
|
51
58
|
}
|
|
52
59
|
await unsubscribePromise.then((unsubscribe) => unsubscribe?.());
|
|
53
60
|
}
|
|
@@ -56,8 +63,13 @@ class Publisher {
|
|
|
56
63
|
if (terminalState?.kind === "cancelled") {
|
|
57
64
|
return { done: true, value: void 0 };
|
|
58
65
|
}
|
|
59
|
-
|
|
60
|
-
|
|
66
|
+
const value = bufferedEvents.shift();
|
|
67
|
+
if (value !== void 0) {
|
|
68
|
+
if (bufferedEvents.length < bufferFloor) {
|
|
69
|
+
bufferFloor = bufferedEvents.length;
|
|
70
|
+
bufferLimit = bufferFloor + maxBufferedEvents;
|
|
71
|
+
}
|
|
72
|
+
return { done: false, value };
|
|
61
73
|
}
|
|
62
74
|
if (terminalState?.kind === "error") {
|
|
63
75
|
throw terminalState.error;
|
|
@@ -70,5 +82,31 @@ class Publisher {
|
|
|
70
82
|
);
|
|
71
83
|
}
|
|
72
84
|
}
|
|
85
|
+
class Queue {
|
|
86
|
+
items = [];
|
|
87
|
+
head = 0;
|
|
88
|
+
get length() {
|
|
89
|
+
return this.items.length - this.head;
|
|
90
|
+
}
|
|
91
|
+
push(item) {
|
|
92
|
+
this.items.push(item);
|
|
93
|
+
}
|
|
94
|
+
shift() {
|
|
95
|
+
if (this.head === this.items.length) {
|
|
96
|
+
return void 0;
|
|
97
|
+
}
|
|
98
|
+
const item = this.items[this.head];
|
|
99
|
+
this.items[this.head++] = void 0;
|
|
100
|
+
if (this.head >= 32 && this.head * 2 >= this.items.length) {
|
|
101
|
+
this.items.splice(0, this.head);
|
|
102
|
+
this.head = 0;
|
|
103
|
+
}
|
|
104
|
+
return item;
|
|
105
|
+
}
|
|
106
|
+
clear() {
|
|
107
|
+
this.items.length = 0;
|
|
108
|
+
this.head = 0;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
73
111
|
|
|
74
112
|
export { Publisher as P };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orpc/publisher",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.0.0-beta.
|
|
4
|
+
"version": "2.0.0-beta.41",
|
|
5
5
|
"description": "Typesafe pub/sub for oRPC event streaming, with memory, Redis, and Upstash adapters",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"funding": [
|
|
@@ -70,9 +70,9 @@
|
|
|
70
70
|
}
|
|
71
71
|
},
|
|
72
72
|
"dependencies": {
|
|
73
|
-
"@orpc/client": "2.0.0-beta.
|
|
74
|
-
"@orpc/shared": "2.0.0-beta.
|
|
75
|
-
"@standard-server/core": "~0.
|
|
73
|
+
"@orpc/client": "2.0.0-beta.41",
|
|
74
|
+
"@orpc/shared": "2.0.0-beta.41",
|
|
75
|
+
"@standard-server/core": "~0.10.0"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
78
|
"@upstash/redis": "^1.38.2",
|