@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
@@ -3,13 +3,20 @@ import type { WebSocket, WebSocketServer } from 'ws'
3
3
  import { ActionRouter } from './action-router'
4
4
  import { ConnectionRegistry } from './connection-registry'
5
5
  import { buildRequestContext } from './event-synthesizer'
6
- import { LocalGatewayClient } from './local-gateway-client'
6
+ import { SocketGatewayClient } from './socket-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
- export { LocalGatewayClient } from './local-gateway-client'
14
+ export { SocketGatewayClient } from './socket-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.
@@ -22,10 +29,26 @@ export function attachSocketAdapter(wss: WebSocketServer, config?: SocketAdapter
22
29
  const stage = config?.stage ?? 'local'
23
30
 
24
31
  const connectionRegistry = new ConnectionRegistry()
25
- const gatewayClient = new LocalGatewayClient(connectionRegistry)
32
+ const gatewayClient = new SocketGatewayClient(connectionRegistry)
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
+ }
@@ -0,0 +1,83 @@
1
+ import type { ConnectionId, PostToConnectionFn } from './types'
2
+ import type { ConnectionRegistry } from './connection-registry'
3
+
4
+ /**
5
+ * Unified gateway client that auto-detects local vs production mode.
6
+ *
7
+ * - Pass a `ConnectionRegistry` → local mode (sends via in-memory WebSocket)
8
+ * - Pass a URL string → API Gateway mode (sends via AWS SDK PostToConnectionCommand)
9
+ */
10
+ export class SocketGatewayClient {
11
+ private mode: 'local' | 'apigateway'
12
+ private registry?: ConnectionRegistry
13
+ private endpoint?: string
14
+
15
+ constructor(backend: ConnectionRegistry | string) {
16
+ if (typeof backend === 'string') {
17
+ this.mode = 'apigateway'
18
+ this.endpoint = backend
19
+ } else {
20
+ this.mode = 'local'
21
+ this.registry = backend
22
+ }
23
+ }
24
+
25
+ async postToConnection(connectionId: ConnectionId, data: unknown): Promise<void> {
26
+ if (this.mode === 'local') {
27
+ return this.postLocal(connectionId, data)
28
+ }
29
+ return this.postApiGateway(connectionId, data)
30
+ }
31
+
32
+ private async postLocal(connectionId: ConnectionId, data: unknown): Promise<void> {
33
+ const { WebSocket } = require('ws') as typeof import('ws')
34
+ const ws = this.registry!.get(connectionId)
35
+
36
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
37
+ const error = new Error(`GoneException: Connection ${connectionId} is no longer available`)
38
+ ;(error as any).statusCode = 410
39
+ ;(error as any).name = 'GoneException'
40
+ throw error
41
+ }
42
+
43
+ const payload = typeof data === 'string' ? data : JSON.stringify(data)
44
+ ws.send(payload)
45
+ }
46
+
47
+ private async postApiGateway(connectionId: ConnectionId, data: unknown): Promise<void> {
48
+ let ApiGatewayManagementApiClient: any, PostToConnectionCommand: any
49
+ try {
50
+ const mod = require('@aws-sdk/client-apigatewaymanagementapi')
51
+ ApiGatewayManagementApiClient = mod.ApiGatewayManagementApiClient
52
+ PostToConnectionCommand = mod.PostToConnectionCommand
53
+ } catch {
54
+ throw new Error(
55
+ 'API Gateway mode requires "@aws-sdk/client-apigatewaymanagementapi". '
56
+ + 'Install it with: npm install @aws-sdk/client-apigatewaymanagementapi'
57
+ )
58
+ }
59
+
60
+ const client = new ApiGatewayManagementApiClient({ endpoint: this.endpoint })
61
+ const payload = typeof data === 'string' ? data : JSON.stringify(data)
62
+
63
+ try {
64
+ await client.send(new PostToConnectionCommand({
65
+ ConnectionId: connectionId,
66
+ Data: new TextEncoder().encode(payload),
67
+ }))
68
+ } catch (err: any) {
69
+ if (err.name === 'GoneException' || err.$metadata?.httpStatusCode === 410) {
70
+ const error = new Error(`GoneException: Connection ${connectionId} is no longer available`)
71
+ ;(error as any).statusCode = 410
72
+ ;(error as any).name = 'GoneException'
73
+ throw error
74
+ }
75
+ throw err
76
+ }
77
+ }
78
+
79
+ /** Returns a bound PostToConnectionFn for dependency injection */
80
+ asFunction(): PostToConnectionFn {
81
+ return this.postToConnection.bind(this)
82
+ }
83
+ }
@@ -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
  }
@@ -1,31 +0,0 @@
1
- import { WebSocket } from 'ws'
2
- import type { ConnectionRegistry } from './connection-registry'
3
- import type { ConnectionId, PostToConnectionFn } from './types'
4
-
5
- /**
6
- * Local replacement for @aws-sdk/client-apigatewaymanagementapi.
7
- * Looks up the WebSocket in ConnectionRegistry and sends data directly.
8
- */
9
- export class LocalGatewayClient {
10
- constructor(private registry: ConnectionRegistry) {}
11
-
12
- /** Send data to a specific connection. Throws GoneException (410) if not found. */
13
- async postToConnection(connectionId: ConnectionId, data: unknown): Promise<void> {
14
- const ws = this.registry.get(connectionId)
15
-
16
- if (!ws || ws.readyState !== WebSocket.OPEN) {
17
- const error = new Error(`GoneException: Connection ${connectionId} is no longer available`)
18
- ;(error as any).statusCode = 410
19
- ;(error as any).name = 'GoneException'
20
- throw error
21
- }
22
-
23
- const payload = typeof data === 'string' ? data : JSON.stringify(data)
24
- ws.send(payload)
25
- }
26
-
27
- /** Returns a bound PostToConnectionFn for dependency injection */
28
- asFunction(): PostToConnectionFn {
29
- return this.postToConnection.bind(this)
30
- }
31
- }