-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathloxpress.js
More file actions
380 lines (322 loc) · 10.3 KB
/
Copy pathloxpress.js
File metadata and controls
380 lines (322 loc) · 10.3 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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
import { net } from 'lib/net.js'
import { Loop } from 'lib/loop.js'
import { RequestParser } from 'lib/pico.js'
const { ptr } = lo
const {
socket: createSocket, bind: netBind, listen: netListen,
accept4, setsockopt, send_string, recv2, close: netClose
} = net
const {
AF_INET, SOCK_STREAM, SOCK_NONBLOCK, SOL_SOCKET, SOCKADDR_LEN,
O_NONBLOCK, EAGAIN
} = net.constants
const BUF_SIZE = 65536
const STATUS_CODES = {
200: 'OK',
201: 'Created',
204: 'No Content',
301: 'Moved Permanently',
302: 'Found',
304: 'Not Modified',
400: 'Bad Request',
401: 'Unauthorized',
403: 'Forbidden',
404: 'Not Found',
405: 'Method Not Allowed',
409: 'Conflict',
422: 'Unprocessable Entity',
429: 'Too Many Requests',
500: 'Internal Server Error',
502: 'Bad Gateway',
503: 'Service Unavailable'
}
// Pre-allocated recv buffer + parser (reused across requests since handling is synchronous)
const recvBuf = ptr(new Uint8Array(BUF_SIZE))
const parser = new RequestParser(recvBuf)
// Pre-allocated handler array (reused per request, cleared before use)
const handlersBuf = []
// Pre-built 404 handler (shared, not created per request)
function notFoundHandler (rq, rs) {
rs.status(404).send('{"error":"Not Found"}', 'application/json')
}
// --- Route compilation & matching ---
function compileRoute (pattern) {
if (pattern.indexOf(':') === -1) {
return { exact: pattern, regex: null, keys: null }
}
const keys = []
const regexStr = pattern.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, key) => {
keys.push(key)
return '([^/]+)'
})
return { exact: null, regex: new RegExp(`^${regexStr}$`), keys }
}
function matchRoute (compiled, pathname) {
if (compiled.exact !== null) {
return pathname === compiled.exact ? {} : null
}
const m = compiled.regex.exec(pathname)
if (!m) return null
const params = {}
for (let i = 0; i < compiled.keys.length; i++) {
params[compiled.keys[i]] = decodeURIComponent(m[i + 1])
}
return params
}
// --- Query string parsing ---
const emptyQuery = Object.create(null)
function parseQueryString (qs) {
if (!qs) return emptyQuery
const result = Object.create(null)
const pairs = qs.split('&')
for (let i = 0; i < pairs.length; i++) {
const idx = pairs[i].indexOf('=')
if (idx === -1) {
result[decodeURIComponent(pairs[i])] = ''
} else {
result[decodeURIComponent(pairs[i].slice(0, idx))] =
decodeURIComponent(pairs[i].slice(idx + 1))
}
}
return result
}
// --- Inline request + response on a reusable object ---
const emptyParams = Object.create(null)
const req = {
method: '',
url: '',
path: '',
query: emptyQuery,
headers: null,
params: emptyParams,
body: null,
_parsedBytes: 0,
_bytesRead: 0
}
// --- Response (writes directly, minimal object) ---
function sendResponse (fd, statusCode, body, contentType) {
const statusText = STATUS_CODES[statusCode] || 'Unknown'
const response = contentType
? `HTTP/1.1 ${statusCode} ${statusText}\r\ncontent-type: ${contentType}\r\ncontent-length: ${body.length}\r\nconnection: close\r\n\r\n${body}`
: `HTTP/1.1 ${statusCode} ${statusText}\r\ncontent-length: ${body.length}\r\nconnection: close\r\n\r\n${body}`
send_string(fd, response)
}
function createResponse (clientFd) {
let statusCode = 200
let extraHeaders = null
let headersSent = false
const res = {
status (code) {
statusCode = code
return res
},
set (name, value) {
if (!extraHeaders) extraHeaders = []
extraHeaders.push(name.toLowerCase(), value)
return res
},
header (name, value) {
return res.set(name, value)
},
json (obj) {
res.send(JSON.stringify(obj), 'application/json')
},
send (body, contentType) {
if (headersSent) return
headersSent = true
if (body == null) body = ''
if (!extraHeaders) {
sendResponse(clientFd, statusCode, body, contentType)
} else {
// Slow path: custom headers
const statusText = STATUS_CODES[statusCode] || 'Unknown'
let head = `HTTP/1.1 ${statusCode} ${statusText}\r\n`
if (contentType) head += `content-type: ${contentType}\r\n`
for (let i = 0; i < extraHeaders.length; i += 2) {
head += `${extraHeaders[i]}: ${extraHeaders[i + 1]}\r\n`
}
head += `content-length: ${body.length}\r\nconnection: close\r\n\r\n`
send_string(clientFd, head + body)
}
},
end (body) {
if (body !== undefined) {
res.send(body)
} else if (!headersSent) {
res.send('')
}
}
}
return res
}
// --- Body reading (synchronous from initial recv buffer) ---
const decoder = new TextDecoder()
function readBody (r) {
const contentLength = parseInt(r.headers['content-length'] || '0', 10)
if (contentLength === 0) return
const available = r._bytesRead - r._parsedBytes
if (available <= 0) return
const n = Math.min(available, contentLength)
r.body = decoder.decode(recvBuf.subarray(r._parsedBytes, r._parsedBytes + n))
const ct = r.headers['content-type'] || ''
if (ct.indexOf('application/json') !== -1) {
try { r.body = JSON.parse(r.body) } catch (e) {}
}
}
// --- Middleware / route dispatch ---
function dispatch (r, res, handlers, idx, len) {
while (idx < len) {
const handler = handlers[idx]
let calledNext = false
handler(r, res, () => { calledNext = true })
if (!calledNext) return
idx++
}
}
// --- Connection handler ---
const rxUnderscore = /_/g
function handleConnection (clientFd, middlewares, mwLen, routes) {
const bytesRead = recv2(clientFd, recvBuf.ptr, BUF_SIZE, 0)
if (bytesRead === -1 && lo.errno === EAGAIN) {
const loop = lo.loop
loop.add(clientFd, () => {
loop.remove(clientFd)
handleConnection(clientFd, middlewares, mwLen, routes)
}, Loop.Readable)
return
}
if (bytesRead <= 0) {
netClose(clientFd)
return
}
const parsed = parser.parse(bytesRead)
if (parsed <= 0) {
send_string(clientFd,
'HTTP/1.1 400 Bad Request\r\nConnection: close\r\nContent-Length: 11\r\n\r\nBad Request')
netClose(clientFd)
return
}
// Populate reusable req object
const fullPath = parser.path
const qIdx = fullPath.indexOf('?')
req.method = parser.method
req.url = fullPath
req.path = qIdx === -1 ? fullPath : fullPath.slice(0, qIdx)
req.query = qIdx === -1 ? emptyQuery : parseQueryString(fullPath.slice(qIdx + 1))
req.params = emptyParams
req.body = null
req._parsedBytes = parsed
req._bytesRead = bytesRead
// Copy headers (picohttp uses underscores)
const picoHeaders = parser.headers
const headers = Object.create(null)
for (const key in picoHeaders) {
headers[key.replace(rxUnderscore, '-')] = picoHeaders[key]
}
req.headers = headers
const res = createResponse(clientFd)
const method = req.method
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
readBody(req)
}
// Build handler chain into pre-allocated array
let hLen = 0
for (let i = 0; i < mwLen; i++) {
const mw = middlewares[i]
if (mw.path === null || req.path.startsWith(mw.path)) {
handlersBuf[hLen++] = mw.handler
}
}
const methodRoutes = routes[method]
if (methodRoutes) {
const rLen = methodRoutes.length
for (let i = 0; i < rLen; i++) {
const route = methodRoutes[i]
const params = matchRoute(route.compiled, req.path)
if (params !== null) {
req.params = params
handlersBuf[hLen++] = route.handler
break
}
}
}
handlersBuf[hLen++] = notFoundHandler
dispatch(req, res, handlersBuf, 0, hLen)
netClose(clientFd)
}
// --- Accept handler ---
function onServerReadable (serverFd, middlewares, mwLen, routes) {
while (true) {
const clientFd = accept4(serverFd, 0, 0, O_NONBLOCK)
if (clientFd <= 0) break
handleConnection(clientFd, middlewares, mwLen, routes)
}
}
// --- Server startup ---
function startServer (port, address, middlewares, routes, callback) {
if (!lo.loop) {
lo.loop = new Loop()
}
const fd = createSocket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0)
if (fd < 0) throw new Error(`socket failed: errno=${lo.errno}`)
if (setsockopt && net.on) {
setsockopt(fd, SOL_SOCKET, net.SO_REUSEADDR || 2, net.on.ptr, 4)
}
const addr = ptr(new Uint8Array(16))
const adv = new DataView(addr.buffer)
adv.setInt16(0, AF_INET, true)
adv.setUint16(2, port & 0xffff)
const parts = address.split('.').map(v => parseInt(v, 10) & 0xff)
adv.setUint32(4, (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3])
const rc = netBind(fd, addr.ptr, SOCKADDR_LEN)
if (rc !== 0) throw new Error(`bind ${address}:${port} failed: errno=${lo.errno}`)
const rc2 = netListen(fd, 128)
if (rc2 !== 0) throw new Error(`listen failed: errno=${lo.errno}`)
if (callback) callback()
// Cache middleware length for inner loop
const mwLen = middlewares.length
const loop = lo.loop
loop.add(fd, () => onServerReadable(fd, middlewares, mwLen, routes), Loop.Readable)
while (loop.poll() > 0) {}
}
// --- App factory ---
function loxpress () {
const middlewares = []
const routes = {}
function addRoute (method, pattern, handler) {
if (!routes[method]) routes[method] = []
routes[method].push({
pattern,
compiled: compileRoute(pattern),
handler
})
return app
}
const app = {
use (pathOrHandler, handler) {
if (typeof pathOrHandler === 'function') {
middlewares.push({ path: null, handler: pathOrHandler })
} else {
middlewares.push({ path: pathOrHandler, handler })
}
return app
},
get (pattern, handler) { return addRoute('GET', pattern, handler) },
post (pattern, handler) { return addRoute('POST', pattern, handler) },
put (pattern, handler) { return addRoute('PUT', pattern, handler) },
delete (pattern, handler) { return addRoute('DELETE', pattern, handler) },
patch (pattern, handler) { return addRoute('PATCH', pattern, handler) },
listen (port = 3000, addressOrCallback, callback) {
let address = '0.0.0.0'
if (typeof addressOrCallback === 'function') {
callback = addressOrCallback
} else if (typeof addressOrCallback === 'string') {
address = addressOrCallback
}
startServer(port, address, middlewares, routes, callback)
return app
}
}
return app
}
export { loxpress }