-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathredis.ts
More file actions
142 lines (120 loc) · 4.13 KB
/
Copy pathredis.ts
File metadata and controls
142 lines (120 loc) · 4.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
/**
* @boringnode/bus
*
* @license MIT
* @copyright BoringNode
*/
import { Redis, Cluster } from 'ioredis'
import { assert } from '@poppinss/utils/assert'
import debug from '../debug.js'
import { JsonEncoder } from '../encoders/json_encoder.js'
import type {
Transport,
TransportEncoder,
Serializable,
SubscribeHandler,
RedisTransportConfig,
RedisTransportOptions,
} from '../types/main.js'
type Handler = (message: Buffer | string) => void | Promise<void>
export function redis(config: RedisTransportConfig, encoder?: TransportEncoder) {
return () => new RedisTransport(config, encoder)
}
export class RedisTransport implements Transport {
readonly #publisher: Redis | Cluster
readonly #subscriber: Redis | Cluster
readonly #encoder: TransportEncoder
readonly #useMessageBuffer: boolean = false
readonly #handlers = new Map<string, Set<Handler>>()
#id: string | undefined
constructor(path: string, encoder?: TransportEncoder)
constructor(options: RedisTransportConfig, encoder?: TransportEncoder)
constructor(
connection: Redis | Cluster,
encoder?: TransportEncoder,
options?: RedisTransportOptions
)
constructor(
options: RedisTransportConfig | string | Redis | Cluster,
encoder?: TransportEncoder,
transportOptions?: RedisTransportOptions
) {
this.#encoder = encoder ?? new JsonEncoder()
/**
* If an existing Redis or Cluster instance is passed, we duplicate it
* to have separate connections for publisher and subscriber
*/
if (options instanceof Redis || options instanceof Cluster) {
this.#publisher = options.duplicate()
this.#subscriber = options.duplicate()
this.#useMessageBuffer = transportOptions?.useMessageBuffer ?? false
this.#setupSubscriber()
return
}
// @ts-expect-error - merged definitions of overloaded constructor is not public
this.#publisher = new Redis(options)
// @ts-expect-error - merged definitions of overloaded constructor is not public
this.#subscriber = new Redis(options)
if (typeof options === 'object') {
this.#useMessageBuffer = options.useMessageBuffer ?? false
}
this.#setupSubscriber()
}
#setupSubscriber = () => {
const event = this.#useMessageBuffer ? 'messageBuffer' : 'message'
this.#subscriber.on(event, this.#onMessage)
}
#onMessage = async (receivedChannel: Buffer | string, message: Buffer | string) => {
const channel = receivedChannel.toString()
const handlers = this.#handlers.get(channel)
debug('received message for channel "%s"', channel)
if (!handlers || handlers.size === 0) {
debug('no handlers for channel "%s"', channel)
return
}
for (const handler of handlers) {
await handler(message)
}
}
#makeHandler = <T extends Serializable>(handler: SubscribeHandler<T>) => {
return async (message: Buffer | string) => {
const data = this.#encoder.decode<T>(message)
if (data.busId === this.#id) {
debug('ignoring message published by the same bus instance')
return
}
await handler(data.payload)
}
}
setId(id: string): Transport {
this.#id = id
return this
}
async disconnect(): Promise<void> {
await Promise.all([this.#publisher.quit(), this.#subscriber.quit()])
}
async publish(channel: string, message: Serializable): Promise<number> {
assert(this.#id, 'You must set an id before publishing a message')
const encoded = this.#encoder.encode({ payload: message, busId: this.#id })
return await this.#publisher.publish(channel, encoded)
}
async subscribe<T extends Serializable>(
channel: string,
handler: SubscribeHandler<T>
): Promise<void> {
let handlers = this.#handlers.get(channel)
if (!handlers) {
handlers = new Set()
this.#handlers.set(channel, handlers)
await this.#subscriber.subscribe(channel)
}
handlers.add(this.#makeHandler(handler))
}
onReconnect(callback: () => void): void {
this.#subscriber.on('reconnecting', callback)
}
async unsubscribe(channel: string): Promise<void> {
this.#handlers.delete(channel)
await this.#subscriber.unsubscribe(channel)
}
}