@orpc/publisher 2.0.0-beta.4 → 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.
@@ -0,0 +1,106 @@
1
+ import { RPCJsonSerializer } from '@orpc/client';
2
+ import { stringifyJSON, once, parseEmptyableJSON } from '@orpc/shared';
3
+ import { unwrapEvent, withEventMeta, getEventMeta } from '@standard-server/core';
4
+ import { P as Publisher } from '../shared/publisher.DcPG7zl6.mjs';
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..'"}')`;
7
+ class BaseRedisPublisher extends Publisher {
8
+ prefix;
9
+ serializer;
10
+ resumeEnabled;
11
+ resumeSeconds;
12
+ /**
13
+ * Tests switch this to `=` for deterministic trimming.
14
+ */
15
+ xTrimExactness = "~";
16
+ lastTrimTimes = /* @__PURE__ */ new Map();
17
+ constructor({ prefix, serializer, resume, ...options } = {}) {
18
+ super(options);
19
+ this.prefix = prefix ?? "";
20
+ this.serializer = serializer ?? new RPCJsonSerializer();
21
+ this.resumeEnabled = resume?.enabled ?? false;
22
+ this.resumeSeconds = resume?.seconds ?? 300;
23
+ }
24
+ async publish(event, payload) {
25
+ const channel = `${this.prefix}${event}`;
26
+ const data = this.serializePayload(payload);
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;
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
+ );
48
+ }
49
+ await this.evalScript(PUBLISH_SCRIPT, [channel], args);
50
+ }
51
+ async subscribeListener(event, listener, { lastEventId, onError } = {}) {
52
+ const channel = `${this.prefix}${event}`;
53
+ let pendingPayloads = [];
54
+ const resumedIds = /* @__PURE__ */ new Set();
55
+ const deduplicatingListener = (payload) => {
56
+ if (pendingPayloads) {
57
+ pendingPayloads.push(payload);
58
+ return;
59
+ }
60
+ const id = getEventMeta(payload)?.id;
61
+ if (id === void 0 || !resumedIds.has(id)) {
62
+ listener(payload);
63
+ }
64
+ };
65
+ const messageListener = (message) => {
66
+ try {
67
+ const { id, data } = parseIfText(message);
68
+ deduplicatingListener(this.deserializePayload(id, data));
69
+ } catch (error) {
70
+ onError?.(error);
71
+ }
72
+ };
73
+ const unsubscribe = await this.subscribeChannel(channel, messageListener, onError);
74
+ try {
75
+ if (this.resumeEnabled && lastEventId !== void 0) {
76
+ for (const { id, data } of await this.readStreamEntries(channel, lastEventId)) {
77
+ resumedIds.add(id);
78
+ listener(this.deserializePayload(id, parseIfText(data)));
79
+ }
80
+ }
81
+ const pending = pendingPayloads;
82
+ pendingPayloads = void 0;
83
+ pending.forEach(deduplicatingListener);
84
+ } catch (error) {
85
+ await unsubscribe();
86
+ throw error;
87
+ }
88
+ return once(unsubscribe);
89
+ }
90
+ serializePayload(payload) {
91
+ const [original, meta] = unwrapEvent(payload);
92
+ const { json, meta: jsonMeta } = this.serializer.serialize(original);
93
+ return { payload: { json, meta: jsonMeta }, meta };
94
+ }
95
+ deserializePayload(id, { payload, meta }) {
96
+ return withEventMeta(
97
+ this.serializer.deserialize(payload),
98
+ id === void 0 ? { ...meta } : { ...meta, id }
99
+ );
100
+ }
101
+ }
102
+ function parseIfText(value) {
103
+ return typeof value === "string" ? parseEmptyableJSON(value) : value;
104
+ }
105
+
106
+ export { BaseRedisPublisher };
@@ -3,16 +3,16 @@ import '@orpc/shared';
3
3
 
4
4
  interface MemoryPublisherOptions extends PublisherOptions {
5
5
  /**
6
- * Configuration for event replay support.
6
+ * Configuration for event resume support.
7
7
  *
8
8
  * When enabled, published events are temporarily stored so new
9
9
  * subscribers can resume from a previous position using `lastEventId`.
10
10
  *
11
11
  * @default { enabled: false }
12
12
  */
13
- replay?: {
13
+ resume?: {
14
14
  /**
15
- * Whether event replay support is enabled.
15
+ * Whether event resume support is enabled.
16
16
  *
17
17
  * When enabled, published events are temporarily stored so new
18
18
  * subscribers can resume from a previous position using `lastEventId`.
@@ -21,7 +21,7 @@ interface MemoryPublisherOptions extends PublisherOptions {
21
21
  */
22
22
  enabled: boolean;
23
23
  /**
24
- * How long (in seconds) to retain events for replay.
24
+ * How long (in seconds) to retain events for resume.
25
25
  *
26
26
  * Expired events are cleaned up lazily for performance reasons, so
27
27
  * some events may remain available slightly longer than this period.
@@ -31,13 +31,19 @@ interface MemoryPublisherOptions extends PublisherOptions {
31
31
  seconds?: number;
32
32
  };
33
33
  }
34
+ /**
35
+ * Publisher adapter backed by in-memory storage, with optional resume support.
36
+ * Events are delivered to subscribers within the same process only.
37
+ *
38
+ * @see {@link https://orpc.dev/docs/helpers/publisher#adapters | Publisher Helpers - Adapters}
39
+ */
34
40
  declare class MemoryPublisher<T extends Record<string, object>> extends Publisher<T> {
35
41
  private readonly listenersMap;
36
42
  private readonly idGenerator;
37
43
  private readonly eventsMap;
38
- private readonly replayEnabled;
39
- private readonly replaySeconds;
40
- constructor({ replay, ...options }?: MemoryPublisherOptions);
44
+ private readonly resumeEnabled;
45
+ private readonly resumeSeconds;
46
+ constructor({ resume, ...options }?: MemoryPublisherOptions);
41
47
  publish<K extends keyof T & string>(event: K, payload: T[K]): Promise<void>;
42
48
  protected subscribeListener<K extends keyof T & string>(event: K, listener: (payload: T[K]) => void, options?: PublisherSubscribeListenerOptions): Promise<() => Promise<void>>;
43
49
  private lastCleanupTime;
@@ -3,16 +3,16 @@ import '@orpc/shared';
3
3
 
4
4
  interface MemoryPublisherOptions extends PublisherOptions {
5
5
  /**
6
- * Configuration for event replay support.
6
+ * Configuration for event resume support.
7
7
  *
8
8
  * When enabled, published events are temporarily stored so new
9
9
  * subscribers can resume from a previous position using `lastEventId`.
10
10
  *
11
11
  * @default { enabled: false }
12
12
  */
13
- replay?: {
13
+ resume?: {
14
14
  /**
15
- * Whether event replay support is enabled.
15
+ * Whether event resume support is enabled.
16
16
  *
17
17
  * When enabled, published events are temporarily stored so new
18
18
  * subscribers can resume from a previous position using `lastEventId`.
@@ -21,7 +21,7 @@ interface MemoryPublisherOptions extends PublisherOptions {
21
21
  */
22
22
  enabled: boolean;
23
23
  /**
24
- * How long (in seconds) to retain events for replay.
24
+ * How long (in seconds) to retain events for resume.
25
25
  *
26
26
  * Expired events are cleaned up lazily for performance reasons, so
27
27
  * some events may remain available slightly longer than this period.
@@ -31,13 +31,19 @@ interface MemoryPublisherOptions extends PublisherOptions {
31
31
  seconds?: number;
32
32
  };
33
33
  }
34
+ /**
35
+ * Publisher adapter backed by in-memory storage, with optional resume support.
36
+ * Events are delivered to subscribers within the same process only.
37
+ *
38
+ * @see {@link https://orpc.dev/docs/helpers/publisher#adapters | Publisher Helpers - Adapters}
39
+ */
34
40
  declare class MemoryPublisher<T extends Record<string, object>> extends Publisher<T> {
35
41
  private readonly listenersMap;
36
42
  private readonly idGenerator;
37
43
  private readonly eventsMap;
38
- private readonly replayEnabled;
39
- private readonly replaySeconds;
40
- constructor({ replay, ...options }?: MemoryPublisherOptions);
44
+ private readonly resumeEnabled;
45
+ private readonly resumeSeconds;
46
+ constructor({ resume, ...options }?: MemoryPublisherOptions);
41
47
  publish<K extends keyof T & string>(event: K, payload: T[K]): Promise<void>;
42
48
  protected subscribeListener<K extends keyof T & string>(event: K, listener: (payload: T[K]) => void, options?: PublisherSubscribeListenerOptions): Promise<() => Promise<void>>;
43
49
  private lastCleanupTime;
@@ -1,22 +1,22 @@
1
1
  import { SequentialIdGenerator, once, compareSequentialIds } from '@orpc/shared';
2
- import { unwrapEvent, withEventMeta, getEventMeta } from '@standardserver/core';
3
- import { P as Publisher } from '../shared/publisher.BSQf8ybR.mjs';
2
+ import { unwrapEvent, withEventMeta, getEventMeta } from '@standard-server/core';
3
+ import { P as Publisher } from '../shared/publisher.DcPG7zl6.mjs';
4
4
 
5
5
  class MemoryPublisher extends Publisher {
6
6
  listenersMap = /* @__PURE__ */ new Map();
7
7
  idGenerator = new SequentialIdGenerator();
8
8
  eventsMap = /* @__PURE__ */ new Map();
9
- replayEnabled;
10
- replaySeconds;
11
- constructor({ replay, ...options } = {}) {
9
+ resumeEnabled;
10
+ resumeSeconds;
11
+ constructor({ resume, ...options } = {}) {
12
12
  super(options);
13
- this.replayEnabled = replay?.enabled ?? false;
14
- this.replaySeconds = replay?.seconds ?? 300;
13
+ this.resumeEnabled = resume?.enabled ?? false;
14
+ this.resumeSeconds = resume?.seconds ?? 300;
15
15
  }
16
16
  async publish(event, payload) {
17
17
  this.cleanup();
18
- if (this.replayEnabled) {
19
- const expiresAt = Date.now() + this.replaySeconds * 1e3;
18
+ if (this.resumeEnabled) {
19
+ const expiresAt = Date.now() + this.resumeSeconds * 1e3;
20
20
  let bucket = this.eventsMap.get(event);
21
21
  if (!bucket) {
22
22
  this.eventsMap.set(event, bucket = []);
@@ -25,15 +25,14 @@ class MemoryPublisher extends Publisher {
25
25
  payload = withEventMeta(original, { ...meta, id: this.idGenerator.generate() });
26
26
  bucket.push({ expiresAt, payload });
27
27
  }
28
- const listeners = this.listenersMap.get(event);
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();
33
- if (this.replayEnabled && options?.lastEventId !== void 0) {
32
+ if (this.resumeEnabled && options?.lastEventId !== void 0) {
34
33
  const bucket = this.eventsMap.get(event);
35
34
  if (bucket?.length) {
36
- const startIdx = findReplayStartIndex(bucket, options.lastEventId);
35
+ const startIdx = findResumeStartIndex(bucket, options.lastEventId);
37
36
  for (let i = startIdx; i < bucket.length; i++) {
38
37
  listener(bucket[i].payload);
39
38
  }
@@ -45,19 +44,23 @@ class MemoryPublisher extends Publisher {
45
44
  }
46
45
  listeners.push(listener);
47
46
  return once(async () => {
48
- listeners.splice(listeners.indexOf(listener), 1);
49
- if (listeners.length === 0) {
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
  }
54
57
  lastCleanupTime = 0;
55
58
  cleanup() {
56
- if (!this.replayEnabled) {
59
+ if (!this.resumeEnabled) {
57
60
  return;
58
61
  }
59
62
  const now = Date.now();
60
- if (now - this.lastCleanupTime < this.replaySeconds * 1e3) {
63
+ if (now - this.lastCleanupTime < this.resumeSeconds * 1e3) {
61
64
  return;
62
65
  }
63
66
  this.lastCleanupTime = now;
@@ -69,7 +72,7 @@ class MemoryPublisher extends Publisher {
69
72
  }
70
73
  }
71
74
  }
72
- function findReplayStartIndex(bucket, lastEventId) {
75
+ function findResumeStartIndex(bucket, lastEventId) {
73
76
  let lo = 0;
74
77
  let hi = bucket.length;
75
78
  while (lo < hi) {
@@ -1,9 +1,10 @@
1
1
  import { RedisClientType } from 'redis';
2
- import { Publisher, PublisherOptions, PublisherSubscribeListenerOptions } from '../index.mjs';
3
- import { RPCSerializer } from '@orpc/client';
2
+ import { BaseRedisPublisher, BaseRedisPublisherOptions, RedisStreamEntry } from './base-redis.mjs';
4
3
  import '@orpc/shared';
4
+ import '../index.mjs';
5
+ import '@orpc/client';
5
6
 
6
- interface RedisPublisherOptions extends PublisherOptions {
7
+ interface RedisPublisherOptions extends BaseRedisPublisherOptions {
7
8
  /**
8
9
  * Redis subscriber instance.
9
10
  * Pub/Sub takes over the connection, so a client with subscriptions
@@ -12,63 +13,26 @@ interface RedisPublisherOptions extends PublisherOptions {
12
13
  * @default redis.duplicate()
13
14
  */
14
15
  subscriber?: undefined | RedisClientType<any, any, any, any, any>;
15
- /**
16
- * The prefix to use for Redis keys.
17
- */
18
- prefix?: string;
19
- /**
20
- * Serializer for serialize and deserialize payloads.
21
- *
22
- * @default RPCSerializer
23
- */
24
- serializer?: undefined | Pick<RPCSerializer, keyof RPCSerializer>;
25
- /**
26
- * Configuration for event replay support.
27
- *
28
- * When enabled, published events are temporarily stored so new
29
- * subscribers can resume from a previous position using `lastEventId`.
30
- *
31
- * @default { enabled: false }
32
- */
33
- replay?: {
34
- /**
35
- * Whether event replay support is enabled.
36
- *
37
- * When enabled, published events are temporarily stored so new
38
- * subscribers can resume from a previous position using `lastEventId`.
39
- *
40
- * @default false
41
- */
42
- enabled: boolean;
43
- /**
44
- * How long (in seconds) to retain events for replay.
45
- *
46
- * Expired events are cleaned up lazily for performance reasons, so
47
- * some events may remain available slightly longer than this period.
48
- *
49
- * @default 300 (5 min)
50
- */
51
- seconds?: number;
52
- };
53
16
  }
54
- declare class RedisPublisher<T extends Record<string, object>> extends Publisher<T> {
17
+ /**
18
+ * Publisher adapter for Redis. Distributes events across processes via
19
+ * Redis Pub/Sub, with optional resume support backed by Redis Streams.
20
+ *
21
+ * @see {@link https://orpc.dev/docs/helpers/publisher#adapters | Publisher Helpers - Adapters}
22
+ */
23
+ declare class RedisPublisher<T extends Record<string, object>> extends BaseRedisPublisher<T> {
55
24
  private readonly redis;
56
25
  private readonly subscriber;
57
- private readonly prefix;
58
- private readonly serializer;
59
- private readonly replayEnabled;
60
- private readonly replaySeconds;
61
26
  /**
62
- * The exactness of the `XTRIM` command.
63
- * Used for testing purpose.
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.
64
29
  */
65
- private readonly xTrimExactness;
66
- constructor(redis: RedisClientType<any, any, any, any, any>, options?: RedisPublisherOptions);
67
- private readonly firstPublishTimeMap;
68
- publish<K extends keyof T & string>(event: K, payload: T[K]): Promise<void>;
69
- protected subscribeListener<K extends keyof T & string>(event: K, originalListener: (payload: T[K]) => void, { lastEventId, onError }?: PublisherSubscribeListenerOptions): Promise<() => Promise<void>>;
70
- private serializePayload;
71
- private deserializePayload;
30
+ private readonly channelPrefix;
31
+ constructor(redis: RedisClientType<any, any, any, any, any>, { subscriber, ...options }?: RedisPublisherOptions);
32
+ protected publishMessage(channel: string, message: string): Promise<void>;
33
+ protected subscribeChannel(channel: string, listener: (message: unknown) => void): Promise<() => Promise<void>>;
34
+ protected evalScript(script: string, keys: string[], args: string[]): Promise<unknown>;
35
+ protected readStreamEntries(key: string, lastId: string): Promise<RedisStreamEntry[]>;
72
36
  }
73
37
 
74
38
  export { RedisPublisher };
@@ -1,9 +1,10 @@
1
1
  import { RedisClientType } from 'redis';
2
- import { Publisher, PublisherOptions, PublisherSubscribeListenerOptions } from '../index.js';
3
- import { RPCSerializer } from '@orpc/client';
2
+ import { BaseRedisPublisher, BaseRedisPublisherOptions, RedisStreamEntry } from './base-redis.js';
4
3
  import '@orpc/shared';
4
+ import '../index.js';
5
+ import '@orpc/client';
5
6
 
6
- interface RedisPublisherOptions extends PublisherOptions {
7
+ interface RedisPublisherOptions extends BaseRedisPublisherOptions {
7
8
  /**
8
9
  * Redis subscriber instance.
9
10
  * Pub/Sub takes over the connection, so a client with subscriptions
@@ -12,63 +13,26 @@ interface RedisPublisherOptions extends PublisherOptions {
12
13
  * @default redis.duplicate()
13
14
  */
14
15
  subscriber?: undefined | RedisClientType<any, any, any, any, any>;
15
- /**
16
- * The prefix to use for Redis keys.
17
- */
18
- prefix?: string;
19
- /**
20
- * Serializer for serialize and deserialize payloads.
21
- *
22
- * @default RPCSerializer
23
- */
24
- serializer?: undefined | Pick<RPCSerializer, keyof RPCSerializer>;
25
- /**
26
- * Configuration for event replay support.
27
- *
28
- * When enabled, published events are temporarily stored so new
29
- * subscribers can resume from a previous position using `lastEventId`.
30
- *
31
- * @default { enabled: false }
32
- */
33
- replay?: {
34
- /**
35
- * Whether event replay support is enabled.
36
- *
37
- * When enabled, published events are temporarily stored so new
38
- * subscribers can resume from a previous position using `lastEventId`.
39
- *
40
- * @default false
41
- */
42
- enabled: boolean;
43
- /**
44
- * How long (in seconds) to retain events for replay.
45
- *
46
- * Expired events are cleaned up lazily for performance reasons, so
47
- * some events may remain available slightly longer than this period.
48
- *
49
- * @default 300 (5 min)
50
- */
51
- seconds?: number;
52
- };
53
16
  }
54
- declare class RedisPublisher<T extends Record<string, object>> extends Publisher<T> {
17
+ /**
18
+ * Publisher adapter for Redis. Distributes events across processes via
19
+ * Redis Pub/Sub, with optional resume support backed by Redis Streams.
20
+ *
21
+ * @see {@link https://orpc.dev/docs/helpers/publisher#adapters | Publisher Helpers - Adapters}
22
+ */
23
+ declare class RedisPublisher<T extends Record<string, object>> extends BaseRedisPublisher<T> {
55
24
  private readonly redis;
56
25
  private readonly subscriber;
57
- private readonly prefix;
58
- private readonly serializer;
59
- private readonly replayEnabled;
60
- private readonly replaySeconds;
61
26
  /**
62
- * The exactness of the `XTRIM` command.
63
- * Used for testing purpose.
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.
64
29
  */
65
- private readonly xTrimExactness;
66
- constructor(redis: RedisClientType<any, any, any, any, any>, options?: RedisPublisherOptions);
67
- private readonly firstPublishTimeMap;
68
- publish<K extends keyof T & string>(event: K, payload: T[K]): Promise<void>;
69
- protected subscribeListener<K extends keyof T & string>(event: K, originalListener: (payload: T[K]) => void, { lastEventId, onError }?: PublisherSubscribeListenerOptions): Promise<() => Promise<void>>;
70
- private serializePayload;
71
- private deserializePayload;
30
+ private readonly channelPrefix;
31
+ constructor(redis: RedisClientType<any, any, any, any, any>, { subscriber, ...options }?: RedisPublisherOptions);
32
+ protected publishMessage(channel: string, message: string): Promise<void>;
33
+ protected subscribeChannel(channel: string, listener: (message: unknown) => void): Promise<() => Promise<void>>;
34
+ protected evalScript(script: string, keys: string[], args: string[]): Promise<unknown>;
35
+ protected readStreamEntries(key: string, lastId: string): Promise<RedisStreamEntry[]>;
72
36
  }
73
37
 
74
38
  export { RedisPublisher };
@@ -1,124 +1,52 @@
1
- import { RPCSerializer } from '@orpc/client';
2
- import { stringifyJSON, parseEmptyableJSON, once } from '@orpc/shared';
3
- import { unwrapEvent, withEventMeta, getEventMeta } from '@standardserver/core';
4
- import { P as Publisher } from '../shared/publisher.BSQf8ybR.mjs';
1
+ import { BaseRedisPublisher } from './base-redis.mjs';
2
+ import '@orpc/client';
3
+ import '@orpc/shared';
4
+ import '@standard-server/core';
5
+ import '../shared/publisher.DcPG7zl6.mjs';
5
6
 
6
- class RedisPublisher extends Publisher {
7
- constructor(redis, options = {}) {
7
+ class RedisPublisher extends BaseRedisPublisher {
8
+ constructor(redis, { subscriber, ...options } = {}) {
8
9
  super(options);
9
10
  this.redis = redis;
10
- this.prefix = options.prefix ?? "";
11
- this.serializer = options.serializer ?? new RPCSerializer();
12
- this.replayEnabled = options.replay?.enabled ?? false;
13
- this.replaySeconds = options.replay?.seconds ?? 300;
14
- this.subscriber = options.subscriber ?? redis.duplicate();
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
+ }
15
+ this.subscriber = subscriber ?? redis.duplicate();
16
+ this.channelPrefix = keyPrefix ?? "";
15
17
  }
16
18
  subscriber;
17
- prefix;
18
- serializer;
19
- replayEnabled;
20
- replaySeconds;
21
19
  /**
22
- * The exactness of the `XTRIM` command.
23
- * Used for testing purpose.
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.
24
22
  */
25
- xTrimExactness = "~";
26
- firstPublishTimeMap = /* @__PURE__ */ new Map();
27
- async publish(event, payload) {
28
- const redisKey = `${this.prefix}${event}`;
29
- const data = this.serializePayload(payload);
30
- let id;
31
- if (!this.redis.isOpen) {
32
- await this.redis.connect();
33
- }
34
- if (this.replayEnabled) {
35
- const now = Date.now();
36
- for (const [event2, firstPublishTime] of this.firstPublishTimeMap) {
37
- if (firstPublishTime + this.replaySeconds * 1e3 < now) {
38
- this.firstPublishTimeMap.delete(event2);
39
- }
40
- }
41
- if (!this.firstPublishTimeMap.has(event)) {
42
- this.firstPublishTimeMap.set(event, now);
43
- const result = await this.redis.multi().xAdd(redisKey, "*", { data: stringifyJSON(data) }).xTrim(redisKey, "MINID", `${now - this.replaySeconds * 1e3}-0`, { strategyModifier: this.xTrimExactness }).expire(redisKey, this.replaySeconds * 2).exec();
44
- id = result[0];
45
- } else {
46
- id = await this.redis.xAdd(redisKey, "*", { data: stringifyJSON(data) });
47
- }
48
- }
49
- await this.redis.publish(redisKey, stringifyJSON({ data, id }));
23
+ channelPrefix;
24
+ async publishMessage(channel, message) {
25
+ await connectIfNeeded(this.redis);
26
+ await this.redis.publish(`${this.channelPrefix}${channel}`, message);
50
27
  }
51
- async subscribeListener(event, originalListener, { lastEventId, onError } = {}) {
52
- const redisKey = `${this.prefix}${event}`;
53
- if (!this.subscriber.isOpen) {
54
- await this.subscriber.connect();
55
- }
56
- let pendingPayloads = [];
57
- const replayedIds = /* @__PURE__ */ new Set();
58
- const deduplicatingListener = (payload) => {
59
- if (pendingPayloads) {
60
- pendingPayloads.push(payload);
61
- return;
62
- }
63
- const id = getEventMeta(payload)?.id;
64
- if (id !== void 0 && replayedIds.has(id)) {
65
- return;
66
- }
67
- originalListener(payload);
28
+ async subscribeChannel(channel, listener) {
29
+ const prefixedChannel = `${this.channelPrefix}${channel}`;
30
+ await connectIfNeeded(this.subscriber);
31
+ await this.subscriber.subscribe(prefixedChannel, listener);
32
+ return async () => {
33
+ await this.subscriber.unsubscribe(prefixedChannel, listener);
68
34
  };
69
- const redisListener = (message) => {
70
- try {
71
- const { id, data } = parseEmptyableJSON(message);
72
- const payload = this.deserializePayload(id, data);
73
- deduplicatingListener(payload);
74
- } catch (error) {
75
- onError?.(error);
76
- }
77
- };
78
- try {
79
- const subscribePromise = this.subscriber.subscribe(redisKey, redisListener);
80
- try {
81
- if (this.replayEnabled && lastEventId !== void 0) {
82
- if (!this.redis.isOpen) {
83
- await this.redis.connect();
84
- }
85
- const results = await this.redis.xRead({ key: redisKey, id: lastEventId });
86
- if (results && results[0]) {
87
- const { messages } = results[0];
88
- for (const { id, message } of messages) {
89
- const rawData = message.data;
90
- const data = parseEmptyableJSON(rawData);
91
- const payload = this.deserializePayload(id, data);
92
- replayedIds.add(id);
93
- originalListener(payload);
94
- }
95
- }
96
- }
97
- } finally {
98
- const pending = pendingPayloads;
99
- pendingPayloads = void 0;
100
- for (const payload of pending) {
101
- deduplicatingListener(payload);
102
- }
103
- }
104
- await subscribePromise;
105
- } catch (error) {
106
- await this.subscriber.unsubscribe(redisKey, redisListener);
107
- throw error;
108
- }
109
- return once(async () => {
110
- await this.subscriber.unsubscribe(redisKey, redisListener);
111
- });
112
35
  }
113
- serializePayload(payload) {
114
- const [original, meta] = unwrapEvent(payload);
115
- return { payload: this.serializer.serialize(original, { useFormDataForBlobFields: false }), meta };
36
+ async evalScript(script, keys, args) {
37
+ await connectIfNeeded(this.redis);
38
+ return await this.redis.eval(script, { keys, arguments: args });
116
39
  }
117
- deserializePayload(id, { payload, meta }) {
118
- return withEventMeta(
119
- this.serializer.deserialize(payload),
120
- id === void 0 ? { ...meta } : { ...meta, id }
121
- );
40
+ async readStreamEntries(key, lastId) {
41
+ await connectIfNeeded(this.redis);
42
+ const results = await this.redis.xRead({ key, id: lastId });
43
+ const entries = results?.[0]?.messages ?? [];
44
+ return entries.map(({ id, message }) => ({ id, data: message.data }));
45
+ }
46
+ }
47
+ async function connectIfNeeded(client) {
48
+ if (!client.isOpen) {
49
+ await client.connect();
122
50
  }
123
51
  }
124
52