omni-playground 0.1.0
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/LICENSE +201 -0
- package/README.md +73 -0
- package/bin/omni-playground.js +62 -0
- package/dist/web/assets/bricolage-grotesque-latin-400-normal-A6LyuA6R.woff2 +0 -0
- package/dist/web/assets/bricolage-grotesque-latin-400-normal-D89K-qEP.woff +0 -0
- package/dist/web/assets/bricolage-grotesque-latin-600-normal-Bw0J83-2.woff +0 -0
- package/dist/web/assets/bricolage-grotesque-latin-600-normal-nxTgbNFE.woff2 +0 -0
- package/dist/web/assets/bricolage-grotesque-latin-700-normal-DvJDmrLu.woff +0 -0
- package/dist/web/assets/bricolage-grotesque-latin-700-normal-gtcctNPv.woff2 +0 -0
- package/dist/web/assets/bricolage-grotesque-latin-ext-400-normal-BH175q6o.woff +0 -0
- package/dist/web/assets/bricolage-grotesque-latin-ext-400-normal-D42HmrHD.woff2 +0 -0
- package/dist/web/assets/bricolage-grotesque-latin-ext-600-normal-CN5JKkBm.woff2 +0 -0
- package/dist/web/assets/bricolage-grotesque-latin-ext-600-normal-DF26P3Q7.woff +0 -0
- package/dist/web/assets/bricolage-grotesque-latin-ext-700-normal-BXpPFNFj.woff +0 -0
- package/dist/web/assets/bricolage-grotesque-latin-ext-700-normal-fBkt-7fi.woff2 +0 -0
- package/dist/web/assets/bricolage-grotesque-vietnamese-400-normal-B7Iv8-Rg.woff2 +0 -0
- package/dist/web/assets/bricolage-grotesque-vietnamese-400-normal-Sle7MYWg.woff +0 -0
- package/dist/web/assets/bricolage-grotesque-vietnamese-600-normal-BppAn7jL.woff +0 -0
- package/dist/web/assets/bricolage-grotesque-vietnamese-600-normal-Ipewt4hn.woff2 +0 -0
- package/dist/web/assets/bricolage-grotesque-vietnamese-700-normal-BLYPkJbo.woff2 +0 -0
- package/dist/web/assets/bricolage-grotesque-vietnamese-700-normal-CHrFuVHU.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/dist/web/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/dist/web/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/dist/web/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/dist/web/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dist/web/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/dist/web/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/dist/web/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/dist/web/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/dist/web/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/dist/web/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/dist/web/assets/index-B56r5dOo.css +1 -0
- package/dist/web/assets/index-BtldC4pA.js +74 -0
- package/dist/web/assets/instrument-sans-latin-400-normal-D1W7dsQl.woff +0 -0
- package/dist/web/assets/instrument-sans-latin-400-normal-DRC__1Mx.woff2 +0 -0
- package/dist/web/assets/instrument-sans-latin-500-normal-Dk9ku72i.woff2 +0 -0
- package/dist/web/assets/instrument-sans-latin-500-normal-Z6ESRlEs.woff +0 -0
- package/dist/web/assets/instrument-sans-latin-600-normal-B7fBEWYG.woff2 +0 -0
- package/dist/web/assets/instrument-sans-latin-600-normal-B9e8oLYv.woff +0 -0
- package/dist/web/assets/instrument-sans-latin-ext-400-normal-Q_nF8v4l.woff2 +0 -0
- package/dist/web/assets/instrument-sans-latin-ext-400-normal-r32jotim.woff +0 -0
- package/dist/web/assets/instrument-sans-latin-ext-500-normal-CAxz3nsc.woff +0 -0
- package/dist/web/assets/instrument-sans-latin-ext-500-normal-CTEe1bJa.woff2 +0 -0
- package/dist/web/assets/instrument-sans-latin-ext-600-normal-BsaQcF38.woff2 +0 -0
- package/dist/web/assets/instrument-sans-latin-ext-600-normal-DMks36a2.woff +0 -0
- package/dist/web/index.html +18 -0
- package/package.json +61 -0
- package/server/gateway.d.ts +10 -0
- package/server/gateway.js +166 -0
- package/server/index.js +73 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<meta name="color-scheme" content="dark" />
|
|
7
|
+
<title>omni-playground</title>
|
|
8
|
+
<link
|
|
9
|
+
rel="icon"
|
|
10
|
+
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Ccircle cx='16' cy='16' r='13' fill='none' stroke='%23ffb224' stroke-width='3'/%3E%3Ccircle cx='16' cy='16' r='5' fill='%23ffb224'/%3E%3C/svg%3E"
|
|
11
|
+
/>
|
|
12
|
+
<script type="module" crossorigin src="/assets/index-BtldC4pA.js"></script>
|
|
13
|
+
<link rel="stylesheet" crossorigin href="/assets/index-B56r5dOo.css">
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<div id="root"></div>
|
|
17
|
+
</body>
|
|
18
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "omni-playground",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A local web playground for multimodal generation — chat, text-to-image, image editing, video, realtime voice and streaming against vLLM-omni endpoints.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/FayeSpica/omni-playground.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/FayeSpica/omni-playground#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/FayeSpica/omni-playground/issues"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"omni-playground": "bin/omni-playground.js"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"registry": "https://registry.npmjs.org/"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"bin",
|
|
23
|
+
"server",
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"dev": "vite --config web/vite.config.ts",
|
|
31
|
+
"build": "tsc -p web/tsconfig.json --noEmit && vite build --config web/vite.config.ts",
|
|
32
|
+
"start": "node bin/omni-playground.js",
|
|
33
|
+
"prepublishOnly": "npm run build"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"vllm",
|
|
37
|
+
"vllm-omni",
|
|
38
|
+
"multimodal",
|
|
39
|
+
"playground",
|
|
40
|
+
"text-to-image",
|
|
41
|
+
"image-to-video",
|
|
42
|
+
"realtime",
|
|
43
|
+
"text-to-speech",
|
|
44
|
+
"websocket",
|
|
45
|
+
"webui"
|
|
46
|
+
],
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@fontsource/bricolage-grotesque": "^5.2.10",
|
|
49
|
+
"@fontsource/ibm-plex-mono": "^5.2.7",
|
|
50
|
+
"@fontsource/instrument-sans": "^5.2.8",
|
|
51
|
+
"@types/node": "^26.1.0",
|
|
52
|
+
"@types/react": "^19.2.17",
|
|
53
|
+
"@types/react-dom": "^19.2.3",
|
|
54
|
+
"@vitejs/plugin-react": "^6.0.3",
|
|
55
|
+
"marked": "^18.0.5",
|
|
56
|
+
"react": "^19.2.7",
|
|
57
|
+
"react-dom": "^19.2.7",
|
|
58
|
+
"typescript": "^6.0.3",
|
|
59
|
+
"vite": "^8.1.3"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
|
+
import type { Duplex } from 'node:stream'
|
|
3
|
+
|
|
4
|
+
export interface Gateway {
|
|
5
|
+
handle(req: IncomingMessage, res: ServerResponse): boolean
|
|
6
|
+
handleUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer): boolean
|
|
7
|
+
readonly target: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createGateway(initialTarget: string): Gateway
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import http from 'node:http'
|
|
2
|
+
import https from 'node:https'
|
|
3
|
+
|
|
4
|
+
// Hop-by-hop headers must not be forwarded (RFC 7230 §6.1)
|
|
5
|
+
const HOP_BY_HOP = new Set([
|
|
6
|
+
'connection',
|
|
7
|
+
'keep-alive',
|
|
8
|
+
'proxy-authenticate',
|
|
9
|
+
'proxy-authorization',
|
|
10
|
+
'te',
|
|
11
|
+
'trailer',
|
|
12
|
+
'transfer-encoding',
|
|
13
|
+
'upgrade',
|
|
14
|
+
])
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* API gateway shared by the packaged server and the vite dev server:
|
|
18
|
+
* proxies /api/* to the vLLM-omni target and serves /playground/config
|
|
19
|
+
* for reading/updating that target at runtime.
|
|
20
|
+
*/
|
|
21
|
+
export function createGateway(initialTarget) {
|
|
22
|
+
let targetUrl = new URL(initialTarget)
|
|
23
|
+
|
|
24
|
+
function proxy(req, res) {
|
|
25
|
+
const upstreamPath = req.url.replace(/^\/api/, '') || '/'
|
|
26
|
+
const isHttps = targetUrl.protocol === 'https:'
|
|
27
|
+
const headers = {}
|
|
28
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
29
|
+
if (!HOP_BY_HOP.has(k) && k !== 'host') headers[k] = v
|
|
30
|
+
}
|
|
31
|
+
const upstream = (isHttps ? https : http).request(
|
|
32
|
+
{
|
|
33
|
+
hostname: targetUrl.hostname,
|
|
34
|
+
port: targetUrl.port || (isHttps ? 443 : 80),
|
|
35
|
+
path: upstreamPath,
|
|
36
|
+
method: req.method,
|
|
37
|
+
headers,
|
|
38
|
+
},
|
|
39
|
+
(upRes) => {
|
|
40
|
+
const resHeaders = {}
|
|
41
|
+
for (const [k, v] of Object.entries(upRes.headers)) {
|
|
42
|
+
if (!HOP_BY_HOP.has(k)) resHeaders[k] = v
|
|
43
|
+
}
|
|
44
|
+
res.writeHead(upRes.statusCode, resHeaders)
|
|
45
|
+
upRes.pipe(res)
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
upstream.on('error', (err) => {
|
|
49
|
+
if (!res.headersSent) {
|
|
50
|
+
res.writeHead(502, { 'content-type': 'application/json' })
|
|
51
|
+
}
|
|
52
|
+
res.end(
|
|
53
|
+
JSON.stringify({
|
|
54
|
+
error: 'upstream_unreachable',
|
|
55
|
+
message: `cannot reach ${targetUrl.origin}: ${err.message}`,
|
|
56
|
+
})
|
|
57
|
+
)
|
|
58
|
+
})
|
|
59
|
+
req.pipe(upstream)
|
|
60
|
+
// If the browser aborts (e.g. user cancels a stream), tear down upstream too
|
|
61
|
+
res.on('close', () => upstream.destroy())
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// WebSocket / upgrade proxy — the omni realtime endpoints (/v1/realtime,
|
|
65
|
+
// /v1/realtime/video) speak WebSocket, which the plain HTTP proxy above can't
|
|
66
|
+
// carry. We forward the handshake with http.request and then tunnel the raw
|
|
67
|
+
// socket both ways; no frame parsing needed since we only relay bytes.
|
|
68
|
+
function proxyUpgrade(req, clientSocket, head) {
|
|
69
|
+
const upstreamPath = req.url.replace(/^\/api/, '') || '/'
|
|
70
|
+
const isHttps = targetUrl.protocol === 'https:'
|
|
71
|
+
// Keep connection/upgrade/sec-websocket-* headers intact — they carry the
|
|
72
|
+
// handshake — but point Host at the upstream.
|
|
73
|
+
const headers = {}
|
|
74
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
75
|
+
if (k !== 'host') headers[k] = v
|
|
76
|
+
}
|
|
77
|
+
headers.host = targetUrl.host
|
|
78
|
+
const upstreamReq = (isHttps ? https : http).request({
|
|
79
|
+
hostname: targetUrl.hostname,
|
|
80
|
+
port: targetUrl.port || (isHttps ? 443 : 80),
|
|
81
|
+
path: upstreamPath,
|
|
82
|
+
method: req.method,
|
|
83
|
+
headers,
|
|
84
|
+
})
|
|
85
|
+
upstreamReq.on('upgrade', (upRes, upstreamSocket, upstreamHead) => {
|
|
86
|
+
const statusLine = `HTTP/1.1 ${upRes.statusCode} ${upRes.statusMessage}\r\n`
|
|
87
|
+
const resHeaders = Object.entries(upRes.headers)
|
|
88
|
+
.map(([k, v]) => `${k}: ${v}\r\n`)
|
|
89
|
+
.join('')
|
|
90
|
+
clientSocket.write(statusLine + resHeaders + '\r\n')
|
|
91
|
+
if (upstreamHead && upstreamHead.length) clientSocket.write(upstreamHead)
|
|
92
|
+
upstreamSocket.pipe(clientSocket)
|
|
93
|
+
clientSocket.pipe(upstreamSocket)
|
|
94
|
+
const teardown = () => {
|
|
95
|
+
upstreamSocket.destroy()
|
|
96
|
+
clientSocket.destroy()
|
|
97
|
+
}
|
|
98
|
+
upstreamSocket.on('error', teardown)
|
|
99
|
+
clientSocket.on('error', teardown)
|
|
100
|
+
upstreamSocket.on('close', () => clientSocket.destroy())
|
|
101
|
+
clientSocket.on('close', () => upstreamSocket.destroy())
|
|
102
|
+
})
|
|
103
|
+
upstreamReq.on('error', () => clientSocket.destroy())
|
|
104
|
+
// Non-101 upstream response (e.g. auth/404): relay it then close.
|
|
105
|
+
upstreamReq.on('response', (upRes) => {
|
|
106
|
+
const statusLine = `HTTP/1.1 ${upRes.statusCode} ${upRes.statusMessage}\r\n`
|
|
107
|
+
const resHeaders = Object.entries(upRes.headers)
|
|
108
|
+
.map(([k, v]) => `${k}: ${v}\r\n`)
|
|
109
|
+
.join('')
|
|
110
|
+
clientSocket.write(statusLine + resHeaders + '\r\n')
|
|
111
|
+
upRes.pipe(clientSocket)
|
|
112
|
+
})
|
|
113
|
+
if (head && head.length) upstreamReq.write(head)
|
|
114
|
+
upstreamReq.end()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function handleConfig(req, res) {
|
|
118
|
+
if (req.method === 'GET') {
|
|
119
|
+
res.writeHead(200, { 'content-type': 'application/json' })
|
|
120
|
+
return res.end(JSON.stringify({ target: targetUrl.origin }))
|
|
121
|
+
}
|
|
122
|
+
if (req.method === 'POST') {
|
|
123
|
+
let body = ''
|
|
124
|
+
req.on('data', (c) => (body += c))
|
|
125
|
+
req.on('end', () => {
|
|
126
|
+
try {
|
|
127
|
+
targetUrl = new URL(JSON.parse(body).target)
|
|
128
|
+
res.writeHead(200, { 'content-type': 'application/json' })
|
|
129
|
+
res.end(JSON.stringify({ target: targetUrl.origin }))
|
|
130
|
+
} catch {
|
|
131
|
+
res.writeHead(400, { 'content-type': 'application/json' })
|
|
132
|
+
res.end(JSON.stringify({ error: 'invalid_target' }))
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
res.writeHead(405)
|
|
138
|
+
res.end()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
/** Returns true if the request was an /api or /playground/config request and was handled. */
|
|
143
|
+
handle(req, res) {
|
|
144
|
+
if (req.url.startsWith('/api/')) {
|
|
145
|
+
proxy(req, res)
|
|
146
|
+
return true
|
|
147
|
+
}
|
|
148
|
+
if (req.url.startsWith('/playground/config')) {
|
|
149
|
+
handleConfig(req, res)
|
|
150
|
+
return true
|
|
151
|
+
}
|
|
152
|
+
return false
|
|
153
|
+
},
|
|
154
|
+
/** Returns true if the upgrade was an /api WebSocket request and was tunneled. */
|
|
155
|
+
handleUpgrade(req, socket, head) {
|
|
156
|
+
if (req.url.startsWith('/api/')) {
|
|
157
|
+
proxyUpgrade(req, socket, head)
|
|
158
|
+
return true
|
|
159
|
+
}
|
|
160
|
+
return false
|
|
161
|
+
},
|
|
162
|
+
get target() {
|
|
163
|
+
return targetUrl.origin
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
}
|
package/server/index.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import http from 'node:http'
|
|
2
|
+
import { createReadStream, existsSync, statSync } from 'node:fs'
|
|
3
|
+
import { extname, join, normalize, resolve } from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { createGateway } from './gateway.js'
|
|
6
|
+
|
|
7
|
+
const WEB_ROOT = resolve(fileURLToPath(new URL('../dist/web', import.meta.url)))
|
|
8
|
+
|
|
9
|
+
const MIME = {
|
|
10
|
+
'.html': 'text/html; charset=utf-8',
|
|
11
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
12
|
+
'.css': 'text/css; charset=utf-8',
|
|
13
|
+
'.json': 'application/json; charset=utf-8',
|
|
14
|
+
'.svg': 'image/svg+xml',
|
|
15
|
+
'.png': 'image/png',
|
|
16
|
+
'.jpg': 'image/jpeg',
|
|
17
|
+
'.ico': 'image/x-icon',
|
|
18
|
+
'.woff': 'font/woff',
|
|
19
|
+
'.woff2': 'font/woff2',
|
|
20
|
+
'.webmanifest': 'application/manifest+json',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function startServer({ target, port, host }) {
|
|
24
|
+
const gateway = createGateway(target)
|
|
25
|
+
|
|
26
|
+
function serveStatic(req, res) {
|
|
27
|
+
const pathname = decodeURIComponent(new URL(req.url, 'http://x').pathname)
|
|
28
|
+
let filePath = normalize(join(WEB_ROOT, pathname))
|
|
29
|
+
if (!filePath.startsWith(WEB_ROOT)) {
|
|
30
|
+
res.writeHead(403)
|
|
31
|
+
return res.end()
|
|
32
|
+
}
|
|
33
|
+
if (!existsSync(filePath) || statSync(filePath).isDirectory()) {
|
|
34
|
+
filePath = join(WEB_ROOT, 'index.html') // SPA fallback
|
|
35
|
+
}
|
|
36
|
+
if (!existsSync(filePath)) {
|
|
37
|
+
res.writeHead(404, { 'content-type': 'text/plain' })
|
|
38
|
+
return res.end(
|
|
39
|
+
'omni-playground: web assets not found. Run `npm run build` first (dist/web missing).'
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
const type = MIME[extname(filePath)] || 'application/octet-stream'
|
|
43
|
+
const cache = pathname.startsWith('/assets/')
|
|
44
|
+
? 'public, max-age=31536000, immutable'
|
|
45
|
+
: 'no-cache'
|
|
46
|
+
res.writeHead(200, { 'content-type': type, 'cache-control': cache })
|
|
47
|
+
createReadStream(filePath).pipe(res)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const server = http.createServer((req, res) => {
|
|
51
|
+
if (gateway.handle(req, res)) return
|
|
52
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
53
|
+
res.writeHead(405)
|
|
54
|
+
return res.end()
|
|
55
|
+
}
|
|
56
|
+
serveStatic(req, res)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// Tunnel WebSocket upgrades (e.g. /api/v1/realtime) through to the target.
|
|
60
|
+
server.on('upgrade', (req, socket, head) => {
|
|
61
|
+
if (!gateway.handleUpgrade(req, socket, head)) socket.destroy()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
return new Promise((resolvePromise, reject) => {
|
|
65
|
+
server.on('error', reject)
|
|
66
|
+
server.listen(port, host, () => {
|
|
67
|
+
const url = `http://${host}:${port}`
|
|
68
|
+
console.log(`\n ◉ omni-playground ${url}`)
|
|
69
|
+
console.log(` proxying /api → ${gateway.target}\n`)
|
|
70
|
+
resolvePromise({ server, url })
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
}
|