cprime-supergateway 3.4.3
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/.github/workflows/docker-publish.yaml +79 -0
- package/.husky/pre-commit +17 -0
- package/.prettierignore +8 -0
- package/.prettierrc +5 -0
- package/AGENTS.md +29 -0
- package/LICENSE +21 -0
- package/README.md +348 -0
- package/dist/gateways/sseToStdio.js +139 -0
- package/dist/gateways/stdioToSse.js +147 -0
- package/dist/gateways/stdioToStatefulStreamableHttp.js +188 -0
- package/dist/gateways/stdioToStatelessStreamableHttp.js +208 -0
- package/dist/gateways/stdioToWs.js +113 -0
- package/dist/gateways/streamableHttpToStdio.js +134 -0
- package/dist/index.js +266 -0
- package/dist/lib/corsOrigin.js +23 -0
- package/dist/lib/getLogger.js +44 -0
- package/dist/lib/getVersion.js +16 -0
- package/dist/lib/headers.js +31 -0
- package/dist/lib/onSignals.js +27 -0
- package/dist/lib/serializeCorsOrigin.js +6 -0
- package/dist/lib/sessionAccessCounter.js +77 -0
- package/dist/server/websocket.js +102 -0
- package/dist/services/EncryptionService.js +236 -0
- package/dist/types.js +1 -0
- package/docker/base.Dockerfile +9 -0
- package/docker/deno.Dockerfile +2 -0
- package/docker/uvx.Dockerfile +3 -0
- package/docker-bake.hcl +51 -0
- package/package.json +61 -0
- package/scripts/decrypt-sample.ts +34 -0
- package/scripts/encryption-play.ts +145 -0
- package/src/gateways/sseToStdio.ts +195 -0
- package/src/gateways/stdioToSse.ts +260 -0
- package/src/gateways/stdioToStatefulStreamableHttp.ts +274 -0
- package/src/gateways/stdioToStatelessStreamableHttp.ts +303 -0
- package/src/gateways/stdioToWs.ts +151 -0
- package/src/gateways/streamableHttpToStdio.ts +196 -0
- package/src/index.ts +286 -0
- package/src/lib/corsOrigin.ts +31 -0
- package/src/lib/getLogger.ts +83 -0
- package/src/lib/getVersion.ts +17 -0
- package/src/lib/headers.ts +55 -0
- package/src/lib/initMongoClient.ts +10 -0
- package/src/lib/mcpServerLogRepository.ts +48 -0
- package/src/lib/onSignals.ts +39 -0
- package/src/lib/serializeCorsOrigin.ts +14 -0
- package/src/lib/sessionAccessCounter.ts +118 -0
- package/src/server/websocket.ts +121 -0
- package/src/services/encryptionService.ts +309 -0
- package/src/types.ts +4 -0
- package/supergateway.png +0 -0
- package/tests/baseUrl.test.ts +62 -0
- package/tests/concurrency.test.ts +137 -0
- package/tests/helpers/mock-mcp-server.js +94 -0
- package/tests/protocolVersion.test.ts +60 -0
- package/tests/stdioToStatefulStreamableHttp.test.ts +70 -0
- package/tests/stdioToStatelessStreamableHttp.test.ts +71 -0
- package/tests/streamableHttpCli.test.ts +24 -0
- package/tests/streamableHttpToStdio.test.ts +64 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +12 -0
- package/tsconfig.test.json +10 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import util from 'node:util'
|
|
2
|
+
import { Logger } from '../types.js'
|
|
3
|
+
|
|
4
|
+
const defaultFormatArgs = (args: any[]) => args
|
|
5
|
+
|
|
6
|
+
const log =
|
|
7
|
+
(
|
|
8
|
+
{
|
|
9
|
+
formatArgs = defaultFormatArgs,
|
|
10
|
+
}: {
|
|
11
|
+
formatArgs?: typeof defaultFormatArgs
|
|
12
|
+
} = { formatArgs: defaultFormatArgs },
|
|
13
|
+
) =>
|
|
14
|
+
(...args: any[]) =>
|
|
15
|
+
console.log('[supergateway]', ...formatArgs(args))
|
|
16
|
+
|
|
17
|
+
const logStderr =
|
|
18
|
+
(
|
|
19
|
+
{
|
|
20
|
+
formatArgs = defaultFormatArgs,
|
|
21
|
+
}: {
|
|
22
|
+
formatArgs?: typeof defaultFormatArgs
|
|
23
|
+
} = { formatArgs: defaultFormatArgs },
|
|
24
|
+
) =>
|
|
25
|
+
(...args: any[]) =>
|
|
26
|
+
console.error('[supergateway]', ...formatArgs(args))
|
|
27
|
+
|
|
28
|
+
const noneLogger: Logger = {
|
|
29
|
+
info: () => {},
|
|
30
|
+
error: () => {},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const infoLogger: Logger = {
|
|
34
|
+
info: log(),
|
|
35
|
+
error: logStderr(),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const infoLoggerStdio: Logger = {
|
|
39
|
+
info: logStderr(),
|
|
40
|
+
error: logStderr(),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const debugFormatArgs = (args: any[]) =>
|
|
44
|
+
args.map((arg) => {
|
|
45
|
+
if (typeof arg === 'object') {
|
|
46
|
+
return util.inspect(arg, {
|
|
47
|
+
depth: null,
|
|
48
|
+
colors: process.stderr.isTTY,
|
|
49
|
+
compact: false,
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return arg
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const debugLogger: Logger = {
|
|
57
|
+
info: log({ formatArgs: debugFormatArgs }),
|
|
58
|
+
error: logStderr({ formatArgs: debugFormatArgs }),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const debugLoggerStdio: Logger = {
|
|
62
|
+
info: logStderr({ formatArgs: debugFormatArgs }),
|
|
63
|
+
error: logStderr({ formatArgs: debugFormatArgs }),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const getLogger = ({
|
|
67
|
+
logLevel,
|
|
68
|
+
outputTransport,
|
|
69
|
+
}: {
|
|
70
|
+
logLevel: string
|
|
71
|
+
outputTransport: string
|
|
72
|
+
}): Logger => {
|
|
73
|
+
if (logLevel === 'none') {
|
|
74
|
+
return noneLogger
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (logLevel === 'debug') {
|
|
78
|
+
return outputTransport === 'stdio' ? debugLoggerStdio : debugLogger
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// info logLevel
|
|
82
|
+
return outputTransport === 'stdio' ? infoLoggerStdio : infoLogger
|
|
83
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { fileURLToPath } from 'url'
|
|
2
|
+
import { join, dirname } from 'path'
|
|
3
|
+
import { readFileSync } from 'fs'
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
6
|
+
const __dirname = dirname(__filename)
|
|
7
|
+
|
|
8
|
+
export function getVersion(): string {
|
|
9
|
+
try {
|
|
10
|
+
const packageJsonPath = join(__dirname, '../../package.json')
|
|
11
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
|
|
12
|
+
return packageJson.version || '1.0.0'
|
|
13
|
+
} catch (err) {
|
|
14
|
+
console.error('[supergateway]', 'Unable to retrieve version:', err)
|
|
15
|
+
return 'unknown'
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Logger } from '../types.js'
|
|
2
|
+
|
|
3
|
+
const parseHeaders = ({
|
|
4
|
+
argvHeader,
|
|
5
|
+
logger,
|
|
6
|
+
}: {
|
|
7
|
+
argvHeader: (string | number)[]
|
|
8
|
+
logger: Logger
|
|
9
|
+
}): Record<string, string> => {
|
|
10
|
+
return argvHeader.reduce<Record<string, string>>((acc, rawHeader) => {
|
|
11
|
+
const header = `${rawHeader}`
|
|
12
|
+
|
|
13
|
+
const colonIndex = header.indexOf(':')
|
|
14
|
+
if (colonIndex === -1) {
|
|
15
|
+
logger.error(`Invalid header format: ${header}, ignoring`)
|
|
16
|
+
return acc
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const key = header.slice(0, colonIndex).trim()
|
|
20
|
+
const value = header.slice(colonIndex + 1).trim()
|
|
21
|
+
|
|
22
|
+
if (!key || !value) {
|
|
23
|
+
logger.error(`Invalid header format: ${header}, ignoring`)
|
|
24
|
+
return acc
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
acc[key] = value
|
|
28
|
+
return acc
|
|
29
|
+
}, {})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const headers = ({
|
|
33
|
+
argv,
|
|
34
|
+
logger,
|
|
35
|
+
}: {
|
|
36
|
+
argv: {
|
|
37
|
+
header: (string | number)[]
|
|
38
|
+
oauth2Bearer: string | undefined
|
|
39
|
+
}
|
|
40
|
+
logger: Logger
|
|
41
|
+
}): Record<string, string> => {
|
|
42
|
+
const headers = parseHeaders({
|
|
43
|
+
argvHeader: argv.header,
|
|
44
|
+
logger,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
if ('oauth2Bearer' in argv) {
|
|
48
|
+
return {
|
|
49
|
+
...headers,
|
|
50
|
+
Authorization: `Bearer ${argv.oauth2Bearer}`,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return headers
|
|
55
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js'
|
|
2
|
+
import { Collection, MongoClient } from 'mongodb'
|
|
3
|
+
|
|
4
|
+
export class McpServerLogRepository {
|
|
5
|
+
private readonly collection: Collection
|
|
6
|
+
|
|
7
|
+
constructor(
|
|
8
|
+
private readonly client: MongoClient,
|
|
9
|
+
dbName = 'local',
|
|
10
|
+
collectionName = 'mcp_server_logs',
|
|
11
|
+
) {
|
|
12
|
+
this.collection = this.client.db(dbName).collection(collectionName)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
insert(data: McpServerLogDto) {
|
|
16
|
+
return this.collection.insertOne(data)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
update(
|
|
20
|
+
sessionId: string,
|
|
21
|
+
update: {
|
|
22
|
+
result?: unknown
|
|
23
|
+
id: number
|
|
24
|
+
error?: { message: string; code: number }
|
|
25
|
+
},
|
|
26
|
+
) {
|
|
27
|
+
return this.collection.updateOne(
|
|
28
|
+
{ sessionId, 'data.id': update.id },
|
|
29
|
+
{
|
|
30
|
+
$set: {
|
|
31
|
+
'data.result': update.result,
|
|
32
|
+
'data.error': update.error,
|
|
33
|
+
updatedAt: new Date(),
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type McpServerLogDto = {
|
|
41
|
+
ip: string
|
|
42
|
+
type: 'rpc' | 'error' | 'system'
|
|
43
|
+
userId: string
|
|
44
|
+
sessionId: string
|
|
45
|
+
data: JSONRPCMessage | string
|
|
46
|
+
createdAt: Date
|
|
47
|
+
updatedAt: Date
|
|
48
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Logger } from '../types.js'
|
|
2
|
+
|
|
3
|
+
export interface OnSignalsOptions {
|
|
4
|
+
logger: Logger
|
|
5
|
+
cleanup?: () => void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sets up signal handlers for graceful shutdown.
|
|
10
|
+
*
|
|
11
|
+
* @param options Configuration options
|
|
12
|
+
* @param options.logger Logger instance
|
|
13
|
+
* @param options.cleanup Optional cleanup function to be called before exit
|
|
14
|
+
*/
|
|
15
|
+
export function onSignals(options: OnSignalsOptions): void {
|
|
16
|
+
const { logger, cleanup } = options
|
|
17
|
+
|
|
18
|
+
const handleSignal = (signal: string) => {
|
|
19
|
+
logger.info(`Caught ${signal}. Exiting...`)
|
|
20
|
+
if (cleanup) {
|
|
21
|
+
cleanup()
|
|
22
|
+
}
|
|
23
|
+
process.exit(0)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
process.on('SIGINT', () => handleSignal('SIGINT'))
|
|
27
|
+
|
|
28
|
+
process.on('SIGTERM', () => handleSignal('SIGTERM'))
|
|
29
|
+
|
|
30
|
+
process.on('SIGHUP', () => handleSignal('SIGHUP'))
|
|
31
|
+
|
|
32
|
+
process.stdin.on('close', () => {
|
|
33
|
+
logger.info('stdin closed. Exiting...')
|
|
34
|
+
if (cleanup) {
|
|
35
|
+
cleanup()
|
|
36
|
+
}
|
|
37
|
+
process.exit(0)
|
|
38
|
+
})
|
|
39
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CorsOptions } from 'cors'
|
|
2
|
+
|
|
3
|
+
export const serializeCorsOrigin = ({
|
|
4
|
+
corsOrigin,
|
|
5
|
+
}: {
|
|
6
|
+
corsOrigin: CorsOptions['origin']
|
|
7
|
+
}) =>
|
|
8
|
+
JSON.stringify(corsOrigin, (_key, value) => {
|
|
9
|
+
if (value instanceof RegExp) {
|
|
10
|
+
return value.toString()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return value
|
|
14
|
+
})
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Logger } from '../types.js'
|
|
2
|
+
|
|
3
|
+
export class SessionAccessCounter {
|
|
4
|
+
private sessions: Map<
|
|
5
|
+
string,
|
|
6
|
+
{ accessCount: number } | { timeout: NodeJS.Timeout }
|
|
7
|
+
> = new Map()
|
|
8
|
+
|
|
9
|
+
constructor(
|
|
10
|
+
public timeout: number,
|
|
11
|
+
public cleanup: (sessionId: string) => unknown,
|
|
12
|
+
public logger: Logger,
|
|
13
|
+
) {}
|
|
14
|
+
|
|
15
|
+
inc(sessionId: string, reason: string) {
|
|
16
|
+
this.logger.info(
|
|
17
|
+
`SessionAccessCounter.inc() ${sessionId}, caused by ${reason}`,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
const session = this.sessions.get(sessionId)
|
|
21
|
+
|
|
22
|
+
if (!session) {
|
|
23
|
+
// New session
|
|
24
|
+
this.logger.info(
|
|
25
|
+
`Session access count 0 -> 1 for ${sessionId} (new session)`,
|
|
26
|
+
)
|
|
27
|
+
this.sessions.set(sessionId, { accessCount: 1 })
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if ('timeout' in session) {
|
|
32
|
+
// Clear pending cleanup and reactivate
|
|
33
|
+
this.logger.info(
|
|
34
|
+
`Session access count 0 -> 1, clearing cleanup timeout for ${sessionId}`,
|
|
35
|
+
)
|
|
36
|
+
clearTimeout(session.timeout)
|
|
37
|
+
this.sessions.set(sessionId, { accessCount: 1 })
|
|
38
|
+
} else {
|
|
39
|
+
// Increment active session
|
|
40
|
+
this.logger.info(
|
|
41
|
+
`Session access count ${session.accessCount} -> ${session.accessCount + 1} for ${sessionId}`,
|
|
42
|
+
)
|
|
43
|
+
session.accessCount++
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
dec(sessionId: string, reason: string) {
|
|
48
|
+
this.logger.info(
|
|
49
|
+
`SessionAccessCounter.dec() ${sessionId}, caused by ${reason}`,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const session = this.sessions.get(sessionId)
|
|
53
|
+
|
|
54
|
+
if (!session) {
|
|
55
|
+
this.logger.error(
|
|
56
|
+
`Called dec() on non-existent session ${sessionId}, ignoring`,
|
|
57
|
+
)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if ('timeout' in session) {
|
|
62
|
+
this.logger.error(
|
|
63
|
+
`Called dec() on session ${sessionId} that is already pending cleanup, ignoring`,
|
|
64
|
+
)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (session.accessCount <= 0) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Invalid access count ${session.accessCount} for session ${sessionId}`,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
session.accessCount--
|
|
75
|
+
this.logger.info(
|
|
76
|
+
`Session access count ${session.accessCount + 1} -> ${session.accessCount} for ${sessionId}`,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if (session.accessCount === 0) {
|
|
80
|
+
this.logger.info(
|
|
81
|
+
`Session access count reached 0, setting cleanup timeout for ${sessionId}`,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
this.sessions.set(sessionId, {
|
|
85
|
+
timeout: setTimeout(() => {
|
|
86
|
+
this.logger.info(`Session ${sessionId} timed out, cleaning up`)
|
|
87
|
+
this.sessions.delete(sessionId)
|
|
88
|
+
this.cleanup(sessionId)
|
|
89
|
+
}, this.timeout),
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
clear(sessionId: string, runCleanup: boolean, reason: string) {
|
|
95
|
+
this.logger.info(
|
|
96
|
+
`SessionAccessCounter.clear() ${sessionId}, caused by ${reason}`,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
const session = this.sessions.get(sessionId)
|
|
100
|
+
if (!session) {
|
|
101
|
+
this.logger.info(`Attempted to clear non-existent session ${sessionId}`)
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Clear any pending timeout
|
|
106
|
+
if ('timeout' in session) {
|
|
107
|
+
clearTimeout(session.timeout)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Remove from tracking
|
|
111
|
+
this.sessions.delete(sessionId)
|
|
112
|
+
|
|
113
|
+
// Run cleanup if requested
|
|
114
|
+
if (runCleanup) {
|
|
115
|
+
this.cleanup(sessionId)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Transport,
|
|
3
|
+
TransportSendOptions,
|
|
4
|
+
} from '@modelcontextprotocol/sdk/shared/transport.js'
|
|
5
|
+
import { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js'
|
|
6
|
+
import { v4 as uuidv4 } from 'uuid'
|
|
7
|
+
import { WebSocket, WebSocketServer } from 'ws'
|
|
8
|
+
import { Server } from 'http'
|
|
9
|
+
|
|
10
|
+
export class WebSocketServerTransport implements Transport {
|
|
11
|
+
private wss!: WebSocketServer
|
|
12
|
+
private clients: Map<string, WebSocket> = new Map()
|
|
13
|
+
|
|
14
|
+
onclose?: () => void
|
|
15
|
+
onerror?: (err: Error) => void
|
|
16
|
+
private messageHandler?: (msg: JSONRPCMessage, clientId: string) => void
|
|
17
|
+
onconnection?: (clientId: string) => void
|
|
18
|
+
ondisconnection?: (clientId: string) => void
|
|
19
|
+
|
|
20
|
+
set onmessage(handler: ((message: JSONRPCMessage) => void) | undefined) {
|
|
21
|
+
this.messageHandler = handler
|
|
22
|
+
? (msg, clientId) => {
|
|
23
|
+
// @ts-ignore
|
|
24
|
+
if (msg.id === undefined) {
|
|
25
|
+
console.log('Broadcast message:', msg)
|
|
26
|
+
return handler(msg)
|
|
27
|
+
}
|
|
28
|
+
// @ts-ignore
|
|
29
|
+
return handler({
|
|
30
|
+
...msg,
|
|
31
|
+
// @ts-ignore
|
|
32
|
+
id: clientId + ':' + msg.id,
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
: undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
constructor({ path, server }: { path: string; server: Server }) {
|
|
39
|
+
this.wss = new WebSocketServer({
|
|
40
|
+
path,
|
|
41
|
+
server,
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async start(): Promise<void> {
|
|
46
|
+
this.wss.on('connection', (ws: WebSocket) => {
|
|
47
|
+
const clientId = uuidv4()
|
|
48
|
+
this.clients.set(clientId, ws)
|
|
49
|
+
this.onconnection?.(clientId)
|
|
50
|
+
|
|
51
|
+
ws.on('message', (data: Buffer) => {
|
|
52
|
+
try {
|
|
53
|
+
const msg = JSON.parse(data.toString())
|
|
54
|
+
this.messageHandler?.(msg, clientId)
|
|
55
|
+
} catch (err) {
|
|
56
|
+
this.onerror?.(new Error(`Failed to parse message: ${err}`))
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
ws.on('close', () => {
|
|
61
|
+
this.clients.delete(clientId)
|
|
62
|
+
this.ondisconnection?.(clientId)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
ws.on('error', (err: Error) => {
|
|
66
|
+
this.onerror?.(err)
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async send(
|
|
72
|
+
msg: JSONRPCMessage,
|
|
73
|
+
options?: TransportSendOptions | string,
|
|
74
|
+
): Promise<void> {
|
|
75
|
+
// decide if they passed a raw clientId (legacy) or options object
|
|
76
|
+
const clientId = typeof options === 'string' ? options : undefined
|
|
77
|
+
|
|
78
|
+
// if your protocol mangles IDs to include clientId, strip it off
|
|
79
|
+
const [cId, rawId] = clientId?.split(':') ?? []
|
|
80
|
+
if (rawId !== undefined) {
|
|
81
|
+
// @ts-ignore
|
|
82
|
+
msg.id = parseInt(rawId, 10)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const payload = JSON.stringify(msg)
|
|
86
|
+
|
|
87
|
+
if (cId) {
|
|
88
|
+
// send only to the one client
|
|
89
|
+
const ws = this.clients.get(cId)
|
|
90
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
91
|
+
ws.send(payload)
|
|
92
|
+
} else {
|
|
93
|
+
this.clients.delete(cId)
|
|
94
|
+
this.ondisconnection?.(cId)
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
// broadcast to everyone
|
|
98
|
+
for (const [id, ws] of this.clients) {
|
|
99
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
100
|
+
ws.send(payload)
|
|
101
|
+
} else {
|
|
102
|
+
this.clients.delete(id)
|
|
103
|
+
this.ondisconnection?.(id)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async broadcast(msg: JSONRPCMessage): Promise<void> {
|
|
110
|
+
return this.send(msg)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async close(): Promise<void> {
|
|
114
|
+
return new Promise((resolve) => {
|
|
115
|
+
this.wss.close(() => {
|
|
116
|
+
this.clients.clear()
|
|
117
|
+
resolve()
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
}
|