@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.
Files changed (35) hide show
  1. package/dist/cache-relay.d.ts +162 -0
  2. package/dist/cache-relay.d.ts.map +1 -0
  3. package/dist/cache-relay.js +302 -0
  4. package/dist/index.d.ts +8 -2
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +12 -3
  7. package/dist/local-server/index.d.ts.map +1 -1
  8. package/dist/local-server/index.js +7 -1
  9. package/dist/local-server/types.d.ts +2 -0
  10. package/dist/local-server/types.d.ts.map +1 -1
  11. package/dist/websocket/channel-store.d.ts +28 -0
  12. package/dist/websocket/channel-store.d.ts.map +1 -0
  13. package/dist/websocket/channel-store.js +91 -0
  14. package/dist/websocket/index.d.ts +9 -2
  15. package/dist/websocket/index.d.ts.map +1 -1
  16. package/dist/websocket/index.js +31 -5
  17. package/dist/websocket/socket-emitter.d.ts +25 -0
  18. package/dist/websocket/socket-emitter.d.ts.map +1 -0
  19. package/dist/websocket/socket-emitter.js +49 -0
  20. package/dist/websocket/socket-gateway-client.d.ts +20 -0
  21. package/dist/websocket/socket-gateway-client.d.ts.map +1 -0
  22. package/dist/websocket/socket-gateway-client.js +76 -0
  23. package/dist/websocket/types.d.ts +9 -0
  24. package/dist/websocket/types.d.ts.map +1 -1
  25. package/package.json +13 -1
  26. package/src/cache-relay.ts +490 -0
  27. package/src/index.ts +25 -1
  28. package/src/local-server/index.ts +7 -1
  29. package/src/local-server/types.ts +2 -0
  30. package/src/websocket/channel-store.ts +116 -0
  31. package/src/websocket/index.ts +30 -4
  32. package/src/websocket/socket-emitter.ts +64 -0
  33. package/src/websocket/socket-gateway-client.ts +83 -0
  34. package/src/websocket/types.ts +10 -0
  35. 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 { LocalGatewayClient } from './websocket/local-gateway-client'
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
- return adapterFn(wss, config)
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 {
@@ -32,4 +32,6 @@ export interface LambdaOptionsALB {
32
32
 
33
33
  export interface LocalServerConfig {
34
34
  debug?: boolean
35
+ useRedis?: boolean
36
+ redis?: import('../websocket/types').RedisConfig
35
37
  }
@@ -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
+ }