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