@mono-labs/dev 0.1.251 → 0.1.256
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/dist/cache-relay.d.ts +162 -0
- package/dist/cache-relay.d.ts.map +1 -0
- package/dist/cache-relay.js +302 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -3
- package/dist/local-server/index.d.ts.map +1 -1
- package/dist/local-server/index.js +7 -1
- package/dist/local-server/types.d.ts +2 -0
- package/dist/local-server/types.d.ts.map +1 -1
- package/dist/websocket/channel-store.d.ts +28 -0
- package/dist/websocket/channel-store.d.ts.map +1 -0
- package/dist/websocket/channel-store.js +91 -0
- package/dist/websocket/index.d.ts +9 -2
- package/dist/websocket/index.d.ts.map +1 -1
- package/dist/websocket/index.js +31 -5
- package/dist/websocket/socket-emitter.d.ts +25 -0
- package/dist/websocket/socket-emitter.d.ts.map +1 -0
- package/dist/websocket/socket-emitter.js +49 -0
- package/dist/websocket/socket-gateway-client.d.ts +20 -0
- package/dist/websocket/socket-gateway-client.d.ts.map +1 -0
- package/dist/websocket/socket-gateway-client.js +76 -0
- package/dist/websocket/types.d.ts +9 -0
- package/dist/websocket/types.d.ts.map +1 -1
- package/package.json +13 -1
- package/src/cache-relay.ts +490 -0
- package/src/index.ts +25 -1
- package/src/local-server/index.ts +7 -1
- package/src/local-server/types.ts +2 -0
- package/src/websocket/channel-store.ts +116 -0
- package/src/websocket/index.ts +30 -4
- package/src/websocket/socket-emitter.ts +64 -0
- package/src/websocket/socket-gateway-client.ts +83 -0
- package/src/websocket/types.ts +10 -0
- package/src/websocket/local-gateway-client.ts +0 -31
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// CacheRelay — full Redis abstraction over ioredis
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
|
+
type RedisClient = any
|
|
7
|
+
|
|
8
|
+
// ---- Namespace interfaces --------------------------------------------------
|
|
9
|
+
|
|
10
|
+
export interface StringOps {
|
|
11
|
+
get(key: string): Promise<string | null>
|
|
12
|
+
set(key: string, value: string, ttlSeconds?: number): Promise<void>
|
|
13
|
+
mget(...keys: string[]): Promise<(string | null)[]>
|
|
14
|
+
mset(pairs: Record<string, string>): Promise<void>
|
|
15
|
+
incr(key: string): Promise<number>
|
|
16
|
+
incrby(key: string, increment: number): Promise<number>
|
|
17
|
+
decr(key: string): Promise<number>
|
|
18
|
+
decrby(key: string, decrement: number): Promise<number>
|
|
19
|
+
append(key: string, value: string): Promise<number>
|
|
20
|
+
strlen(key: string): Promise<number>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface HashOps {
|
|
24
|
+
get(key: string, field: string): Promise<string | null>
|
|
25
|
+
set(key: string, field: string, value: string): Promise<void>
|
|
26
|
+
getAll(key: string): Promise<Record<string, string>>
|
|
27
|
+
del(key: string, ...fields: string[]): Promise<number>
|
|
28
|
+
exists(key: string, field: string): Promise<boolean>
|
|
29
|
+
keys(key: string): Promise<string[]>
|
|
30
|
+
vals(key: string): Promise<string[]>
|
|
31
|
+
len(key: string): Promise<number>
|
|
32
|
+
mset(key: string, pairs: Record<string, string>): Promise<void>
|
|
33
|
+
mget(key: string, ...fields: string[]): Promise<(string | null)[]>
|
|
34
|
+
incrby(key: string, field: string, increment: number): Promise<number>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ListOps {
|
|
38
|
+
push(key: string, ...values: string[]): Promise<number>
|
|
39
|
+
lpush(key: string, ...values: string[]): Promise<number>
|
|
40
|
+
pop(key: string): Promise<string | null>
|
|
41
|
+
lpop(key: string): Promise<string | null>
|
|
42
|
+
range(key: string, start: number, stop: number): Promise<string[]>
|
|
43
|
+
len(key: string): Promise<number>
|
|
44
|
+
trim(key: string, start: number, stop: number): Promise<void>
|
|
45
|
+
index(key: string, index: number): Promise<string | null>
|
|
46
|
+
set(key: string, index: number, value: string): Promise<void>
|
|
47
|
+
rem(key: string, count: number, value: string): Promise<number>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SetOps {
|
|
51
|
+
add(key: string, ...members: string[]): Promise<number>
|
|
52
|
+
rem(key: string, ...members: string[]): Promise<number>
|
|
53
|
+
members(key: string): Promise<string[]>
|
|
54
|
+
isMember(key: string, member: string): Promise<boolean>
|
|
55
|
+
card(key: string): Promise<number>
|
|
56
|
+
union(...keys: string[]): Promise<string[]>
|
|
57
|
+
inter(...keys: string[]): Promise<string[]>
|
|
58
|
+
diff(...keys: string[]): Promise<string[]>
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface SortedSetOps {
|
|
62
|
+
add(key: string, score: number, member: string): Promise<number>
|
|
63
|
+
rem(key: string, ...members: string[]): Promise<number>
|
|
64
|
+
range(key: string, start: number, stop: number): Promise<string[]>
|
|
65
|
+
rangeWithScores(key: string, start: number, stop: number): Promise<{ member: string; score: number }[]>
|
|
66
|
+
rangeByScore(key: string, min: number | string, max: number | string): Promise<string[]>
|
|
67
|
+
revRange(key: string, start: number, stop: number): Promise<string[]>
|
|
68
|
+
score(key: string, member: string): Promise<number | null>
|
|
69
|
+
rank(key: string, member: string): Promise<number | null>
|
|
70
|
+
card(key: string): Promise<number>
|
|
71
|
+
incrby(key: string, increment: number, member: string): Promise<number>
|
|
72
|
+
remRangeByRank(key: string, start: number, stop: number): Promise<number>
|
|
73
|
+
remRangeByScore(key: string, min: number | string, max: number | string): Promise<number>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface KeyOps {
|
|
77
|
+
exists(...keys: string[]): Promise<number>
|
|
78
|
+
expire(key: string, seconds: number): Promise<boolean>
|
|
79
|
+
ttl(key: string): Promise<number>
|
|
80
|
+
pttl(key: string): Promise<number>
|
|
81
|
+
persist(key: string): Promise<boolean>
|
|
82
|
+
rename(key: string, newKey: string): Promise<void>
|
|
83
|
+
type(key: string): Promise<string>
|
|
84
|
+
scan(cursor: number, options?: { match?: string; count?: number }): Promise<{ cursor: number; keys: string[] }>
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface PubSubOps {
|
|
88
|
+
publish(channel: string, message: string): Promise<number>
|
|
89
|
+
subscribe(channel: string, callback: (message: string, channel: string) => void): Promise<void>
|
|
90
|
+
unsubscribe(channel: string): Promise<void>
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface TransactionOps {
|
|
94
|
+
multi(): RedisClient
|
|
95
|
+
exec(pipeline: RedisClient): Promise<unknown[]>
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ScriptOps {
|
|
99
|
+
eval(script: string, keys: string[], args: string[]): Promise<unknown>
|
|
100
|
+
evalsha(sha: string, keys: string[], args: string[]): Promise<unknown>
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface GeoOps {
|
|
104
|
+
add(key: string, longitude: number, latitude: number, member: string): Promise<number>
|
|
105
|
+
pos(key: string, ...members: string[]): Promise<([number, number] | null)[]>
|
|
106
|
+
dist(key: string, member1: string, member2: string, unit?: string): Promise<string | null>
|
|
107
|
+
radius(key: string, longitude: number, latitude: number, radius: number, unit: string): Promise<string[]>
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface HyperLogLogOps {
|
|
111
|
+
add(key: string, ...elements: string[]): Promise<number>
|
|
112
|
+
count(...keys: string[]): Promise<number>
|
|
113
|
+
merge(destKey: string, ...sourceKeys: string[]): Promise<void>
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface BitmapOps {
|
|
117
|
+
setbit(key: string, offset: number, value: 0 | 1): Promise<number>
|
|
118
|
+
getbit(key: string, offset: number): Promise<number>
|
|
119
|
+
count(key: string, start?: number, end?: number): Promise<number>
|
|
120
|
+
op(operation: 'AND' | 'OR' | 'XOR' | 'NOT', destKey: string, ...keys: string[]): Promise<number>
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface StreamOps {
|
|
124
|
+
add(key: string, id: string, fields: Record<string, string>): Promise<string>
|
|
125
|
+
read(key: string, id: string, count?: number): Promise<unknown[]>
|
|
126
|
+
len(key: string): Promise<number>
|
|
127
|
+
range(key: string, start: string, end: string, count?: number): Promise<unknown[]>
|
|
128
|
+
trim(key: string, maxLen: number): Promise<number>
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---- CacheRelay type -------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
export interface CacheRelay {
|
|
134
|
+
/** Get a raw string value */
|
|
135
|
+
get(key: string): Promise<string | null>
|
|
136
|
+
/** Get and JSON-parse a value */
|
|
137
|
+
gett<T = unknown>(key: string): Promise<T | null>
|
|
138
|
+
/** Set a value (objects are JSON-stringified) */
|
|
139
|
+
set(key: string, value: unknown, options?: { ttlSeconds?: number }): Promise<void>
|
|
140
|
+
/** Delete one or more keys */
|
|
141
|
+
del(...keys: string[]): Promise<number>
|
|
142
|
+
|
|
143
|
+
/** Raw ioredis client for advanced usage */
|
|
144
|
+
raw: RedisClient
|
|
145
|
+
|
|
146
|
+
strings: StringOps
|
|
147
|
+
hashes: HashOps
|
|
148
|
+
lists: ListOps
|
|
149
|
+
sets: SetOps
|
|
150
|
+
sortedSets: SortedSetOps
|
|
151
|
+
keys: KeyOps
|
|
152
|
+
pubsub: PubSubOps
|
|
153
|
+
transactions: TransactionOps
|
|
154
|
+
scripts: ScriptOps
|
|
155
|
+
geo: GeoOps
|
|
156
|
+
hyperloglog: HyperLogLogOps
|
|
157
|
+
bitmaps: BitmapOps
|
|
158
|
+
streams: StreamOps
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---- Singleton state -------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
let _cacheRelay: CacheRelay | undefined
|
|
164
|
+
let _currentConnectionString: string | undefined
|
|
165
|
+
|
|
166
|
+
// ---- Builder ---------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
function buildCacheRelay(redis: RedisClient): CacheRelay {
|
|
169
|
+
const relay: CacheRelay = {
|
|
170
|
+
raw: redis,
|
|
171
|
+
|
|
172
|
+
// -- Top-level convenience ------------------------------------------------
|
|
173
|
+
|
|
174
|
+
async get(key: string) {
|
|
175
|
+
return redis.get(key)
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
async gett<T = unknown>(key: string): Promise<T | null> {
|
|
179
|
+
const raw = await redis.get(key)
|
|
180
|
+
if (raw === null || raw === undefined) return null
|
|
181
|
+
try {
|
|
182
|
+
return JSON.parse(raw) as T
|
|
183
|
+
} catch {
|
|
184
|
+
return null
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
async set(key: string, value: unknown, options?: { ttlSeconds?: number }) {
|
|
189
|
+
const serialized = typeof value === 'string' ? value : JSON.stringify(value)
|
|
190
|
+
if (options?.ttlSeconds) {
|
|
191
|
+
await redis.set(key, serialized, 'EX', options.ttlSeconds)
|
|
192
|
+
} else {
|
|
193
|
+
await redis.set(key, serialized)
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
async del(...keys: string[]) {
|
|
198
|
+
if (keys.length === 0) return 0
|
|
199
|
+
return redis.del(...keys)
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
// -- Strings --------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
strings: {
|
|
205
|
+
get: (key) => redis.get(key),
|
|
206
|
+
async set(key, value, ttlSeconds?) {
|
|
207
|
+
if (ttlSeconds) {
|
|
208
|
+
await redis.set(key, value, 'EX', ttlSeconds)
|
|
209
|
+
} else {
|
|
210
|
+
await redis.set(key, value)
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
mget: (...keys) => redis.mget(...keys),
|
|
214
|
+
async mset(pairs) {
|
|
215
|
+
const args: string[] = []
|
|
216
|
+
for (const [k, v] of Object.entries(pairs)) {
|
|
217
|
+
args.push(k, v)
|
|
218
|
+
}
|
|
219
|
+
await redis.mset(...args)
|
|
220
|
+
},
|
|
221
|
+
incr: (key) => redis.incr(key),
|
|
222
|
+
incrby: (key, inc) => redis.incrby(key, inc),
|
|
223
|
+
decr: (key) => redis.decr(key),
|
|
224
|
+
decrby: (key, dec) => redis.decrby(key, dec),
|
|
225
|
+
append: (key, value) => redis.append(key, value),
|
|
226
|
+
strlen: (key) => redis.strlen(key),
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
// -- Hashes ---------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
hashes: {
|
|
232
|
+
get: (key, field) => redis.hget(key, field),
|
|
233
|
+
async set(key, field, value) {
|
|
234
|
+
await redis.hset(key, field, value)
|
|
235
|
+
},
|
|
236
|
+
async getAll(key) {
|
|
237
|
+
return redis.hgetall(key)
|
|
238
|
+
},
|
|
239
|
+
del: (key, ...fields) => redis.hdel(key, ...fields),
|
|
240
|
+
async exists(key, field) {
|
|
241
|
+
return (await redis.hexists(key, field)) === 1
|
|
242
|
+
},
|
|
243
|
+
keys: (key) => redis.hkeys(key),
|
|
244
|
+
vals: (key) => redis.hvals(key),
|
|
245
|
+
len: (key) => redis.hlen(key),
|
|
246
|
+
async mset(key, pairs) {
|
|
247
|
+
const args: string[] = []
|
|
248
|
+
for (const [f, v] of Object.entries(pairs)) {
|
|
249
|
+
args.push(f, v)
|
|
250
|
+
}
|
|
251
|
+
await redis.hmset(key, ...args)
|
|
252
|
+
},
|
|
253
|
+
mget: (key, ...fields) => redis.hmget(key, ...fields),
|
|
254
|
+
incrby: (key, field, inc) => redis.hincrby(key, field, inc),
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
// -- Lists ----------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
lists: {
|
|
260
|
+
push: (key, ...values) => redis.rpush(key, ...values),
|
|
261
|
+
lpush: (key, ...values) => redis.lpush(key, ...values),
|
|
262
|
+
pop: (key) => redis.rpop(key),
|
|
263
|
+
lpop: (key) => redis.lpop(key),
|
|
264
|
+
range: (key, start, stop) => redis.lrange(key, start, stop),
|
|
265
|
+
len: (key) => redis.llen(key),
|
|
266
|
+
async trim(key, start, stop) {
|
|
267
|
+
await redis.ltrim(key, start, stop)
|
|
268
|
+
},
|
|
269
|
+
index: (key, idx) => redis.lindex(key, idx),
|
|
270
|
+
async set(key, idx, value) {
|
|
271
|
+
await redis.lset(key, idx, value)
|
|
272
|
+
},
|
|
273
|
+
rem: (key, count, value) => redis.lrem(key, count, value),
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
// -- Sets -----------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
sets: {
|
|
279
|
+
add: (key, ...members) => redis.sadd(key, ...members),
|
|
280
|
+
rem: (key, ...members) => redis.srem(key, ...members),
|
|
281
|
+
members: (key) => redis.smembers(key),
|
|
282
|
+
async isMember(key, member) {
|
|
283
|
+
return (await redis.sismember(key, member)) === 1
|
|
284
|
+
},
|
|
285
|
+
card: (key) => redis.scard(key),
|
|
286
|
+
union: (...keys) => redis.sunion(...keys),
|
|
287
|
+
inter: (...keys) => redis.sinter(...keys),
|
|
288
|
+
diff: (...keys) => redis.sdiff(...keys),
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
// -- Sorted Sets ----------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
sortedSets: {
|
|
294
|
+
add: (key, score, member) => redis.zadd(key, score, member),
|
|
295
|
+
rem: (key, ...members) => redis.zrem(key, ...members),
|
|
296
|
+
range: (key, start, stop) => redis.zrange(key, start, stop),
|
|
297
|
+
async rangeWithScores(key, start, stop) {
|
|
298
|
+
const raw: string[] = await redis.zrange(key, start, stop, 'WITHSCORES')
|
|
299
|
+
const result: { member: string; score: number }[] = []
|
|
300
|
+
for (let i = 0; i < raw.length; i += 2) {
|
|
301
|
+
result.push({ member: raw[i], score: parseFloat(raw[i + 1]) })
|
|
302
|
+
}
|
|
303
|
+
return result
|
|
304
|
+
},
|
|
305
|
+
rangeByScore: (key, min, max) => redis.zrangebyscore(key, min, max),
|
|
306
|
+
revRange: (key, start, stop) => redis.zrevrange(key, start, stop),
|
|
307
|
+
async score(key, member) {
|
|
308
|
+
const s = await redis.zscore(key, member)
|
|
309
|
+
return s === null ? null : parseFloat(s)
|
|
310
|
+
},
|
|
311
|
+
rank: (key, member) => redis.zrank(key, member),
|
|
312
|
+
card: (key) => redis.zcard(key),
|
|
313
|
+
incrby: (key, inc, member) => redis.zincrby(key, inc, member),
|
|
314
|
+
remRangeByRank: (key, start, stop) => redis.zremrangebyrank(key, start, stop),
|
|
315
|
+
remRangeByScore: (key, min, max) => redis.zremrangebyscore(key, min, max),
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
// -- Keys -----------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
keys: {
|
|
321
|
+
exists: (...keys) => redis.exists(...keys),
|
|
322
|
+
async expire(key, seconds) {
|
|
323
|
+
return (await redis.expire(key, seconds)) === 1
|
|
324
|
+
},
|
|
325
|
+
ttl: (key) => redis.ttl(key),
|
|
326
|
+
pttl: (key) => redis.pttl(key),
|
|
327
|
+
async persist(key) {
|
|
328
|
+
return (await redis.persist(key)) === 1
|
|
329
|
+
},
|
|
330
|
+
async rename(key, newKey) {
|
|
331
|
+
await redis.rename(key, newKey)
|
|
332
|
+
},
|
|
333
|
+
type: (key) => redis.type(key),
|
|
334
|
+
async scan(cursor, options?) {
|
|
335
|
+
const args: (string | number)[] = [cursor]
|
|
336
|
+
if (options?.match) args.push('MATCH', options.match)
|
|
337
|
+
if (options?.count) args.push('COUNT', options.count)
|
|
338
|
+
const [nextCursor, keys] = await redis.scan(...args)
|
|
339
|
+
return { cursor: parseInt(nextCursor, 10), keys }
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
// -- Pub/Sub --------------------------------------------------------------
|
|
344
|
+
|
|
345
|
+
pubsub: {
|
|
346
|
+
publish: (channel, message) => redis.publish(channel, message),
|
|
347
|
+
async subscribe(channel, callback) {
|
|
348
|
+
const sub = redis.duplicate()
|
|
349
|
+
await sub.subscribe(channel)
|
|
350
|
+
sub.on('message', (ch: string, msg: string) => {
|
|
351
|
+
if (ch === channel) callback(msg, ch)
|
|
352
|
+
})
|
|
353
|
+
},
|
|
354
|
+
async unsubscribe(channel) {
|
|
355
|
+
await redis.unsubscribe(channel)
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
// -- Transactions ---------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
transactions: {
|
|
362
|
+
multi() {
|
|
363
|
+
return redis.multi()
|
|
364
|
+
},
|
|
365
|
+
async exec(pipeline) {
|
|
366
|
+
return pipeline.exec()
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
|
|
370
|
+
// -- Scripts --------------------------------------------------------------
|
|
371
|
+
|
|
372
|
+
scripts: {
|
|
373
|
+
eval: (script, keys, args) =>
|
|
374
|
+
redis.eval(script, keys.length, ...keys, ...args),
|
|
375
|
+
evalsha: (sha, keys, args) =>
|
|
376
|
+
redis.evalsha(sha, keys.length, ...keys, ...args),
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
// -- Geo ------------------------------------------------------------------
|
|
380
|
+
|
|
381
|
+
geo: {
|
|
382
|
+
add: (key, lon, lat, member) => redis.geoadd(key, lon, lat, member),
|
|
383
|
+
pos: (key, ...members) => redis.geopos(key, ...members),
|
|
384
|
+
dist: (key, m1, m2, unit?) => {
|
|
385
|
+
if (unit) return redis.geodist(key, m1, m2, unit)
|
|
386
|
+
return redis.geodist(key, m1, m2)
|
|
387
|
+
},
|
|
388
|
+
radius: (key, lon, lat, r, unit) =>
|
|
389
|
+
redis.georadius(key, lon, lat, r, unit),
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
// -- HyperLogLog ----------------------------------------------------------
|
|
393
|
+
|
|
394
|
+
hyperloglog: {
|
|
395
|
+
add: (key, ...elements) => redis.pfadd(key, ...elements),
|
|
396
|
+
count: (...keys) => redis.pfcount(...keys),
|
|
397
|
+
async merge(destKey, ...sourceKeys) {
|
|
398
|
+
await redis.pfmerge(destKey, ...sourceKeys)
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
|
|
402
|
+
// -- Bitmaps --------------------------------------------------------------
|
|
403
|
+
|
|
404
|
+
bitmaps: {
|
|
405
|
+
setbit: (key, offset, value) => redis.setbit(key, offset, value),
|
|
406
|
+
getbit: (key, offset) => redis.getbit(key, offset),
|
|
407
|
+
count(key, start?, end?) {
|
|
408
|
+
if (start !== undefined && end !== undefined) {
|
|
409
|
+
return redis.bitcount(key, start, end)
|
|
410
|
+
}
|
|
411
|
+
return redis.bitcount(key)
|
|
412
|
+
},
|
|
413
|
+
op: (operation, destKey, ...keys) =>
|
|
414
|
+
redis.bitop(operation, destKey, ...keys),
|
|
415
|
+
},
|
|
416
|
+
|
|
417
|
+
// -- Streams --------------------------------------------------------------
|
|
418
|
+
|
|
419
|
+
streams: {
|
|
420
|
+
add(key, id, fields) {
|
|
421
|
+
const args: string[] = []
|
|
422
|
+
for (const [f, v] of Object.entries(fields)) {
|
|
423
|
+
args.push(f, v)
|
|
424
|
+
}
|
|
425
|
+
return redis.xadd(key, id, ...args)
|
|
426
|
+
},
|
|
427
|
+
read(key, id, count?) {
|
|
428
|
+
if (count) return redis.xread('COUNT', count, 'STREAMS', key, id)
|
|
429
|
+
return redis.xread('STREAMS', key, id)
|
|
430
|
+
},
|
|
431
|
+
len: (key) => redis.xlen(key),
|
|
432
|
+
range(key, start, end, count?) {
|
|
433
|
+
if (count) return redis.xrange(key, start, end, 'COUNT', count)
|
|
434
|
+
return redis.xrange(key, start, end)
|
|
435
|
+
},
|
|
436
|
+
trim: (key, maxLen) => redis.xtrim(key, 'MAXLEN', maxLen),
|
|
437
|
+
},
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return relay
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ---- Public API ------------------------------------------------------------
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Initialize (or reinitialize) the CacheRelay singleton.
|
|
447
|
+
*
|
|
448
|
+
* @param connectionString — `host:port` (default `localhost:6379`)
|
|
449
|
+
* @returns the CacheRelay instance
|
|
450
|
+
*
|
|
451
|
+
* Only creates a new Redis connection when the connection string changes.
|
|
452
|
+
* Requires `ioredis` as a peer dependency — throws a clear error if missing.
|
|
453
|
+
*/
|
|
454
|
+
export function initCacheRelay(connectionString?: string): CacheRelay {
|
|
455
|
+
const raw = connectionString ?? 'localhost:6379'
|
|
456
|
+
|
|
457
|
+
// Normalize: bare hostname (no port) → hostname:6379
|
|
458
|
+
const connStr = raw.includes(':') ? raw : `${raw}:6379`
|
|
459
|
+
|
|
460
|
+
// Reuse existing instance when the connection string hasn't changed
|
|
461
|
+
if (_cacheRelay && _currentConnectionString === connStr) return _cacheRelay
|
|
462
|
+
|
|
463
|
+
// Dynamic require — gives a clear error when ioredis isn't installed
|
|
464
|
+
let Redis: new (port: number, host: string) => RedisClient
|
|
465
|
+
try {
|
|
466
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
467
|
+
Redis = require('ioredis')
|
|
468
|
+
} catch {
|
|
469
|
+
throw new Error(
|
|
470
|
+
'CacheRelay requires "ioredis" as a peer dependency. Install it with: npm install ioredis'
|
|
471
|
+
)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const [host, portStr] = connStr.split(':')
|
|
475
|
+
const port = parseInt(portStr ?? '6379', 10)
|
|
476
|
+
|
|
477
|
+
const redis = new Redis(port, host || 'localhost')
|
|
478
|
+
_cacheRelay = buildCacheRelay(redis)
|
|
479
|
+
_currentConnectionString = connStr
|
|
480
|
+
return _cacheRelay
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Get the current CacheRelay singleton.
|
|
485
|
+
* Lazily initializes with default settings (`localhost:6379`) if not yet created.
|
|
486
|
+
*/
|
|
487
|
+
export function getCacheRelay(): CacheRelay {
|
|
488
|
+
if (!_cacheRelay) return initCacheRelay()
|
|
489
|
+
return _cacheRelay
|
|
490
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -6,11 +6,14 @@ export type { ApiGatewayHandler, ALBHandler, LocalServerConfig } from './local-s
|
|
|
6
6
|
export { attachSocketAdapter } from './websocket'
|
|
7
7
|
export { ConnectionRegistry } from './websocket/connection-registry'
|
|
8
8
|
export { ActionRouter } from './websocket/action-router'
|
|
9
|
-
export {
|
|
9
|
+
export { SocketGatewayClient } from './websocket/socket-gateway-client'
|
|
10
|
+
export { InMemoryChannelStore, RedisChannelStore } from './websocket/channel-store'
|
|
11
|
+
export { SocketEmitter } from './websocket/socket-emitter'
|
|
10
12
|
export type {
|
|
11
13
|
ConnectionId,
|
|
12
14
|
PostToConnectionFn,
|
|
13
15
|
SocketAdapterConfig,
|
|
16
|
+
RedisConfig,
|
|
14
17
|
ConnectHandlerFn,
|
|
15
18
|
DisconnectHandlerFn,
|
|
16
19
|
ActionHandler,
|
|
@@ -19,3 +22,24 @@ export type {
|
|
|
19
22
|
LocalRequestContext,
|
|
20
23
|
WebSocketUserContext,
|
|
21
24
|
} from './websocket/types'
|
|
25
|
+
export type { ChannelStore } from './websocket/channel-store'
|
|
26
|
+
export type { EmitTarget } from './websocket/socket-emitter'
|
|
27
|
+
|
|
28
|
+
// CacheRelay
|
|
29
|
+
export { initCacheRelay, getCacheRelay } from './cache-relay'
|
|
30
|
+
export type {
|
|
31
|
+
CacheRelay,
|
|
32
|
+
StringOps,
|
|
33
|
+
HashOps,
|
|
34
|
+
ListOps,
|
|
35
|
+
SetOps,
|
|
36
|
+
SortedSetOps,
|
|
37
|
+
KeyOps,
|
|
38
|
+
PubSubOps,
|
|
39
|
+
TransactionOps,
|
|
40
|
+
ScriptOps,
|
|
41
|
+
GeoOps,
|
|
42
|
+
HyperLogLogOps,
|
|
43
|
+
BitmapOps,
|
|
44
|
+
StreamOps,
|
|
45
|
+
} from './cache-relay'
|
|
@@ -79,7 +79,13 @@ export class LocalServer {
|
|
|
79
79
|
config?: SocketAdapterConfig,
|
|
80
80
|
): ReturnType<typeof attachSocketAdapter> {
|
|
81
81
|
const wss = new WebSocketServer({ server: this.httpServer })
|
|
82
|
-
|
|
82
|
+
const mergedConfig: SocketAdapterConfig = {
|
|
83
|
+
...config,
|
|
84
|
+
useRedis: config?.useRedis ?? this.config.useRedis,
|
|
85
|
+
redis: config?.redis ?? this.config.redis,
|
|
86
|
+
debug: config?.debug ?? this.config.debug,
|
|
87
|
+
}
|
|
88
|
+
return adapterFn(wss, mergedConfig)
|
|
83
89
|
}
|
|
84
90
|
|
|
85
91
|
listen(port: number, hostname?: string): void {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { ConnectionId } from './types'
|
|
2
|
+
|
|
3
|
+
// ---- Interface -------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
export interface ChannelStore {
|
|
6
|
+
subscribe(connectionId: ConnectionId, channel: string): Promise<void>
|
|
7
|
+
unsubscribe(connectionId: ConnectionId, channel: string): Promise<void>
|
|
8
|
+
getSubscribers(channel: string): Promise<ConnectionId[]>
|
|
9
|
+
removeAll(connectionId: ConnectionId): Promise<void>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// ---- InMemoryChannelStore --------------------------------------------------
|
|
13
|
+
|
|
14
|
+
export class InMemoryChannelStore implements ChannelStore {
|
|
15
|
+
private channelToConnections = new Map<string, Set<ConnectionId>>()
|
|
16
|
+
private connectionToChannels = new Map<ConnectionId, Set<string>>()
|
|
17
|
+
|
|
18
|
+
async subscribe(connectionId: ConnectionId, channel: string): Promise<void> {
|
|
19
|
+
let conns = this.channelToConnections.get(channel)
|
|
20
|
+
if (!conns) {
|
|
21
|
+
conns = new Set()
|
|
22
|
+
this.channelToConnections.set(channel, conns)
|
|
23
|
+
}
|
|
24
|
+
conns.add(connectionId)
|
|
25
|
+
|
|
26
|
+
let channels = this.connectionToChannels.get(connectionId)
|
|
27
|
+
if (!channels) {
|
|
28
|
+
channels = new Set()
|
|
29
|
+
this.connectionToChannels.set(connectionId, channels)
|
|
30
|
+
}
|
|
31
|
+
channels.add(channel)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async unsubscribe(connectionId: ConnectionId, channel: string): Promise<void> {
|
|
35
|
+
const conns = this.channelToConnections.get(channel)
|
|
36
|
+
if (conns) {
|
|
37
|
+
conns.delete(connectionId)
|
|
38
|
+
if (conns.size === 0) this.channelToConnections.delete(channel)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const channels = this.connectionToChannels.get(connectionId)
|
|
42
|
+
if (channels) {
|
|
43
|
+
channels.delete(channel)
|
|
44
|
+
if (channels.size === 0) this.connectionToChannels.delete(connectionId)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async getSubscribers(channel: string): Promise<ConnectionId[]> {
|
|
49
|
+
const conns = this.channelToConnections.get(channel)
|
|
50
|
+
return conns ? Array.from(conns) : []
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async removeAll(connectionId: ConnectionId): Promise<void> {
|
|
54
|
+
const channels = this.connectionToChannels.get(connectionId)
|
|
55
|
+
if (!channels) return
|
|
56
|
+
|
|
57
|
+
for (const channel of channels) {
|
|
58
|
+
const conns = this.channelToConnections.get(channel)
|
|
59
|
+
if (conns) {
|
|
60
|
+
conns.delete(connectionId)
|
|
61
|
+
if (conns.size === 0) this.channelToConnections.delete(channel)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.connectionToChannels.delete(connectionId)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---- RedisChannelStore -----------------------------------------------------
|
|
70
|
+
|
|
71
|
+
export class RedisChannelStore implements ChannelStore {
|
|
72
|
+
private channelKeyPrefix: string
|
|
73
|
+
private connChannelsPrefix: string
|
|
74
|
+
|
|
75
|
+
constructor(options?: { keyPrefix?: string }) {
|
|
76
|
+
const prefix = options?.keyPrefix ?? 'ws:'
|
|
77
|
+
this.channelKeyPrefix = `${prefix}channel:`
|
|
78
|
+
this.connChannelsPrefix = `${prefix}conn-channels:`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private getRelay() {
|
|
82
|
+
// Lazy import to avoid circular dependency and to defer until Redis is initialized
|
|
83
|
+
const { getCacheRelay } = require('../cache-relay') as typeof import('../cache-relay')
|
|
84
|
+
return getCacheRelay()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async subscribe(connectionId: ConnectionId, channel: string): Promise<void> {
|
|
88
|
+
const relay = this.getRelay()
|
|
89
|
+
await relay.raw.sadd(`${this.channelKeyPrefix}${channel}`, connectionId)
|
|
90
|
+
await relay.raw.sadd(`${this.connChannelsPrefix}${connectionId}`, channel)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async unsubscribe(connectionId: ConnectionId, channel: string): Promise<void> {
|
|
94
|
+
const relay = this.getRelay()
|
|
95
|
+
await relay.raw.srem(`${this.channelKeyPrefix}${channel}`, connectionId)
|
|
96
|
+
await relay.raw.srem(`${this.connChannelsPrefix}${connectionId}`, channel)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async getSubscribers(channel: string): Promise<ConnectionId[]> {
|
|
100
|
+
const relay = this.getRelay()
|
|
101
|
+
return relay.raw.smembers(`${this.channelKeyPrefix}${channel}`)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async removeAll(connectionId: ConnectionId): Promise<void> {
|
|
105
|
+
const relay = this.getRelay()
|
|
106
|
+
const channels: string[] = await relay.raw.smembers(`${this.connChannelsPrefix}${connectionId}`)
|
|
107
|
+
|
|
108
|
+
await Promise.allSettled(
|
|
109
|
+
channels.map((channel) =>
|
|
110
|
+
relay.raw.srem(`${this.channelKeyPrefix}${channel}`, connectionId)
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
await relay.raw.del(`${this.connChannelsPrefix}${connectionId}`)
|
|
115
|
+
}
|
|
116
|
+
}
|