@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
package/src/websocket/index.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
|
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
|
+
}
|
package/src/websocket/types.ts
CHANGED
|
@@ -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
|
-
}
|