@vyckr/tachyon 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/.env.example +24 -0
- package/Dockerfile +43 -0
- package/README.md +124 -0
- package/bun.lockb +0 -0
- package/package.json +36 -0
- package/routes/_utils/validation.ts +36 -0
- package/routes/byos/[primary]/doc.ts +28 -0
- package/routes/byos/[primary]/docs.ts +28 -0
- package/routes/byos/[primary]/join/[secondary]/docs.ts +10 -0
- package/routes/byos/[primary]/schema.ts +17 -0
- package/routes/byos/[primary]/stream/doc.ts +28 -0
- package/routes/byos/[primary]/stream/docs.ts +28 -0
- package/src/Tach.ts +602 -0
- package/src/Yon.ts +25 -0
- package/src/runtime.ts +822 -0
- package/tsconfig.json +17 -0
- package/types/index.d.ts +13 -0
package/src/Tach.ts
ADDED
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { watch } from "node:fs";
|
|
3
|
+
import { exists } from "node:fs/promises";
|
|
4
|
+
import Silo from "@vyckr/byos";
|
|
5
|
+
import { Glob, Server } from "bun";
|
|
6
|
+
|
|
7
|
+
const Tach = {
|
|
8
|
+
|
|
9
|
+
indexedRoutes: new Map<string, Map<string, Function>>(),
|
|
10
|
+
|
|
11
|
+
routeSlugs: new Map<string, Map<string, number>>(),
|
|
12
|
+
|
|
13
|
+
allMethods: process.env.ALLOW_METHODS ? process.env.ALLOW_METHODS.split(',') : ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
|
|
14
|
+
|
|
15
|
+
inDevelopment: process.env.PRODUCTION ? false : true,
|
|
16
|
+
|
|
17
|
+
headers: {
|
|
18
|
+
"Access-Control-Allow-Headers": process.env.ALLOW_HEADERS || "",
|
|
19
|
+
"Access-Control-Allow-Origin": process.env.ALLLOW_ORGINS || "",
|
|
20
|
+
"Access-Control-Allow-Credential": process.env.ALLOW_CREDENTIALS || "false",
|
|
21
|
+
"Access-Control-Expose-Headers": process.env.ALLOW_EXPOSE_HEADERS || "",
|
|
22
|
+
"Access-Control-Max-Age": process.env.ALLOW_MAX_AGE || "",
|
|
23
|
+
"Access-Control-Allow-Methods": process.env.ALLOW_METHODS || ""
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
dbPath: process.env.DB_DIR,
|
|
27
|
+
|
|
28
|
+
saveStats: process.env.SAVE_STATS === 'true',
|
|
29
|
+
saveRequests: process.env.SAVE_REQUESTS === 'true',
|
|
30
|
+
saveErrors: process.env.SAVE_ERRORS === 'true',
|
|
31
|
+
saveLogs: process.env.SAVE_LOGS === 'true',
|
|
32
|
+
|
|
33
|
+
logsTableName: "_logs",
|
|
34
|
+
errorsTableName: "_errors",
|
|
35
|
+
requestTableName: "_requests",
|
|
36
|
+
statsTableName: "_stats",
|
|
37
|
+
|
|
38
|
+
context: new AsyncLocalStorage<_log[]>(),
|
|
39
|
+
|
|
40
|
+
routesPath: process.env.LAMBDA_TASK_ROOT ? `${process.env.LAMBDA_TASK_ROOT}/routes` : `${process.cwd()}/routes`,
|
|
41
|
+
|
|
42
|
+
hasMiddleware: await exists(`${process.env.LAMBDA_TASK_ROOT || process.cwd()}/routes/_middleware.ts`) || await exists(`${process.env.LAMBDA_TASK_ROOT || process.cwd()}/routes/_middleware.js`) ,
|
|
43
|
+
|
|
44
|
+
pathsMatch(routeSegs: string[], pathSegs: string[]) {
|
|
45
|
+
|
|
46
|
+
if (routeSegs.length !== pathSegs.length) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const slugs = Tach.routeSlugs.get(`${routeSegs.join('/')}.ts`) || Tach.routeSlugs.get(`${routeSegs.join('/')}.js`) || new Map<string, number>()
|
|
51
|
+
|
|
52
|
+
for (let i = 0; i < routeSegs.length; i++) {
|
|
53
|
+
if (!slugs.has(routeSegs[i]) && routeSegs[i].replace('.ts', '').replace('.js', '') !== pathSegs[i]) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return true;
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
getHandler(request: Request) {
|
|
62
|
+
|
|
63
|
+
const url = new URL(request.url);
|
|
64
|
+
|
|
65
|
+
let handler;
|
|
66
|
+
let params: string[] = [];
|
|
67
|
+
const paths = url.pathname.split('/').slice(1);
|
|
68
|
+
const allowedMethods: string[] = [];
|
|
69
|
+
|
|
70
|
+
let slugs = new Map<string, string>()
|
|
71
|
+
|
|
72
|
+
let bestMatchKey = '';
|
|
73
|
+
let bestMatchLength = -1;
|
|
74
|
+
|
|
75
|
+
for (const [routeKey] of Tach.indexedRoutes) {
|
|
76
|
+
const routeSegs = routeKey.split('/').map(seg => seg.replace('.ts', '').replace('.js', ''));
|
|
77
|
+
const isMatch = Tach.pathsMatch(routeSegs, paths.slice(0, routeSegs.length));
|
|
78
|
+
|
|
79
|
+
if (isMatch && routeSegs.length > bestMatchLength) {
|
|
80
|
+
bestMatchKey = routeKey;
|
|
81
|
+
bestMatchLength = routeSegs.length;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (bestMatchKey) {
|
|
86
|
+
const routeMap = Tach.indexedRoutes.get(bestMatchKey)!
|
|
87
|
+
handler = routeMap.get(request.method);
|
|
88
|
+
|
|
89
|
+
for (const [key] of routeMap) {
|
|
90
|
+
if (Tach.allMethods.includes(key)) allowedMethods.push(key);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
params = paths.slice(bestMatchLength);
|
|
94
|
+
|
|
95
|
+
const slugMap = Tach.routeSlugs.get(bestMatchKey) ?? new Map<string, number>()
|
|
96
|
+
|
|
97
|
+
slugMap.forEach((idx, key) => slugs.set(key, paths[idx]))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
Tach.headers = { ...Tach.headers, "Access-Control-Allow-Methods": allowedMethods.join(',') };
|
|
101
|
+
|
|
102
|
+
if (!handler) throw new Error(`Route ${request.method} ${url.pathname} not found`, { cause: 404 });
|
|
103
|
+
|
|
104
|
+
return { handler, params: Tach.parseParams(params), slugs }
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
formatDate() {
|
|
108
|
+
return new Date().toISOString().replace('T', ' ').replace('Z', '')
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
formatMsg(...msg: any[]) {
|
|
112
|
+
|
|
113
|
+
if(msg instanceof Set) return "\n" + JSON.stringify(Array.from(msg), null, 2)
|
|
114
|
+
|
|
115
|
+
else if(msg instanceof Map) return "\n" + JSON.stringify(Object.fromEntries(msg), null, 2)
|
|
116
|
+
|
|
117
|
+
else if(msg instanceof FormData) {
|
|
118
|
+
const formEntries: Record<string, any> = {}
|
|
119
|
+
msg.forEach((val, key) => formEntries[key] = val)
|
|
120
|
+
return "\n" + JSON.stringify(formEntries, null, 2)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
else if(Array.isArray(msg)
|
|
124
|
+
|| (typeof msg === 'object' && !Array.isArray(msg))
|
|
125
|
+
|| (typeof msg === 'object' && msg !== null)) return "\n" + JSON.stringify(msg, null, 2)
|
|
126
|
+
|
|
127
|
+
return msg
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
configLogger() {
|
|
131
|
+
|
|
132
|
+
const logger = console.log
|
|
133
|
+
|
|
134
|
+
function log(...args: any[]): void {
|
|
135
|
+
|
|
136
|
+
if (!args.length) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const messages = args.map(arg => Bun.inspect(arg).replace(/\n/g, "\r"));
|
|
141
|
+
|
|
142
|
+
logger(...messages);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const reset = '\x1b[0m'
|
|
146
|
+
|
|
147
|
+
console.info = (...args: any[]) => {
|
|
148
|
+
const info = `[${Tach.formatDate()}]\x1b[32m INFO${reset} (${process.pid}) ${Tach.formatMsg(...args)}`
|
|
149
|
+
log(info)
|
|
150
|
+
if(Tach.context.getStore()) {
|
|
151
|
+
const logWriter = Tach.context.getStore()
|
|
152
|
+
if(logWriter && Tach.dbPath && Tach.saveLogs) logWriter.push({ date: Date.now(), msg: `${info.replace(reset, '').replace('\x1b[32m', '')}\n`, type: "info" })
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.error = (...args: any[]) => {
|
|
157
|
+
const err = `[${Tach.formatDate()}]\x1b[31m ERROR${reset} (${process.pid}) ${Tach.formatMsg(...args)}`
|
|
158
|
+
log(err)
|
|
159
|
+
if(Tach.context.getStore()) {
|
|
160
|
+
const logWriter = Tach.context.getStore()
|
|
161
|
+
if(logWriter && Tach.dbPath && Tach.saveLogs) logWriter.push({ date: Date.now(), msg: `${err.replace(reset, '').replace('\x1b[31m', '')}\n`, type: "error" })
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
console.debug = (...args: any[]) => {
|
|
166
|
+
const bug = `[${Tach.formatDate()}]\x1b[36m DEBUG${reset} (${process.pid}) ${Tach.formatMsg(...args)}`
|
|
167
|
+
log(bug)
|
|
168
|
+
if(Tach.context.getStore()) {
|
|
169
|
+
const logWriter = Tach.context.getStore()
|
|
170
|
+
if(logWriter && Tach.dbPath && Tach.saveLogs) logWriter.push({ date: Date.now(), msg: `${bug.replace(reset, '').replace('\x1b[36m', '')}\n`, type: "debug" })
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.warn = (...args: any[]) => {
|
|
175
|
+
const warn = `[${Tach.formatDate()}]\x1b[33m WARN${reset} (${process.pid}) ${Tach.formatMsg(...args)}`
|
|
176
|
+
log(warn)
|
|
177
|
+
if(Tach.context.getStore()) {
|
|
178
|
+
const logWriter = Tach.context.getStore()
|
|
179
|
+
if(logWriter && Tach.dbPath && Tach.saveLogs) logWriter.push({ date: Date.now(), msg: `${warn.replace(reset, '').replace('\x1b[33m', '')}\n`, type: "warn" })
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.trace = (...args: any[]) => {
|
|
184
|
+
const trace = `[${Tach.formatDate()}]\x1b[35m TRACE${reset} (${process.pid}) ${Tach.formatMsg(...args)}`
|
|
185
|
+
log(trace)
|
|
186
|
+
if(Tach.context.getStore()) {
|
|
187
|
+
const logWriter = Tach.context.getStore()
|
|
188
|
+
if(logWriter && Tach.dbPath && Tach.saveLogs) logWriter.push({ date: Date.now(), msg: `${trace.replace(reset, '').replace('\x1b[35m', '')}\n`, type: "trace" })
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
async logRequest(request: Request, status: number, context: _HTTPContext, data: any = null) {
|
|
194
|
+
|
|
195
|
+
if(Tach.dbPath && Tach.saveRequests) {
|
|
196
|
+
|
|
197
|
+
const url = new URL(request.url)
|
|
198
|
+
const date = Date.now()
|
|
199
|
+
const duration = date - (context.requestTime ?? 0)
|
|
200
|
+
|
|
201
|
+
await Silo.putData(Tach.requestTableName, { ipAddress: context.ipAddress, url: `${url.pathname}${url.search}`, method: request.method, status, duration, date, size: data ? String(data).length : 0, data })
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
async processRequest(request: Request, context: _HTTPContext) {
|
|
206
|
+
|
|
207
|
+
const { handler, params, slugs } = Tach.getHandler(request)
|
|
208
|
+
|
|
209
|
+
if(slugs.size > 0) context.slugs = slugs
|
|
210
|
+
|
|
211
|
+
const body = await request.blob()
|
|
212
|
+
|
|
213
|
+
let data: Blob | Record<string, any> | undefined
|
|
214
|
+
|
|
215
|
+
if(body.size > 0) {
|
|
216
|
+
|
|
217
|
+
if(body.type.includes('form')) data = Tach.parseKVParams(await body.formData())
|
|
218
|
+
else {
|
|
219
|
+
try {
|
|
220
|
+
data = await body.json()
|
|
221
|
+
} catch {
|
|
222
|
+
data = body
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const searchParams = new URL(request.url).searchParams
|
|
228
|
+
|
|
229
|
+
let queryParams: Record<string, any> | undefined;
|
|
230
|
+
|
|
231
|
+
if(searchParams.size > 0) queryParams = Tach.parseKVParams(searchParams)
|
|
232
|
+
|
|
233
|
+
const middlewarePath = await exists(`${Tach.routesPath}/_middleware.ts`) ? `${Tach.routesPath}/_middleware.ts` : `${Tach.routesPath}/_middleware.js`
|
|
234
|
+
|
|
235
|
+
if(params.length > 0 && !queryParams && !data) {
|
|
236
|
+
|
|
237
|
+
let res = undefined
|
|
238
|
+
|
|
239
|
+
if(Tach.hasMiddleware) {
|
|
240
|
+
|
|
241
|
+
const middleware = (await import(middlewarePath)).default
|
|
242
|
+
|
|
243
|
+
res = await middleware(async () => handler(...params, context))
|
|
244
|
+
|
|
245
|
+
} else res = await handler(...params, context)
|
|
246
|
+
|
|
247
|
+
await Tach.logRequest(request, 200, context)
|
|
248
|
+
|
|
249
|
+
return res
|
|
250
|
+
|
|
251
|
+
} else if(params.length === 0 && queryParams && !data) {
|
|
252
|
+
|
|
253
|
+
let res = undefined
|
|
254
|
+
|
|
255
|
+
if(Tach.hasMiddleware) {
|
|
256
|
+
|
|
257
|
+
const middleware = (await import(middlewarePath)).default
|
|
258
|
+
|
|
259
|
+
res = await middleware(async () => handler(queryParams, context))
|
|
260
|
+
|
|
261
|
+
} else res = await handler(queryParams, context)
|
|
262
|
+
|
|
263
|
+
await Tach.logRequest(request, 200, context)
|
|
264
|
+
|
|
265
|
+
return res
|
|
266
|
+
|
|
267
|
+
} else if(params.length === 0 && !queryParams && data) {
|
|
268
|
+
|
|
269
|
+
let res = undefined
|
|
270
|
+
|
|
271
|
+
if(Tach.hasMiddleware) {
|
|
272
|
+
|
|
273
|
+
const middleware = (await import(middlewarePath)).default
|
|
274
|
+
|
|
275
|
+
res = await middleware(async () => handler(data, context))
|
|
276
|
+
|
|
277
|
+
} else res = await handler(data, context)
|
|
278
|
+
|
|
279
|
+
await Tach.logRequest(request, 200, context, await body.text())
|
|
280
|
+
|
|
281
|
+
return res
|
|
282
|
+
|
|
283
|
+
} else if(params.length > 0 && queryParams && !data) {
|
|
284
|
+
|
|
285
|
+
let res = undefined
|
|
286
|
+
|
|
287
|
+
if(Tach.hasMiddleware) {
|
|
288
|
+
|
|
289
|
+
const middleware = (await import(middlewarePath)).default
|
|
290
|
+
|
|
291
|
+
res = await middleware(async () => handler(...params, queryParams, context))
|
|
292
|
+
|
|
293
|
+
} else res = await handler(...params, queryParams, context)
|
|
294
|
+
|
|
295
|
+
await Tach.logRequest(request, 200, context)
|
|
296
|
+
|
|
297
|
+
return res
|
|
298
|
+
|
|
299
|
+
} else if(params.length > 0 && !queryParams && data) {
|
|
300
|
+
|
|
301
|
+
let res = undefined
|
|
302
|
+
|
|
303
|
+
if(Tach.hasMiddleware) {
|
|
304
|
+
|
|
305
|
+
const middleware = (await import(middlewarePath)).default
|
|
306
|
+
|
|
307
|
+
res = await middleware(async () => handler(...params, data, context))
|
|
308
|
+
|
|
309
|
+
} else res = await handler(...params, data, context)
|
|
310
|
+
|
|
311
|
+
await Tach.logRequest(request, 200, context, await body.text())
|
|
312
|
+
|
|
313
|
+
return res
|
|
314
|
+
|
|
315
|
+
} else if(params.length === 0 && data && queryParams) {
|
|
316
|
+
|
|
317
|
+
let res = undefined
|
|
318
|
+
|
|
319
|
+
if(Tach.hasMiddleware) {
|
|
320
|
+
|
|
321
|
+
const middleware = (await import(middlewarePath)).default
|
|
322
|
+
|
|
323
|
+
res = await middleware(async () => handler(queryParams, data, context))
|
|
324
|
+
|
|
325
|
+
} else res = await handler(queryParams, data, context)
|
|
326
|
+
|
|
327
|
+
await Tach.logRequest(request, 200, context, await body.text())
|
|
328
|
+
|
|
329
|
+
return res
|
|
330
|
+
|
|
331
|
+
} else if(params.length > 0 && data && queryParams) {
|
|
332
|
+
|
|
333
|
+
let res = undefined
|
|
334
|
+
|
|
335
|
+
if(Tach.hasMiddleware) {
|
|
336
|
+
|
|
337
|
+
const middleware = (await import(middlewarePath)).default
|
|
338
|
+
|
|
339
|
+
res = await middleware(async () => handler(...params, queryParams, data, context))
|
|
340
|
+
|
|
341
|
+
} else res = await handler(...params, queryParams, data, context)
|
|
342
|
+
|
|
343
|
+
await Tach.logRequest(request, 200, context, await body.text())
|
|
344
|
+
|
|
345
|
+
return res
|
|
346
|
+
|
|
347
|
+
} else {
|
|
348
|
+
|
|
349
|
+
let res = undefined
|
|
350
|
+
|
|
351
|
+
if(Tach.hasMiddleware) {
|
|
352
|
+
|
|
353
|
+
const middleware = (await import(middlewarePath)).default
|
|
354
|
+
|
|
355
|
+
res = await middleware(async () => handler(context))
|
|
356
|
+
|
|
357
|
+
} else res = await handler(context)
|
|
358
|
+
|
|
359
|
+
await Tach.logRequest(request, 200, context)
|
|
360
|
+
|
|
361
|
+
return res
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
isAsyncIterator(data: any) {
|
|
366
|
+
return typeof data === "object" && Object.hasOwn(data, Symbol.asyncIterator)
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
hasFunctions(data: any) {
|
|
370
|
+
return typeof data === "object" && (Object.keys(data).some((elem) => typeof elem === "function") || Object.values(data).some((elem) => typeof elem === "function"))
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
processResponse(status: number, data?: any) {
|
|
374
|
+
|
|
375
|
+
const headers = Tach.headers
|
|
376
|
+
|
|
377
|
+
if(data instanceof Set) return Response.json(Array.from(data), { status, headers })
|
|
378
|
+
|
|
379
|
+
if(data instanceof Map) return Response.json(Object.fromEntries(data), { status, headers })
|
|
380
|
+
|
|
381
|
+
if(data instanceof FormData || data instanceof Blob) return new Response(data, { status, headers })
|
|
382
|
+
|
|
383
|
+
if(typeof data === "object" && !Array.isArray(data) && !Tach.isAsyncIterator(data) && !Tach.hasFunctions(data)) return Response.json(data, { status, headers })
|
|
384
|
+
|
|
385
|
+
if((typeof data === "object" && Array.isArray(data)) || data instanceof Array) return Response.json(data, { status, headers })
|
|
386
|
+
|
|
387
|
+
if(typeof data === "number" || typeof data === "boolean") return Response.json(data, { status, headers })
|
|
388
|
+
|
|
389
|
+
return new Response(data, { status, headers })
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
async logError(e: Error, ipAddress: string, url: URL, method: string, logs: _log[], startTime?: number) {
|
|
393
|
+
|
|
394
|
+
const path = url.pathname
|
|
395
|
+
|
|
396
|
+
if(logs.length > 0 && Tach.saveLogs && Tach.dbPath) await Promise.all(logs.map(log => {
|
|
397
|
+
return Silo.putData(Tach.logsTableName, { ipAddress, path, method, ...log })
|
|
398
|
+
}))
|
|
399
|
+
|
|
400
|
+
if(Tach.dbPath && Tach.saveErrors) await Silo.putData(Tach.errorsTableName, { ipAddress, date: Date.now(),path, method, error: e.message })
|
|
401
|
+
|
|
402
|
+
console.error(`"${method} ${path}" ${e.cause as number ?? 500} ${startTime ? `- ${Date.now() - startTime}ms` : ''} - ${e.message.length} byte(s)`)
|
|
403
|
+
},
|
|
404
|
+
|
|
405
|
+
watchFiles() {
|
|
406
|
+
|
|
407
|
+
if(Tach.inDevelopment) {
|
|
408
|
+
|
|
409
|
+
watch(Tach.routesPath, { recursive: true }, async (ev, filename) => {
|
|
410
|
+
delete import.meta.require.cache[`${Tach.routesPath}/${filename}`]
|
|
411
|
+
if(!filename?.split('/').some((path) => path.startsWith('_'))) await Tach.validateRoutes(filename!)
|
|
412
|
+
})
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
|
|
416
|
+
async fetch(req: Request, server: Server) {
|
|
417
|
+
|
|
418
|
+
const request = req.clone()
|
|
419
|
+
|
|
420
|
+
const logs: _log[] = []
|
|
421
|
+
|
|
422
|
+
const url = new URL(req.url)
|
|
423
|
+
|
|
424
|
+
const startTime = Date.now()
|
|
425
|
+
|
|
426
|
+
const ipAddress = server.requestIP ? server.requestIP(req)!.address : '0.0.0.0'
|
|
427
|
+
|
|
428
|
+
return await Tach.context.run(logs, async () => {
|
|
429
|
+
|
|
430
|
+
let res: Response
|
|
431
|
+
|
|
432
|
+
try {
|
|
433
|
+
|
|
434
|
+
const data = await Tach.processRequest(req, { ipAddress, request: req, requestTime: startTime, logs, slugs: new Map<string, any>() })
|
|
435
|
+
|
|
436
|
+
res = Tach.processResponse(200, data)
|
|
437
|
+
|
|
438
|
+
if(logs.length > 0 && Tach.saveLogs && Tach.dbPath) await Promise.all(logs.map(log => {
|
|
439
|
+
return Silo.putData(Tach.logsTableName, { ipAddress, path: url.pathname, method: req.method, ...log })
|
|
440
|
+
}))
|
|
441
|
+
|
|
442
|
+
if(!Tach.isAsyncIterator(data)) {
|
|
443
|
+
|
|
444
|
+
const status = res.status
|
|
445
|
+
const response_size = typeof data !== "undefined" ? String(data).length : 0
|
|
446
|
+
const url = new URL(req.url)
|
|
447
|
+
const method = req.method
|
|
448
|
+
const date = Date.now()
|
|
449
|
+
const duration = date - startTime
|
|
450
|
+
|
|
451
|
+
console.info(`"${method} ${url.pathname}" ${status} - ${duration}ms - ${response_size} byte(s)`)
|
|
452
|
+
|
|
453
|
+
if(Tach.dbPath && Tach.saveStats) await Silo.putData(Tach.statsTableName, { ipAddress, cpu: process.cpuUsage(), memory: process.memoryUsage(), date: Date.now() })
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
} catch(e) {
|
|
457
|
+
|
|
458
|
+
const method = request.method
|
|
459
|
+
|
|
460
|
+
await Tach.logError(e as Error, ipAddress, url, method, logs, startTime)
|
|
461
|
+
|
|
462
|
+
if(Tach.dbPath && Tach.saveStats) await Silo.putData(Tach.statsTableName, { ipAddress, cpu: process.cpuUsage(), memory: process.memoryUsage(), date: Date.now() })
|
|
463
|
+
|
|
464
|
+
res = Response.json({ detail: (e as Error).message }, { status: (e as Error).cause as number ?? 500, headers: Tach.headers })
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return res
|
|
468
|
+
})
|
|
469
|
+
},
|
|
470
|
+
|
|
471
|
+
async validateRoutes(route?: string) {
|
|
472
|
+
|
|
473
|
+
const staticPaths: string[] = []
|
|
474
|
+
|
|
475
|
+
const validateRoute = async (route: string) => {
|
|
476
|
+
|
|
477
|
+
const paths = route.split('/')
|
|
478
|
+
|
|
479
|
+
const pattern = /[<>|\[\]]/
|
|
480
|
+
|
|
481
|
+
const slugs = new Map<string, number>()
|
|
482
|
+
|
|
483
|
+
paths.forEach((path, idx) => {
|
|
484
|
+
|
|
485
|
+
if(pattern.test(path) && (idx % 2 === 0 || paths[idx].includes('.ts') || paths[idx].includes('.js'))) {
|
|
486
|
+
throw new Error(`Invalid route ${route}`)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if(pattern.test(path)) slugs.set(path, idx)
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
const idx = paths.findIndex((path) => pattern.test(path))
|
|
493
|
+
|
|
494
|
+
if(idx > -1 && (idx % 2 === 0 || paths[idx].includes('.ts') || paths[idx].includes('.js'))) throw new Error(`Invalid route ${route}`)
|
|
495
|
+
|
|
496
|
+
const staticPath = paths.filter((path) => !pattern.test(path)).join(',')
|
|
497
|
+
|
|
498
|
+
if(staticPaths.includes(staticPath)) throw new Error(`Duplicate route ${route}`)
|
|
499
|
+
|
|
500
|
+
staticPaths.push(staticPath)
|
|
501
|
+
|
|
502
|
+
const module = await import(`${Tach.routesPath}/${route}`)
|
|
503
|
+
|
|
504
|
+
const controller = (new module.default() as any).constructor
|
|
505
|
+
|
|
506
|
+
const methodFuncs = new Map<string, Function>()
|
|
507
|
+
|
|
508
|
+
for(const method of Tach.allMethods) {
|
|
509
|
+
|
|
510
|
+
if(controller[method]) {
|
|
511
|
+
|
|
512
|
+
methodFuncs.set(method, controller[method])
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
Tach.indexedRoutes.set(route, methodFuncs)
|
|
517
|
+
|
|
518
|
+
if(slugs.size > 0) Tach.routeSlugs.set(route, slugs)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if(route) return await validateRoute(route)
|
|
522
|
+
|
|
523
|
+
const files = Array.from(new Glob(`**/*.{ts,js}`).scanSync({ cwd: Tach.routesPath }))
|
|
524
|
+
|
|
525
|
+
const routes = files.filter((route) => !route.split('/').some((path) => path.startsWith('_')))
|
|
526
|
+
|
|
527
|
+
for(const route of routes) await validateRoute(route)
|
|
528
|
+
},
|
|
529
|
+
|
|
530
|
+
parseParams(input: string[]) {
|
|
531
|
+
|
|
532
|
+
const params: (string | boolean | number | null | undefined)[] = []
|
|
533
|
+
|
|
534
|
+
for(const param of input) {
|
|
535
|
+
|
|
536
|
+
const num = Number(param)
|
|
537
|
+
|
|
538
|
+
if(!Number.isNaN(num)) params.push(num)
|
|
539
|
+
|
|
540
|
+
else if(param === 'true') params.push(true)
|
|
541
|
+
|
|
542
|
+
else if(param === 'false') params.push(false)
|
|
543
|
+
|
|
544
|
+
else if(param === 'null') params.push(null)
|
|
545
|
+
|
|
546
|
+
else if(param === 'undefined') params.push(undefined)
|
|
547
|
+
|
|
548
|
+
else params.push(param)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return params
|
|
552
|
+
},
|
|
553
|
+
|
|
554
|
+
parseKVParams(input: URLSearchParams | FormData) {
|
|
555
|
+
|
|
556
|
+
const params: Record<string, any> = {}
|
|
557
|
+
|
|
558
|
+
for(const [key, val] of input) {
|
|
559
|
+
|
|
560
|
+
if(typeof val === "string") {
|
|
561
|
+
|
|
562
|
+
try {
|
|
563
|
+
|
|
564
|
+
params[key] = JSON.parse(val)
|
|
565
|
+
|
|
566
|
+
} catch {
|
|
567
|
+
|
|
568
|
+
const num = Number(val)
|
|
569
|
+
|
|
570
|
+
if(!Number.isNaN(num)) params[key] = num
|
|
571
|
+
|
|
572
|
+
else if(val === 'true') params[key] = true
|
|
573
|
+
|
|
574
|
+
else if(val === 'false') params[key] = false
|
|
575
|
+
|
|
576
|
+
else if(typeof val === "string" && val.includes(',')) params[key] = Tach.parseParams(val.split(','))
|
|
577
|
+
|
|
578
|
+
else if(val === 'null') params[key] = null
|
|
579
|
+
|
|
580
|
+
if(params[key] === undefined) params[key] = val
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
} else params[key] = val
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return params
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
try {
|
|
591
|
+
|
|
592
|
+
await Tach.validateRoutes()
|
|
593
|
+
|
|
594
|
+
Tach.watchFiles()
|
|
595
|
+
|
|
596
|
+
Tach.configLogger()
|
|
597
|
+
|
|
598
|
+
} catch(e) {
|
|
599
|
+
console.log(`Tach.ts --> ${e}`)
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
export default Tach
|
package/src/Yon.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import Tach from "./Tach.js"
|
|
3
|
+
import Silo from "@vyckr/byos";
|
|
4
|
+
|
|
5
|
+
try {
|
|
6
|
+
|
|
7
|
+
const start = Date.now()
|
|
8
|
+
|
|
9
|
+
const server = Bun.serve({ fetch: Tach.fetch, async error(req) {
|
|
10
|
+
|
|
11
|
+
if(Tach.dbPath && Tach.saveStats) await Silo.putData(Tach.statsTableName, { cpu: process.cpuUsage(), memory: process.memoryUsage(), date: Date.now() })
|
|
12
|
+
|
|
13
|
+
return Response.json({ detail: req.message }, { status: req.cause as number ?? 500, headers: Tach.headers })
|
|
14
|
+
},
|
|
15
|
+
development: Tach.inDevelopment,
|
|
16
|
+
port: process.env.PORT || 8000
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
process.on('SIGINT', () => process.exit(0))
|
|
20
|
+
|
|
21
|
+
console.info(`Live Server is running on http://${server.hostname}:${server.port} (Press CTRL+C to quit) - StartUp Time: ${Date.now() - start}ms`)
|
|
22
|
+
|
|
23
|
+
} catch(e) {
|
|
24
|
+
if(e instanceof Error) console.error(e.message)
|
|
25
|
+
}
|