@mono-labs/dev 0.1.251
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/aws-event-synthesis/index.d.ts +25 -0
- package/dist/aws-event-synthesis/index.d.ts.map +1 -0
- package/dist/aws-event-synthesis/index.js +90 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/local-server/event-synthesizer.d.ts +7 -0
- package/dist/local-server/event-synthesizer.d.ts.map +1 -0
- package/dist/local-server/event-synthesizer.js +62 -0
- package/dist/local-server/index.d.ts +17 -0
- package/dist/local-server/index.d.ts.map +1 -0
- package/dist/local-server/index.js +66 -0
- package/dist/local-server/types.d.ts +14 -0
- package/dist/local-server/types.d.ts.map +1 -0
- package/dist/local-server/types.js +2 -0
- package/dist/websocket/action-router.d.ts +17 -0
- package/dist/websocket/action-router.d.ts.map +1 -0
- package/dist/websocket/action-router.js +48 -0
- package/dist/websocket/connection-registry.d.ts +28 -0
- package/dist/websocket/connection-registry.d.ts.map +1 -0
- package/dist/websocket/connection-registry.js +59 -0
- package/dist/websocket/event-synthesizer.d.ts +24 -0
- package/dist/websocket/event-synthesizer.d.ts.map +1 -0
- package/dist/websocket/event-synthesizer.js +103 -0
- package/dist/websocket/index.d.ts +20 -0
- package/dist/websocket/index.d.ts.map +1 -0
- package/dist/websocket/index.js +132 -0
- package/dist/websocket/local-gateway-client.d.ts +15 -0
- package/dist/websocket/local-gateway-client.d.ts.map +1 -0
- package/dist/websocket/local-gateway-client.js +31 -0
- package/dist/websocket/types.d.ts +55 -0
- package/dist/websocket/types.d.ts.map +1 -0
- package/dist/websocket/types.js +2 -0
- package/package.json +38 -0
- package/src/aws-event-synthesis/index.ts +99 -0
- package/src/index.ts +21 -0
- package/src/local-server/event-synthesizer.ts +71 -0
- package/src/local-server/index.ts +94 -0
- package/src/local-server/types.ts +35 -0
- package/src/websocket/action-router.ts +66 -0
- package/src/websocket/connection-registry.ts +67 -0
- package/src/websocket/event-synthesizer.ts +131 -0
- package/src/websocket/index.ts +146 -0
- package/src/websocket/local-gateway-client.ts +31 -0
- package/src/websocket/types.ts +66 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActionHandler,
|
|
3
|
+
ActionHandlerContext,
|
|
4
|
+
ActionHandlerResult,
|
|
5
|
+
ConnectionId,
|
|
6
|
+
LocalRequestContext,
|
|
7
|
+
PostToConnectionFn,
|
|
8
|
+
} from './types'
|
|
9
|
+
import type { WebSocketUserContext } from './types'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Routes incoming WebSocket messages by `action` field — mirrors
|
|
13
|
+
* API Gateway's `$request.body.action` route selection.
|
|
14
|
+
*/
|
|
15
|
+
export class ActionRouter {
|
|
16
|
+
private routes = new Map<string, ActionHandler>()
|
|
17
|
+
private defaultHandler: ActionHandler | null = null
|
|
18
|
+
|
|
19
|
+
/** Register a handler for a specific action (equivalent to webSocketApi.addRoute()) */
|
|
20
|
+
addRoute(action: string, handler: ActionHandler): void {
|
|
21
|
+
this.routes.set(action, handler)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Set the $default handler for unknown actions */
|
|
25
|
+
setDefaultHandler(handler: ActionHandler): void {
|
|
26
|
+
this.defaultHandler = handler
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Parse incoming message JSON, extract `action`, and dispatch to the matching handler */
|
|
30
|
+
async route(
|
|
31
|
+
connectionId: ConnectionId,
|
|
32
|
+
rawBody: string,
|
|
33
|
+
postToConnection: PostToConnectionFn,
|
|
34
|
+
requestContext: LocalRequestContext,
|
|
35
|
+
userContext: WebSocketUserContext
|
|
36
|
+
): Promise<ActionHandlerResult> {
|
|
37
|
+
let parsed: { action?: string; [k: string]: unknown }
|
|
38
|
+
try {
|
|
39
|
+
parsed = JSON.parse(rawBody)
|
|
40
|
+
} catch {
|
|
41
|
+
return { statusCode: 400, body: JSON.stringify({ error: 'Invalid JSON' }) }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const action = parsed.action
|
|
45
|
+
if (!action || typeof action !== 'string') {
|
|
46
|
+
return { statusCode: 400, body: JSON.stringify({ error: 'Missing "action" field' }) }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const handler = this.routes.get(action) ?? this.defaultHandler
|
|
50
|
+
if (!handler) {
|
|
51
|
+
return {
|
|
52
|
+
statusCode: 400,
|
|
53
|
+
body: JSON.stringify({ error: `Unknown action: ${action}` }),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const ctx: ActionHandlerContext = {
|
|
58
|
+
connectionId,
|
|
59
|
+
requestContext: { ...requestContext, routeKey: action },
|
|
60
|
+
postToConnection,
|
|
61
|
+
userContext,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return handler(rawBody, ctx)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { WebSocket } from 'ws'
|
|
2
|
+
import type { ConnectionId } from './types'
|
|
3
|
+
import type { WebSocketUserContext } from './types'
|
|
4
|
+
|
|
5
|
+
/** Generates a unique connection ID (same pattern as socket-real.ts) */
|
|
6
|
+
const makeConnectionId = (): ConnectionId =>
|
|
7
|
+
`${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* In-memory connection registry — replaces API Gateway's built-in connection management.
|
|
11
|
+
* Maps connectionId → WebSocket instance and connectionId → user context.
|
|
12
|
+
*/
|
|
13
|
+
export class ConnectionRegistry {
|
|
14
|
+
private connections = new Map<ConnectionId, WebSocket>()
|
|
15
|
+
private userContexts = new Map<ConnectionId, WebSocketUserContext>()
|
|
16
|
+
|
|
17
|
+
/** Register a WebSocket and return its assigned connectionId */
|
|
18
|
+
register(ws: WebSocket): ConnectionId {
|
|
19
|
+
const connectionId = makeConnectionId()
|
|
20
|
+
this.connections.set(connectionId, ws)
|
|
21
|
+
return connectionId
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Remove a connection by ID */
|
|
25
|
+
unregister(connectionId: ConnectionId): void {
|
|
26
|
+
this.connections.delete(connectionId)
|
|
27
|
+
this.userContexts.delete(connectionId)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Look up a WebSocket by connectionId */
|
|
31
|
+
get(connectionId: ConnectionId): WebSocket | undefined {
|
|
32
|
+
return this.connections.get(connectionId)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Return all active connectionIds */
|
|
36
|
+
getAll(): ConnectionId[] {
|
|
37
|
+
return Array.from(this.connections.keys())
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Store user context for a connection */
|
|
41
|
+
setUserContext(connectionId: ConnectionId, ctx: WebSocketUserContext): void {
|
|
42
|
+
this.userContexts.set(connectionId, ctx)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Retrieve user context for a connection */
|
|
46
|
+
getUserContext(connectionId: ConnectionId): WebSocketUserContext | undefined {
|
|
47
|
+
return this.userContexts.get(connectionId)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Find all connectionIds belonging to a specific user */
|
|
51
|
+
getConnectionsByUserId(userId: string): ConnectionId[] {
|
|
52
|
+
const result: ConnectionId[] = []
|
|
53
|
+
for (const [connId, ctx] of this.userContexts) {
|
|
54
|
+
if (ctx.userId === userId) result.push(connId)
|
|
55
|
+
}
|
|
56
|
+
return result
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Find all connectionIds belonging to a specific organization */
|
|
60
|
+
getConnectionsByOrgId(orgId: string): ConnectionId[] {
|
|
61
|
+
const result: ConnectionId[] = []
|
|
62
|
+
for (const [connId, ctx] of this.userContexts) {
|
|
63
|
+
if (ctx.organizationId === orgId) result.push(connId)
|
|
64
|
+
}
|
|
65
|
+
return result
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { APIGatewayProxyEvent } from 'aws-lambda'
|
|
2
|
+
import type http from 'node:http'
|
|
3
|
+
import type { ConnectionId, LocalRequestContext } from './types'
|
|
4
|
+
|
|
5
|
+
const EMPTY_MULTI_VALUE: Record<string, string[]> = {}
|
|
6
|
+
const EMPTY_HEADERS: Record<string, string> = {}
|
|
7
|
+
|
|
8
|
+
/** Creates a minimal APIGatewayProxyEvent shell with the given requestContext fields */
|
|
9
|
+
function baseEvent(
|
|
10
|
+
requestContext: Partial<APIGatewayProxyEvent['requestContext']>,
|
|
11
|
+
overrides?: Partial<APIGatewayProxyEvent>
|
|
12
|
+
): APIGatewayProxyEvent {
|
|
13
|
+
return {
|
|
14
|
+
body: null,
|
|
15
|
+
headers: EMPTY_HEADERS,
|
|
16
|
+
multiValueHeaders: EMPTY_MULTI_VALUE,
|
|
17
|
+
httpMethod: 'GET',
|
|
18
|
+
isBase64Encoded: false,
|
|
19
|
+
path: '',
|
|
20
|
+
pathParameters: null,
|
|
21
|
+
queryStringParameters: null,
|
|
22
|
+
multiValueQueryStringParameters: null,
|
|
23
|
+
stageVariables: null,
|
|
24
|
+
resource: '',
|
|
25
|
+
requestContext: {
|
|
26
|
+
accountId: 'local',
|
|
27
|
+
apiId: 'local',
|
|
28
|
+
authorizer: null,
|
|
29
|
+
protocol: 'websocket',
|
|
30
|
+
httpMethod: 'GET',
|
|
31
|
+
identity: {
|
|
32
|
+
accessKey: null,
|
|
33
|
+
accountId: null,
|
|
34
|
+
apiKey: null,
|
|
35
|
+
apiKeyId: null,
|
|
36
|
+
caller: null,
|
|
37
|
+
clientCert: null,
|
|
38
|
+
cognitoAuthenticationProvider: null,
|
|
39
|
+
cognitoAuthenticationType: null,
|
|
40
|
+
cognitoIdentityId: null,
|
|
41
|
+
cognitoIdentityPoolId: null,
|
|
42
|
+
principalOrgId: null,
|
|
43
|
+
sourceIp: '127.0.0.1',
|
|
44
|
+
user: null,
|
|
45
|
+
userAgent: null,
|
|
46
|
+
userArn: null,
|
|
47
|
+
},
|
|
48
|
+
path: '',
|
|
49
|
+
stage: 'local',
|
|
50
|
+
requestId: `local-${Date.now()}`,
|
|
51
|
+
requestTimeEpoch: Date.now(),
|
|
52
|
+
resourceId: '',
|
|
53
|
+
resourcePath: '',
|
|
54
|
+
...requestContext,
|
|
55
|
+
},
|
|
56
|
+
...overrides,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Synthesize a $connect event */
|
|
61
|
+
export function synthesizeConnectEvent(
|
|
62
|
+
connectionId: ConnectionId,
|
|
63
|
+
req: http.IncomingMessage,
|
|
64
|
+
config?: { domainName?: string; stage?: string }
|
|
65
|
+
): APIGatewayProxyEvent {
|
|
66
|
+
const headers: Record<string, string> = {}
|
|
67
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
68
|
+
if (typeof value === 'string') headers[key] = value
|
|
69
|
+
else if (Array.isArray(value)) headers[key] = value.join(', ')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return baseEvent(
|
|
73
|
+
{
|
|
74
|
+
connectionId,
|
|
75
|
+
domainName: config?.domainName ?? 'localhost',
|
|
76
|
+
stage: config?.stage ?? 'local',
|
|
77
|
+
routeKey: '$connect',
|
|
78
|
+
eventType: 'CONNECT',
|
|
79
|
+
} as any,
|
|
80
|
+
{ headers }
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Synthesize a $disconnect event */
|
|
85
|
+
export function synthesizeDisconnectEvent(
|
|
86
|
+
connectionId: ConnectionId,
|
|
87
|
+
config?: { domainName?: string; stage?: string }
|
|
88
|
+
): APIGatewayProxyEvent {
|
|
89
|
+
return baseEvent({
|
|
90
|
+
connectionId,
|
|
91
|
+
domainName: config?.domainName ?? 'localhost',
|
|
92
|
+
stage: config?.stage ?? 'local',
|
|
93
|
+
routeKey: '$disconnect',
|
|
94
|
+
eventType: 'DISCONNECT',
|
|
95
|
+
} as any)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Synthesize a message event with body and routeKey */
|
|
99
|
+
export function synthesizeMessageEvent(
|
|
100
|
+
connectionId: ConnectionId,
|
|
101
|
+
body: string,
|
|
102
|
+
routeKey: string,
|
|
103
|
+
config?: { domainName?: string; stage?: string }
|
|
104
|
+
): APIGatewayProxyEvent {
|
|
105
|
+
return baseEvent(
|
|
106
|
+
{
|
|
107
|
+
connectionId,
|
|
108
|
+
domainName: config?.domainName ?? 'localhost',
|
|
109
|
+
stage: config?.stage ?? 'local',
|
|
110
|
+
routeKey,
|
|
111
|
+
eventType: 'MESSAGE',
|
|
112
|
+
} as any,
|
|
113
|
+
{ body }
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Build a LocalRequestContext from parameters */
|
|
118
|
+
export function buildRequestContext(
|
|
119
|
+
connectionId: ConnectionId,
|
|
120
|
+
routeKey: string,
|
|
121
|
+
eventType: LocalRequestContext['eventType'],
|
|
122
|
+
config?: { domainName?: string; stage?: string }
|
|
123
|
+
): LocalRequestContext {
|
|
124
|
+
return {
|
|
125
|
+
connectionId,
|
|
126
|
+
domainName: config?.domainName ?? 'localhost',
|
|
127
|
+
stage: config?.stage ?? 'local',
|
|
128
|
+
routeKey,
|
|
129
|
+
eventType,
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { WebSocket, WebSocketServer } from 'ws'
|
|
2
|
+
|
|
3
|
+
import { ActionRouter } from './action-router'
|
|
4
|
+
import { ConnectionRegistry } from './connection-registry'
|
|
5
|
+
import { buildRequestContext } from './event-synthesizer'
|
|
6
|
+
import { LocalGatewayClient } from './local-gateway-client'
|
|
7
|
+
import type { ConnectionId, SocketAdapterConfig } from './types'
|
|
8
|
+
|
|
9
|
+
export type { ConnectionId, PostToConnectionFn, SocketAdapterConfig } from './types'
|
|
10
|
+
export { ConnectionRegistry } from './connection-registry'
|
|
11
|
+
export { LocalGatewayClient } from './local-gateway-client'
|
|
12
|
+
export { ActionRouter } from './action-router'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Attaches a full socket adapter to a WebSocketServer instance.
|
|
16
|
+
* Maps 1:1 to each API Gateway WebSocket event so local dev
|
|
17
|
+
* behaves identically to the deployed system.
|
|
18
|
+
*/
|
|
19
|
+
export function attachSocketAdapter(wss: WebSocketServer, config?: SocketAdapterConfig) {
|
|
20
|
+
const debug = config?.debug ?? false
|
|
21
|
+
const domainName = config?.domainName ?? 'localhost'
|
|
22
|
+
const stage = config?.stage ?? 'local'
|
|
23
|
+
|
|
24
|
+
const connectionRegistry = new ConnectionRegistry()
|
|
25
|
+
const gatewayClient = new LocalGatewayClient(connectionRegistry)
|
|
26
|
+
const postToConnection = gatewayClient.asFunction()
|
|
27
|
+
const actionRouter = new ActionRouter()
|
|
28
|
+
|
|
29
|
+
// Register consumer-provided routes
|
|
30
|
+
if (config?.routes) {
|
|
31
|
+
for (const [action, handler] of Object.entries(config.routes)) {
|
|
32
|
+
actionRouter.addRoute(action, handler)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// $default handler for unknown actions
|
|
37
|
+
if (config?.defaultHandler) {
|
|
38
|
+
actionRouter.setDefaultHandler(config.defaultHandler)
|
|
39
|
+
} else {
|
|
40
|
+
actionRouter.setDefaultHandler(async (body, ctx) => {
|
|
41
|
+
let action = 'unknown'
|
|
42
|
+
try {
|
|
43
|
+
action = JSON.parse(body).action ?? 'unknown'
|
|
44
|
+
} catch {}
|
|
45
|
+
return {
|
|
46
|
+
statusCode: 400,
|
|
47
|
+
body: JSON.stringify({ error: `Unknown action: ${action}` }),
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Use consumer-provided handlers or sensible defaults
|
|
53
|
+
const connectHandler = config?.connectHandler ?? (async () => ({
|
|
54
|
+
response: { statusCode: 200 },
|
|
55
|
+
userContext: undefined,
|
|
56
|
+
}))
|
|
57
|
+
|
|
58
|
+
const disconnectHandler = config?.disconnectHandler ?? (async () => {})
|
|
59
|
+
|
|
60
|
+
// Reverse lookup: WebSocket → connectionId
|
|
61
|
+
const wsToConnectionId = new WeakMap<WebSocket, ConnectionId>()
|
|
62
|
+
|
|
63
|
+
wss.on('connection', async (ws: WebSocket, req) => {
|
|
64
|
+
// 1. Extract token from query string
|
|
65
|
+
const url = new URL(req.url ?? '', `http://${req.headers.host ?? 'localhost'}`)
|
|
66
|
+
const token = url.searchParams.get('token') ?? undefined
|
|
67
|
+
|
|
68
|
+
// 2. Register connection and assign connectionId
|
|
69
|
+
const connectionId = connectionRegistry.register(ws)
|
|
70
|
+
wsToConnectionId.set(ws, connectionId)
|
|
71
|
+
|
|
72
|
+
if (debug) console.log(`[socket-adapter] $connect connectionId=${connectionId}`)
|
|
73
|
+
if (debug) console.log(`[socket-adapter] token ${token ? 'present' : 'MISSING'}`)
|
|
74
|
+
|
|
75
|
+
// 3. Authenticate via connectHandler
|
|
76
|
+
const { response, userContext } = await connectHandler(connectionId, { token })
|
|
77
|
+
if (debug) console.log(`[socket-adapter] connectHandler result:`, response)
|
|
78
|
+
if (debug && userContext) {
|
|
79
|
+
console.log(`[socket-adapter] authenticated userId=${userContext.userId} orgId=${userContext.organizationId}`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 4. Reject if connectHandler returns non-200
|
|
83
|
+
if (response.statusCode !== 200) {
|
|
84
|
+
if (debug) console.log(`[socket-adapter] rejected connectionId=${connectionId} status=${response.statusCode}`)
|
|
85
|
+
ws.close(1008, 'Authentication failed')
|
|
86
|
+
connectionRegistry.unregister(connectionId)
|
|
87
|
+
wsToConnectionId.delete(ws)
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 5. Store user context in the registry (if provided)
|
|
92
|
+
if (userContext) {
|
|
93
|
+
connectionRegistry.setUserContext(connectionId, userContext)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 6. Send welcome message to client
|
|
97
|
+
const welcomeMessage: Record<string, unknown> = { type: 'connected', connectionId }
|
|
98
|
+
if (userContext) welcomeMessage.userId = userContext.userId
|
|
99
|
+
ws.send(JSON.stringify(welcomeMessage))
|
|
100
|
+
if (debug) console.log(`[socket-adapter] welcome sent to ${connectionId}${userContext ? ` userId=${userContext.userId}` : ''}`)
|
|
101
|
+
|
|
102
|
+
// 7. Route incoming messages through ActionRouter
|
|
103
|
+
ws.on('message', async (raw) => {
|
|
104
|
+
const rawBody = raw.toString()
|
|
105
|
+
|
|
106
|
+
if (debug) console.log(`[socket-adapter] message from ${connectionId}:`, rawBody)
|
|
107
|
+
|
|
108
|
+
const requestContext = buildRequestContext(connectionId, '$default', 'MESSAGE', {
|
|
109
|
+
domainName,
|
|
110
|
+
stage,
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const resolvedUserContext = userContext ?? connectionRegistry.getUserContext(connectionId) ?? {
|
|
114
|
+
userId: 'anonymous',
|
|
115
|
+
organizationId: 'anonymous',
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const result = await actionRouter.route(connectionId, rawBody, postToConnection, requestContext, resolvedUserContext)
|
|
119
|
+
|
|
120
|
+
// Send the handler result back to the sender
|
|
121
|
+
if (result.body) {
|
|
122
|
+
ws.send(result.body)
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// 8. Handle disconnect
|
|
127
|
+
ws.on('close', async (code, reason) => {
|
|
128
|
+
if (debug) {
|
|
129
|
+
console.log(
|
|
130
|
+
`[socket-adapter] $disconnect connectionId=${connectionId} code=${code} reason=${reason.toString()}`
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await disconnectHandler(connectionId)
|
|
135
|
+
connectionRegistry.unregister(connectionId)
|
|
136
|
+
wsToConnectionId.delete(ws)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
postToConnection,
|
|
142
|
+
connectionRegistry,
|
|
143
|
+
actionRouter,
|
|
144
|
+
getConnectionId: (ws: WebSocket) => wsToConnectionId.get(ws),
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { WebSocket } from 'ws'
|
|
2
|
+
|
|
3
|
+
/** User context attached to a WebSocket connection */
|
|
4
|
+
export interface WebSocketUserContext {
|
|
5
|
+
userId: string
|
|
6
|
+
organizationId: string
|
|
7
|
+
[key: string]: unknown
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Unique identifier for a WebSocket connection (mirrors API Gateway connectionId) */
|
|
11
|
+
export type ConnectionId = string
|
|
12
|
+
|
|
13
|
+
/** Function signature for sending data to a connection (replaces PostToConnectionCommand) */
|
|
14
|
+
export type PostToConnectionFn = (connectionId: ConnectionId, data: unknown) => Promise<void>
|
|
15
|
+
|
|
16
|
+
/** Synthesized equivalent of event.requestContext for local dev */
|
|
17
|
+
export interface LocalRequestContext {
|
|
18
|
+
connectionId: ConnectionId
|
|
19
|
+
domainName: string
|
|
20
|
+
stage: string
|
|
21
|
+
routeKey: string
|
|
22
|
+
eventType: 'CONNECT' | 'DISCONNECT' | 'MESSAGE'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Context passed to action handlers */
|
|
26
|
+
export interface ActionHandlerContext {
|
|
27
|
+
connectionId: ConnectionId
|
|
28
|
+
requestContext: LocalRequestContext
|
|
29
|
+
postToConnection: PostToConnectionFn
|
|
30
|
+
userContext: WebSocketUserContext
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Result returned from an action handler */
|
|
34
|
+
export interface ActionHandlerResult {
|
|
35
|
+
statusCode: number
|
|
36
|
+
body?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Handler function for a routed action */
|
|
40
|
+
export type ActionHandler = (
|
|
41
|
+
body: string,
|
|
42
|
+
ctx: ActionHandlerContext
|
|
43
|
+
) => Promise<ActionHandlerResult>
|
|
44
|
+
|
|
45
|
+
/** Handler called on $connect — returns response + optional user context */
|
|
46
|
+
export type ConnectHandlerFn = (
|
|
47
|
+
connectionId: ConnectionId,
|
|
48
|
+
params: { token?: string },
|
|
49
|
+
) => Promise<{
|
|
50
|
+
response: { statusCode: number; body?: string }
|
|
51
|
+
userContext?: WebSocketUserContext
|
|
52
|
+
}>
|
|
53
|
+
|
|
54
|
+
/** Handler called on $disconnect */
|
|
55
|
+
export type DisconnectHandlerFn = (connectionId: ConnectionId) => Promise<void>
|
|
56
|
+
|
|
57
|
+
/** Configuration for the socket adapter */
|
|
58
|
+
export interface SocketAdapterConfig {
|
|
59
|
+
domainName?: string
|
|
60
|
+
stage?: string
|
|
61
|
+
debug?: boolean
|
|
62
|
+
connectHandler?: ConnectHandlerFn
|
|
63
|
+
disconnectHandler?: DisconnectHandlerFn
|
|
64
|
+
routes?: Record<string, ActionHandler>
|
|
65
|
+
defaultHandler?: ActionHandler
|
|
66
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"rootDir": "./src",
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"target": "ES2022",
|
|
6
|
+
"module": "CommonJS",
|
|
7
|
+
"moduleResolution": "Node",
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"allowSyntheticDefaultImports": true,
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"declarationMap": true,
|
|
12
|
+
"composite": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"resolveJsonModule": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|