mirror of
https://github.com/tale/headplane.git
synced 2026-08-18 09:03:14 +00:00
chore: switch to vite for server dev
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
import { createRequestHandler } from 'react-router'
|
||||
|
||||
export default createRequestHandler(
|
||||
// @ts-expect-error: React Router Vite plugin
|
||||
() => import('virtual:react-router/server-build'),
|
||||
'development'
|
||||
);
|
||||
@@ -1,33 +0,0 @@
|
||||
// This is a polyglot entrypoint for Headplane when running in development
|
||||
// It does some silly magic to load the vite config, set some globals that
|
||||
// are required to function, and create the vite development server.
|
||||
import { createServer } from 'vite';
|
||||
import { env, exit } from 'node:process';
|
||||
import { log } from './utils.mjs';
|
||||
|
||||
log('DEVX', 'INFO', 'This script is only intended for development');
|
||||
env.NODE_ENV = 'development';
|
||||
|
||||
// The production entrypoint uses a global called "PREFIX" to determine
|
||||
// what route the application is being served at and a global called "BUILD"
|
||||
// to determine the Remix handler. We need to set these globals here so that
|
||||
// the development server can function correctly and override the production
|
||||
// values.
|
||||
|
||||
log('DEVX', 'INFO', 'Creating Vite Development Server');
|
||||
const server = await createServer({
|
||||
server: {
|
||||
middlewareMode: true,
|
||||
},
|
||||
});
|
||||
|
||||
// This entrypoint is defined in the documentation to load the server
|
||||
const build = await server.ssrLoadModule('virtual:react-router/server-build');
|
||||
|
||||
// We already handle this logic in the Vite configuration
|
||||
global.PREFIX = server.config.base.slice(0, -1);
|
||||
global.BUILD = build;
|
||||
global.MODE = 'development';
|
||||
global.MIDDLEWARE = server.middlewares;
|
||||
|
||||
await import('./prod.mjs');
|
||||
@@ -0,0 +1,57 @@
|
||||
import log from '~server/log';
|
||||
import { createServer, type ViteDevServer } from 'vite';
|
||||
import { type createRequestHandler } from 'react-router';
|
||||
|
||||
let server: ViteDevServer | undefined;
|
||||
export async function loadDevtools() {
|
||||
log.info('DEVX', 'Starting Vite Development server')
|
||||
process.env.NODE_ENV = 'development';
|
||||
|
||||
// This is loading the ROOT vite.config.ts
|
||||
server = await createServer({
|
||||
server: {
|
||||
middlewareMode: true,
|
||||
}
|
||||
});
|
||||
|
||||
// We can't just do ssrLoadModule for virtual:react-router/server-build
|
||||
// because for hot reload to work server side it needs to be imported
|
||||
// using builtin import in its own file.
|
||||
const handler = await server.ssrLoadModule('./server/dev-handler.ts');
|
||||
return {
|
||||
server,
|
||||
handler: handler.default,
|
||||
};
|
||||
}
|
||||
|
||||
export async function stacksafeTry(
|
||||
devtools: {
|
||||
server: ViteDevServer,
|
||||
handler: any, // import() is dynamic
|
||||
},
|
||||
req: Request,
|
||||
context: unknown
|
||||
) {
|
||||
try {
|
||||
const result = await devtools.handler(req, context);
|
||||
return result;
|
||||
} catch (error) {
|
||||
log.error('DEVX', 'Error in request handler', error);
|
||||
if (typeof error === 'object' && error instanceof Error) {
|
||||
console.log('got error');
|
||||
devtools.server.ssrFixStacktrace(error);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.on('vite:beforeFullReload', () => {
|
||||
server?.close();
|
||||
});
|
||||
|
||||
import.meta.hot.dispose(() => {
|
||||
server?.close();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { createServer } from 'node:http';
|
||||
import { listener } from '~server/listener';
|
||||
import { initWebsocket } from '~server/ws';
|
||||
import { access, constants } from 'node:fs/promises';
|
||||
import log from '~server/log';
|
||||
|
||||
log.info('SRVX', 'Running Node.js %s', process.versions.node);
|
||||
|
||||
try {
|
||||
await access('./node_modules/react-router', constants.F_OK | constants.R_OK);
|
||||
log.info('SRVX', 'Found dependencies');
|
||||
} catch (error) {
|
||||
log.error('SRVX', 'No dependencies found. Please run `npm install`');
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const server = createServer(listener);
|
||||
const port = process.env.PORT || 3000;
|
||||
const host = process.env.HOST || '0.0.0.0';
|
||||
|
||||
const ws = initWebsocket();
|
||||
if (ws) {
|
||||
server.on('upgrade', (req, socket, head) => {
|
||||
ws.handleUpgrade(req, socket, head, (ws) => {
|
||||
ws.emit('connection', ws, req);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
server.listen(port, host, () => {
|
||||
log.info('SRVX', 'Running on %s:%s', host, port);
|
||||
});
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.on('vite:beforeFullReload', () => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
import.meta.hot.dispose(() => {
|
||||
server.close();
|
||||
});
|
||||
}
|
||||
|
||||
// export const app = listener;
|
||||
@@ -0,0 +1,137 @@
|
||||
import { type RequestListener } from 'node:http';
|
||||
import { resolve, join } from 'node:path'
|
||||
import { createServer } from 'vite'
|
||||
import { createRequestHandler } from 'react-router'
|
||||
import { access, constants } from 'node:fs/promises';
|
||||
import { createReadStream, existsSync, statSync } from 'node:fs';
|
||||
import {
|
||||
createReadableStreamFromReadable,
|
||||
writeReadableStreamToWritable,
|
||||
} from '@react-router/node';
|
||||
import mime from 'mime/lite'
|
||||
|
||||
import { loadDevtools, stacksafeTry } from '~server/dev';
|
||||
import { appContext } from '~server/ws';
|
||||
import prodBuild from '~server/prod-handler';
|
||||
|
||||
declare global {
|
||||
// Prefix is a build-time constant
|
||||
const __hp_prefix: string;
|
||||
}
|
||||
|
||||
const devtools = import.meta.env.DEV
|
||||
? await loadDevtools()
|
||||
: undefined;
|
||||
|
||||
const prodHandler = import.meta.env.PROD
|
||||
? await prodBuild()
|
||||
: undefined;
|
||||
|
||||
const buildPath = process.env.BUILD_PATH ?? './build';
|
||||
const baseDir = resolve(join(buildPath, 'client'));
|
||||
|
||||
export const listener: RequestListener = async (req, res) => {
|
||||
const url = new URL(`http://${req.headers.host}${req.url}`);
|
||||
|
||||
// build:strip
|
||||
if (devtools) {
|
||||
await new Promise((resolve) => {
|
||||
devtools.server.middlewares(req, res, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
if (!url.pathname.startsWith(__hp_prefix)) {
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// We need to handle an issue where say we are navigating to $PREFIX
|
||||
// but Remix does not handle it without the trailing slash. This is
|
||||
// because Remix uses the URL constructor to parse the URL and it
|
||||
// will remove the trailing slash. We need to redirect to the correct
|
||||
// URL so that Remix can handle it correctly.
|
||||
if (url.pathname === __hp_prefix) {
|
||||
res.writeHead(301, {
|
||||
Location: `${__hp_prefix}/`,
|
||||
});
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Before we pass any requests to our Remix handler we need to check
|
||||
// if we can handle a raw file request. This is important for the
|
||||
// Remix loader to work correctly.
|
||||
//
|
||||
// To optimize this, we send them as readable streams in the node
|
||||
// response and we also set headers for aggressive caching.
|
||||
if (url.pathname.startsWith(`${__hp_prefix}/assets/`)) {
|
||||
const filePath = join(baseDir, url.pathname.slice(__hp_prefix.length));
|
||||
const exists = existsSync(filePath);
|
||||
const stats = exists ? statSync(filePath) : null;
|
||||
|
||||
if (exists && stats && stats.isFile()) {
|
||||
// Build assets are cache-bust friendly so we can cache them heavily
|
||||
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
}
|
||||
|
||||
// Send the file as a readable stream
|
||||
const fileStream = createReadStream(filePath);
|
||||
const type = mime.getType(filePath);
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': type || 'application/octet-stream',
|
||||
});
|
||||
|
||||
fileStream.pipe(res);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handling the request
|
||||
const controller = new AbortController();
|
||||
res.on('close', () => controller.abort());
|
||||
|
||||
const headers = new Headers();
|
||||
for (const [key, value] of Object.entries(req.headers)) {
|
||||
if (!value) continue;
|
||||
if (Array.isArray(value)) {
|
||||
for (const v of value) {
|
||||
headers.append(key, v);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
headers.append(key, value);
|
||||
}
|
||||
|
||||
const frameworkReq = new Request(url.href, {
|
||||
headers,
|
||||
method: req.method,
|
||||
signal: controller.signal,
|
||||
|
||||
// If we have a body, we set the duplex and load it
|
||||
...(req.method !== 'GET' && req.method !== 'HEAD'
|
||||
? {
|
||||
body: createReadableStreamFromReadable(req),
|
||||
duplex: 'half',
|
||||
} : {}),
|
||||
});
|
||||
|
||||
const response = devtools
|
||||
? await stacksafeTry(devtools, frameworkReq, appContext())
|
||||
: await prodHandler(frameworkReq, appContext());
|
||||
|
||||
res.statusCode = response.status;
|
||||
res.statusMessage = response.statusText;
|
||||
|
||||
for (const [key, value] of response.headers.entries()) {
|
||||
res.appendHeader(key, value);
|
||||
}
|
||||
|
||||
if (response.body) {
|
||||
await writeReadableStreamToWritable(response.body, res);
|
||||
}
|
||||
|
||||
res.end();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
export default {
|
||||
info: (category: string, message: string, ...args: unknown[]) => {
|
||||
defaultLog('INFO', category, message, ...args);
|
||||
},
|
||||
|
||||
warn: (category: string, message: string, ...args: unknown[]) => {
|
||||
defaultLog('WARN', category, message, ...args);
|
||||
},
|
||||
|
||||
error: (category: string, message: string, ...args: unknown[]) => {
|
||||
defaultLog('ERRO', category, message, ...args);
|
||||
},
|
||||
|
||||
debug: (category: string, message: string, ...args: unknown[]) => {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
defaultLog('DEBG', category, message, ...args);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function defaultLog(
|
||||
level: string,
|
||||
category: string,
|
||||
message: string,
|
||||
...args: unknown[]
|
||||
) {
|
||||
const date = new Date().toISOString();
|
||||
console.log(`${date} (${level}) [${category}] ${message}`, ...args);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { createRequestHandler } from 'react-router'
|
||||
import { access, constants } from 'node:fs/promises';
|
||||
import { join, resolve } from 'node:path';
|
||||
import log from '~server/log';
|
||||
|
||||
export default async function() {
|
||||
const buildPath = process.env.BUILD_PATH ?? './build';
|
||||
const server = resolve(join(buildPath, 'server'));
|
||||
|
||||
try {
|
||||
await access(server, constants.F_OK | constants.R_OK);
|
||||
log.info('SRVX', 'Using build directory %s', resolve(buildPath));
|
||||
} catch (error) {
|
||||
log.error('SRVX', 'No build found. Please refer to the documentation');
|
||||
log.error('SRVX', 'https://github.com/tale/headplane/blob/main/docs/integration/Native.md');
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// @vite-ignore
|
||||
const build = await import(resolve(join(server, 'index.js')));
|
||||
return createRequestHandler(build, 'production');
|
||||
}
|
||||
-170
@@ -1,170 +0,0 @@
|
||||
// This is a polyglot entrypoint for Headplane when running in production
|
||||
// It doesn't use any dependencies aside from @remix-run/node and mime
|
||||
// During build we bundle the used dependencies into the file so that
|
||||
// we can only need this file and a Node.js installation to run the server.
|
||||
// PREFIX is defined globally, see vite.config.ts
|
||||
|
||||
import { access, constants } from 'node:fs/promises';
|
||||
import { createReadStream, existsSync, statSync } from 'node:fs';
|
||||
import { createServer } from 'node:http';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { env } from 'node:process';
|
||||
import { log } from './utils.mjs';
|
||||
import { getWss, registerWss } from './ws.mjs';
|
||||
import {
|
||||
createReadableStreamFromReadable,
|
||||
writeReadableStreamToWritable,
|
||||
} from '@react-router/node';
|
||||
|
||||
log('SRVX', 'INFO', `Running with Node.js ${process.versions.node}`);
|
||||
|
||||
try {
|
||||
await access('./node_modules/react-router', constants.F_OK | constants.R_OK);
|
||||
log('SRVX', 'INFO', 'Found node_modules dependencies');
|
||||
} catch (error) {
|
||||
log('SRVX', 'ERROR', 'No node_modules found. Please run `pnpm install`');
|
||||
log('SRVX', 'ERROR', error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { createRequestHandler } = await import('react-router');
|
||||
const { default: mime } = await import('mime');
|
||||
|
||||
const port = env.PORT || 3000;
|
||||
const host = env.HOST || '0.0.0.0';
|
||||
const buildPath = env.BUILD_PATH || './build';
|
||||
const baseDir = resolve(join(buildPath, 'client'));
|
||||
|
||||
if (!global.BUILD) {
|
||||
try {
|
||||
await access(join(buildPath, 'server'), constants.F_OK | constants.R_OK);
|
||||
log('SRVX', 'INFO', 'Found build directory');
|
||||
} catch (error) {
|
||||
const date = new Date().toISOString();
|
||||
log('SRVX', 'ERROR', 'No build found. Please run `pnpm build`');
|
||||
log('SRVX', 'ERROR', error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Because this is a dynamic import without an easily discernable path
|
||||
// we gain the "deoptimization" we want so that Vite doesn't bundle this
|
||||
const build = await import(resolve(join(buildPath, 'server', 'index.js')));
|
||||
global.BUILD = build;
|
||||
global.MODE = 'production';
|
||||
}
|
||||
|
||||
const handler = createRequestHandler(global.BUILD, global.MODE);
|
||||
const http = createServer(async (req, res) => {
|
||||
const url = new URL(`http://${req.headers.host}${req.url}`);
|
||||
|
||||
if (global.MIDDLEWARE) {
|
||||
await new Promise((resolve) => {
|
||||
global.MIDDLEWARE(req, res, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
if (!url.pathname.startsWith(PREFIX)) {
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// We need to handle an issue where say we are navigating to $PREFIX
|
||||
// but Remix does not handle it without the trailing slash. This is
|
||||
// because Remix uses the URL constructor to parse the URL and it
|
||||
// will remove the trailing slash. We need to redirect to the correct
|
||||
// URL so that Remix can handle it correctly.
|
||||
if (url.pathname === PREFIX) {
|
||||
res.writeHead(302, {
|
||||
Location: `${PREFIX}/`,
|
||||
});
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Before we pass any requests to our Remix handler we need to check
|
||||
// if we can handle a raw file request. This is important for the
|
||||
// Remix loader to work correctly.
|
||||
//
|
||||
// To optimize this, we send them as readable streams in the node
|
||||
// response and we also set headers for aggressive caching.
|
||||
if (url.pathname.startsWith(`${PREFIX}/assets/`)) {
|
||||
const filePath = join(baseDir, url.pathname.replace(PREFIX, ''));
|
||||
const exists = existsSync(filePath);
|
||||
const stats = statSync(filePath);
|
||||
|
||||
if (exists && stats.isFile()) {
|
||||
// Build assets are cache-bust friendly so we can cache them heavily
|
||||
if (req.url.startsWith('/build')) {
|
||||
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
}
|
||||
|
||||
// Send the file as a readable stream
|
||||
const fileStream = createReadStream(filePath);
|
||||
const type = mime.getType(filePath);
|
||||
|
||||
res.setHeader('Content-Length', stats.size);
|
||||
res.setHeader('Content-Type', type);
|
||||
fileStream.pipe(res);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Handling the request
|
||||
const controller = new AbortController();
|
||||
res.on('close', () => controller.abort());
|
||||
|
||||
const headers = new Headers();
|
||||
for (const [key, value] of Object.entries(req.headers)) {
|
||||
if (!value) continue;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
for (const v of value) {
|
||||
headers.append(key, v);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
headers.append(key, value);
|
||||
}
|
||||
|
||||
const remixReq = new Request(url.href, {
|
||||
headers,
|
||||
method: req.method,
|
||||
signal: controller.signal,
|
||||
|
||||
// If we have a body we set a duplex and we load the body
|
||||
...(req.method !== 'GET' && req.method !== 'HEAD'
|
||||
? {
|
||||
body: createReadableStreamFromReadable(req),
|
||||
duplex: 'half',
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
|
||||
// Pass our request to the Remix handler and get a response
|
||||
const response = await handler(remixReq, {
|
||||
ws: getWss(),
|
||||
});
|
||||
|
||||
// Handle our response and reply
|
||||
res.statusCode = response.status;
|
||||
res.statusMessage = response.statusText;
|
||||
|
||||
for (const [key, value] of response.headers.entries()) {
|
||||
res.appendHeader(key, value);
|
||||
}
|
||||
|
||||
if (response.body) {
|
||||
await writeReadableStreamToWritable(response.body, res);
|
||||
return;
|
||||
}
|
||||
|
||||
res.end();
|
||||
});
|
||||
|
||||
registerWss(http);
|
||||
http.listen(port, host, () => {
|
||||
log('SRVX', 'INFO', `Running on ${host}:${port}`);
|
||||
});
|
||||
@@ -1,4 +0,0 @@
|
||||
export function log(topic, level, message) {
|
||||
const date = new Date().toISOString();
|
||||
console.log(`${date} (${level}) [${topic}] ${message}`);
|
||||
}
|
||||
+40
-23
@@ -1,33 +1,50 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { VitePluginNode } from 'vite-plugin-node';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
|
||||
const prefix = process.env.__INTERNAL_PREFIX || '/admin';
|
||||
if (prefix.endsWith('/')) {
|
||||
throw new Error('Prefix must not end with a slash');
|
||||
}
|
||||
|
||||
export default defineConfig(() => {
|
||||
return {
|
||||
build: {
|
||||
minify: false,
|
||||
target: 'esnext',
|
||||
rollupOptions: {
|
||||
input: './server/prod.mjs',
|
||||
output: {
|
||||
entryFileNames: 'server.js',
|
||||
dir: 'build/headplane',
|
||||
banner: '#!/usr/bin/env node\n',
|
||||
},
|
||||
external: (id) => id.startsWith('node:') || id === 'ws',
|
||||
export default defineConfig({
|
||||
define: {
|
||||
__hp_prefix: JSON.stringify(prefix),
|
||||
},
|
||||
resolve: {
|
||||
preserveSymlinks: true,
|
||||
alias: {
|
||||
stream: 'node:stream',
|
||||
crypto: 'node:crypto'
|
||||
},
|
||||
},
|
||||
plugins: [tsconfigPaths()],
|
||||
build: {
|
||||
minify: false,
|
||||
target: 'esnext',
|
||||
rollupOptions: {
|
||||
input: './server/entry.ts',
|
||||
treeshake: {
|
||||
moduleSideEffects: false,
|
||||
},
|
||||
},
|
||||
define: {
|
||||
PREFIX: JSON.stringify(prefix),
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
stream: 'node:stream',
|
||||
crypto: 'node:crypto',
|
||||
output: {
|
||||
entryFileNames: 'server.js',
|
||||
dir: 'build/headplane',
|
||||
banner: '#!/usr/bin/env node\n',
|
||||
},
|
||||
// external: (id) => id.startsWith('node:') || id === 'ws',
|
||||
external: (id) => {
|
||||
// Resolve happens before side-effects are removed
|
||||
// ie. vite import because of viteDevServer
|
||||
if (/node_modules/.test(id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return id.startsWith('node:')
|
||||
|| id === 'ws'
|
||||
|| id === 'mime/lite'
|
||||
|| id === '@react-router/node';
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
// The Websocket server is wholly responsible for ingesting messages from
|
||||
// Headplane agent instances (hopefully not more than 1 is running lol)
|
||||
import { WebSocketServer } from 'ws';
|
||||
import { log } from './utils.mjs';
|
||||
|
||||
const wss = new WebSocketServer({ noServer: true });
|
||||
wss.on('connection', (ws, req) => {
|
||||
// On connection the agent will send its NodeID via Headers
|
||||
// We store this for later use to validate and show on the UI
|
||||
const nodeID = req.headers['x-headplane-ts-node-id'];
|
||||
if (!nodeID) {
|
||||
ws.close(1008, 'ERR_NO_HP_TS_NODE_ID');
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
export async function registerWss(server) {
|
||||
log('SRVX', 'INFO', 'Registering Websocket Server');
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
wss.handleUpgrade(request, socket, head, (ws) => {
|
||||
wss.emit('connection', ws, request);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function getWss() {
|
||||
return wss;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import WebSocket, { WebSocketServer } from 'ws'
|
||||
import log from '~server/log'
|
||||
|
||||
const server = new WebSocketServer({ noServer: true });
|
||||
export function initWebsocket() {
|
||||
const key = process.env.LOCAL_AGENT_AUTHKEY;
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('CACH', 'Initializing agent WebSocket');
|
||||
server.on('connection', (ws, req) => {
|
||||
const auth = req.headers['authorization'];
|
||||
if (auth !== `Bearer ${key}`) {
|
||||
log.warn('CACH', 'Invalid agent WebSocket connection');
|
||||
ws.close(1008, 'ERR_INVALID_AUTH');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const nodeID = req.headers['x-headplane-ts-node-id'];
|
||||
if (!nodeID) {
|
||||
log.warn('CACH', 'Invalid agent WebSocket connection');
|
||||
ws.close(1008, 'ERR_INVALID_NODE_ID');
|
||||
return;
|
||||
}
|
||||
|
||||
const pinger = setInterval(() => {
|
||||
if (ws.readyState !== WebSocket.OPEN) {
|
||||
clearInterval(pinger);
|
||||
return;
|
||||
}
|
||||
|
||||
ws.ping();
|
||||
}, 30000);
|
||||
|
||||
ws.on('close', () => {
|
||||
clearInterval(pinger);
|
||||
});
|
||||
|
||||
ws.on('error', (error) => {
|
||||
clearInterval(pinger);
|
||||
log.error('CACH', 'Closing agent WebSocket connection');
|
||||
log.error('CACH', 'Agent WebSocket error: %s', error);
|
||||
ws.close(1011, 'ERR_INTERNAL_ERROR');
|
||||
})
|
||||
});
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
export function appContext() {
|
||||
return {
|
||||
ws: server,
|
||||
wsAuthKey: process.env.LOCAL_AGENT_AUTHKEY,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user