-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathserver.ts
More file actions
155 lines (123 loc) · 4.76 KB
/
server.ts
File metadata and controls
155 lines (123 loc) · 4.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
/**
* @module server
* Custom React Router v7 production server with WebSocket support.
*
* Replaces the default `@react-router/serve` entry to attach a WebSocket upgrade
* handler on the `/ws` path — same HTTP port, no extra process.
*
* Usage (production):
* node --import ./instrument.server.mjs ./server.js
*
* In development, Vite manages its own server; this file is NOT used.
*/
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
import { createReadableStreamFromReadable } from '@react-router/node';
import { createRequestHandler } from 'react-router';
import { handleUpgrade } from './app/lib/.server/ws/ws-server';
const PORT = Number(process.env.PORT) || 5173;
function log(level: string, ...args: unknown[]) {
const timestamp = new Date().toISOString();
// eslint-disable-next-line no-console
console[level === 'error' ? 'error' : 'log'](`[${timestamp}] [Server]`, ...args);
}
// ---------------------------------------------------------------------------
// React Router request handler
// ---------------------------------------------------------------------------
// @ts-ignore — build output is JavaScript, no type declarations
const build = await import('./build/server/index.js');
// @ts-expect-error — build is untyped (JS output), but satisfies ServerBuild at runtime
const requestHandler = createRequestHandler({ build, mode: process.env.NODE_ENV });
// ---------------------------------------------------------------------------
// Node → Web Request adapter
// ---------------------------------------------------------------------------
function createWebRequest(req: IncomingMessage): Request {
const host = req.headers.host ?? `localhost:${PORT}`;
const protocol = req.headers['x-forwarded-proto'] === 'https' ? 'https' : 'http';
const url = new URL(req.url ?? '/', `${protocol}://${host}`);
const headers = new Headers();
for (const [key, value] of Object.entries(req.headers)) {
if (value === undefined) {
continue;
}
if (Array.isArray(value)) {
for (const v of value) {
headers.append(key, v);
}
} else {
headers.set(key, value);
}
}
const method = req.method ?? 'GET';
const hasBody = method !== 'GET' && method !== 'HEAD';
const init: RequestInit = {
method,
headers,
};
if (hasBody) {
init.body = createReadableStreamFromReadable(req as unknown as import('node:stream').Readable);
(init as Record<string, unknown>).duplex = 'half';
}
return new Request(url.href, init);
}
// ---------------------------------------------------------------------------
// Web Response → Node adapter
// ---------------------------------------------------------------------------
async function sendWebResponse(res: ServerResponse, webResponse: Response): Promise<void> {
res.statusCode = webResponse.status;
res.statusMessage = webResponse.statusText;
for (const [key, value] of webResponse.headers) {
// Headers with multiple values (e.g. Set-Cookie) need appendHeader
res.appendHeader(key, value);
}
if (!webResponse.body) {
res.end();
return;
}
const reader = webResponse.body.getReader();
try {
for (;;) {
const { done, value } = await reader.read();
if (done) {
break;
}
res.write(value);
}
} catch (err) {
// Client disconnected (EPIPE / ECONNRESET) — silently ignore
const code = (err as NodeJS.ErrnoException).code;
if (code !== 'EPIPE' && code !== 'ECONNRESET' && code !== 'ERR_STREAM_PREMATURE_CLOSE') {
log('error', 'Error writing response:', err);
}
} finally {
res.end();
}
}
// ---------------------------------------------------------------------------
// HTTP Server
// ---------------------------------------------------------------------------
const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
try {
const webRequest = createWebRequest(req);
const webResponse = await requestHandler(webRequest);
await sendWebResponse(res, webResponse);
} catch (err) {
log('error', 'Unhandled request error:', err);
res.statusCode = 500;
res.end('Internal Server Error');
}
});
// Attach WebSocket upgrade handler — only intercepts /ws
server.on('upgrade', (req, socket, head) => {
const url = new URL(req.url ?? '/', `http://${req.headers.host || 'localhost'}`);
if (url.pathname === '/ws') {
handleUpgrade(req, socket, head);
return;
}
// Not our path — destroy the socket so other upgrade handlers aren't confused
// (In production there shouldn't be any HMR upgrades, but guard anyway.)
socket.destroy();
});
server.listen(PORT, () => {
log('info', `Devonz server listening on http://localhost:${PORT}`);
log('info', `WebSocket endpoint: ws://localhost:${PORT}/ws`);
});