@mono-labs/dev 0.1.251 → 0.1.255

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.
@@ -4,12 +4,19 @@ import { ActionRouter } from './action-router'
4
4
  import { ConnectionRegistry } from './connection-registry'
5
5
  import { buildRequestContext } from './event-synthesizer'
6
6
  import { LocalGatewayClient } from './local-gateway-client'
7
+ import { InMemoryChannelStore, RedisChannelStore } from './channel-store'
8
+ import { SocketEmitter } from './socket-emitter'
9
+ import { initCacheRelay } from '../cache-relay'
7
10
  import type { ConnectionId, SocketAdapterConfig } from './types'
8
11
 
9
- export type { ConnectionId, PostToConnectionFn, SocketAdapterConfig } from './types'
12
+ export type { ConnectionId, PostToConnectionFn, SocketAdapterConfig, RedisConfig } from './types'
10
13
  export { ConnectionRegistry } from './connection-registry'
11
14
  export { LocalGatewayClient } from './local-gateway-client'
12
15
  export { ActionRouter } from './action-router'
16
+ export { InMemoryChannelStore, RedisChannelStore } from './channel-store'
17
+ export type { ChannelStore } from './channel-store'
18
+ export { SocketEmitter } from './socket-emitter'
19
+ export type { EmitTarget } from './socket-emitter'
13
20
 
14
21
  /**
15
22
  * Attaches a full socket adapter to a WebSocketServer instance.
@@ -26,6 +33,22 @@ export function attachSocketAdapter(wss: WebSocketServer, config?: SocketAdapter
26
33
  const postToConnection = gatewayClient.asFunction()
27
34
  const actionRouter = new ActionRouter()
28
35
 
36
+ // Create channel store
37
+ let channelStore = config?.channelStore
38
+ if (!channelStore) {
39
+ if (config?.useRedis) {
40
+ const host = config.redis?.host ?? 'localhost'
41
+ const port = config.redis?.port ?? 6379
42
+ initCacheRelay(`${host}:${port}`)
43
+ channelStore = new RedisChannelStore({ keyPrefix: config.redis?.keyPrefix })
44
+ } else {
45
+ channelStore = new InMemoryChannelStore()
46
+ }
47
+ }
48
+
49
+ // Create socket emitter
50
+ const socketEmitter = new SocketEmitter({ postToConnection, connectionRegistry, channelStore })
51
+
29
52
  // Register consumer-provided routes
30
53
  if (config?.routes) {
31
54
  for (const [action, handler] of Object.entries(config.routes)) {
@@ -131,6 +154,7 @@ export function attachSocketAdapter(wss: WebSocketServer, config?: SocketAdapter
131
154
  )
132
155
  }
133
156
 
157
+ await channelStore.removeAll(connectionId)
134
158
  await disconnectHandler(connectionId)
135
159
  connectionRegistry.unregister(connectionId)
136
160
  wsToConnectionId.delete(ws)
@@ -141,6 +165,8 @@ export function attachSocketAdapter(wss: WebSocketServer, config?: SocketAdapter
141
165
  postToConnection,
142
166
  connectionRegistry,
143
167
  actionRouter,
168
+ channelStore,
169
+ socketEmitter,
144
170
  getConnectionId: (ws: WebSocket) => wsToConnectionId.get(ws),
145
171
  }
146
172
  }
@@ -0,0 +1,64 @@
1
+ import type { ConnectionRegistry } from './connection-registry'
2
+ import type { ChannelStore } from './channel-store'
3
+ import type { ConnectionId, PostToConnectionFn } from './types'
4
+
5
+ export type EmitTarget =
6
+ | { userId: string }
7
+ | { orgId: string }
8
+ | { connectionId: string }
9
+ | { channel: string }
10
+ | 'broadcast'
11
+
12
+ export class SocketEmitter {
13
+ private postToConnection: PostToConnectionFn
14
+ private connectionRegistry: ConnectionRegistry
15
+ private channelStore: ChannelStore
16
+
17
+ constructor(deps: {
18
+ postToConnection: PostToConnectionFn
19
+ connectionRegistry: ConnectionRegistry
20
+ channelStore: ChannelStore
21
+ }) {
22
+ this.postToConnection = deps.postToConnection
23
+ this.connectionRegistry = deps.connectionRegistry
24
+ this.channelStore = deps.channelStore
25
+ }
26
+
27
+ async emit(target: EmitTarget, data: unknown): Promise<void> {
28
+ const connectionIds = await this.resolveConnectionIds(target)
29
+ if (connectionIds.length === 0) return
30
+
31
+ const payload = typeof data === 'string' ? data : JSON.stringify(data)
32
+
33
+ await Promise.allSettled(
34
+ connectionIds.map(async (connId) => {
35
+ try {
36
+ await this.postToConnection(connId, payload)
37
+ } catch (err: unknown) {
38
+ const e = err as { statusCode?: number; name?: string }
39
+ if (e?.statusCode === 410 || e?.name === 'GoneException') return
40
+ console.error(`[SocketEmitter] failed to send to ${connId}:`, err)
41
+ }
42
+ })
43
+ )
44
+ }
45
+
46
+ private async resolveConnectionIds(target: EmitTarget): Promise<ConnectionId[]> {
47
+ if (target === 'broadcast') {
48
+ return this.connectionRegistry.getAll()
49
+ }
50
+ if ('connectionId' in target) {
51
+ return [target.connectionId]
52
+ }
53
+ if ('userId' in target) {
54
+ return this.connectionRegistry.getConnectionsByUserId(target.userId)
55
+ }
56
+ if ('orgId' in target) {
57
+ return this.connectionRegistry.getConnectionsByOrgId(target.orgId)
58
+ }
59
+ if ('channel' in target) {
60
+ return this.channelStore.getSubscribers(target.channel)
61
+ }
62
+ return []
63
+ }
64
+ }
@@ -54,6 +54,13 @@ export type ConnectHandlerFn = (
54
54
  /** Handler called on $disconnect */
55
55
  export type DisconnectHandlerFn = (connectionId: ConnectionId) => Promise<void>
56
56
 
57
+ /** Redis connection configuration */
58
+ export interface RedisConfig {
59
+ host?: string
60
+ port?: number
61
+ keyPrefix?: string
62
+ }
63
+
57
64
  /** Configuration for the socket adapter */
58
65
  export interface SocketAdapterConfig {
59
66
  domainName?: string
@@ -63,4 +70,7 @@ export interface SocketAdapterConfig {
63
70
  disconnectHandler?: DisconnectHandlerFn
64
71
  routes?: Record<string, ActionHandler>
65
72
  defaultHandler?: ActionHandler
73
+ channelStore?: import('./channel-store').ChannelStore
74
+ useRedis?: boolean
75
+ redis?: RedisConfig
66
76
  }