@tamayrp14/fire-api 0.0.0-stage → 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 (98) hide show
  1. package/README.md +276 -2
  2. package/bin/fire.js +139 -0
  3. package/bun.lock +20 -0
  4. package/docs/GETTING_STARTED.md +150 -0
  5. package/docs/LAUNCH_KIT.md +236 -0
  6. package/docs/superpowers/plans/2026-11-14-fire-api-framework.md +144 -0
  7. package/docs/superpowers/specs/fire-api-design-spec.md +111 -0
  8. package/examples/basic-api/index.ts +177 -0
  9. package/package.json +68 -4
  10. package/src/ai/mcp.ts +140 -0
  11. package/src/auth/auth.ts +110 -0
  12. package/src/auth/crypto.ts +70 -0
  13. package/src/auth/jwt.ts +105 -0
  14. package/src/cache/cache.ts +72 -0
  15. package/src/cli/bench.ts +118 -0
  16. package/src/cli/compile.ts +29 -0
  17. package/src/cli/create-app.ts +284 -0
  18. package/src/cli/docker.ts +51 -0
  19. package/src/cli/generator.ts +75 -0
  20. package/src/cli/routes.ts +76 -0
  21. package/src/client.ts +120 -0
  22. package/src/compat/express.ts +139 -0
  23. package/src/core/app.ts +723 -0
  24. package/src/core/context.ts +218 -0
  25. package/src/core/error.ts +42 -0
  26. package/src/core/router.ts +262 -0
  27. package/src/db/db-interface.ts +9 -0
  28. package/src/db/hub.ts +256 -0
  29. package/src/db/memory-adapter.ts +202 -0
  30. package/src/db/migrator.ts +100 -0
  31. package/src/db/sqlite-adapter.ts +58 -0
  32. package/src/docs/collection.ts +57 -0
  33. package/src/docs/scalar-ui.ts +25 -0
  34. package/src/events/event-bus.ts +61 -0
  35. package/src/index.ts +37 -0
  36. package/src/jobs/cron.ts +77 -0
  37. package/src/jobs/queue.ts +108 -0
  38. package/src/logging/logger.ts +66 -0
  39. package/src/metrics/metrics.ts +66 -0
  40. package/src/modules/product/product.route.ts +36 -0
  41. package/src/modules/product/product.schema.ts +10 -0
  42. package/src/schema/mock.ts +60 -0
  43. package/src/schema/openapi.ts +101 -0
  44. package/src/schema/schema.ts +519 -0
  45. package/src/schema/validator.ts +5 -0
  46. package/src/shield/compress.ts +61 -0
  47. package/src/shield/cors.ts +94 -0
  48. package/src/shield/headers.ts +55 -0
  49. package/src/shield/idempotency.ts +76 -0
  50. package/src/shield/rate-limiter.ts +81 -0
  51. package/src/shield/sanitizer.ts +23 -0
  52. package/src/shield/webhook.ts +101 -0
  53. package/src/static/serve.ts +51 -0
  54. package/src/storage/storage.ts +176 -0
  55. package/src/stream/sse.ts +89 -0
  56. package/src/studio/studio.ts +221 -0
  57. package/src/types.ts +241 -0
  58. package/src/ws/ws.ts +118 -0
  59. package/test/ai-mcp.test.ts +61 -0
  60. package/test/auth.test.ts +108 -0
  61. package/test/bench.test.ts +31 -0
  62. package/test/cache.test.ts +36 -0
  63. package/test/cli-create.test.ts +64 -0
  64. package/test/client.test.ts +54 -0
  65. package/test/collection-export.test.ts +52 -0
  66. package/test/compat.test.ts +48 -0
  67. package/test/compress.test.ts +48 -0
  68. package/test/context.test.ts +48 -0
  69. package/test/db.test.ts +62 -0
  70. package/test/docker.test.ts +28 -0
  71. package/test/events.test.ts +62 -0
  72. package/test/file-upload.test.ts +116 -0
  73. package/test/hooks.test.ts +77 -0
  74. package/test/idempotency.test.ts +65 -0
  75. package/test/integration.test.ts +111 -0
  76. package/test/jobs.test.ts +60 -0
  77. package/test/metrics.test.ts +40 -0
  78. package/test/mock.test.ts +33 -0
  79. package/test/multi-tenant.test.ts +80 -0
  80. package/test/observability.test.ts +95 -0
  81. package/test/openapi.test.ts +41 -0
  82. package/test/orm-relations.test.ts +85 -0
  83. package/test/orm-shortcuts.test.ts +52 -0
  84. package/test/rbac.test.ts +85 -0
  85. package/test/router-fastpath.test.ts +42 -0
  86. package/test/router.test.ts +84 -0
  87. package/test/schema.test.ts +49 -0
  88. package/test/seeder-migrator.test.ts +78 -0
  89. package/test/shield.test.ts +55 -0
  90. package/test/shorthand-dx.test.ts +31 -0
  91. package/test/sse.test.ts +43 -0
  92. package/test/static-serve.test.ts +53 -0
  93. package/test/storage.test.ts +31 -0
  94. package/test/studio.test.ts +43 -0
  95. package/test/tx.test.ts +92 -0
  96. package/test/webhook.test.ts +113 -0
  97. package/test/ws.test.ts +54 -0
  98. package/tsconfig.json +19 -0
package/package.json CHANGED
@@ -1,6 +1,70 @@
1
1
  {
2
2
  "name": "@tamayrp14/fire-api",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Ultra-lightweight, blazing-fast, type-safe API framework with universal DB, security shield, turnkey auth, express compat, dev studio, and native AI/MCP integration",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "type": "module",
8
+ "bin": {
9
+ "fire-api": "./bin/fire.js",
10
+ "fire": "./bin/fire.js"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/yogarizyapratama/FIRE-API.git"
15
+ },
16
+ "homepage": "https://github.com/yogarizyapratama/FIRE-API#readme",
17
+ "bugs": {
18
+ "url": "https://github.com/yogarizyapratama/FIRE-API/issues"
19
+ },
20
+ "author": "Yoga Rizya Pratama",
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "exports": {
25
+ ".": {
26
+ "import": "./dist/index.js",
27
+ "types": "./dist/index.d.ts"
28
+ },
29
+ "./client": {
30
+ "import": "./dist/client.js",
31
+ "types": "./dist/client.d.ts"
32
+ },
33
+ "./auth": {
34
+ "import": "./dist/auth/auth.js",
35
+ "types": "./dist/auth/auth.d.ts"
36
+ },
37
+ "./compat": {
38
+ "import": "./dist/compat/express.js",
39
+ "types": "./dist/compat/express.d.ts"
40
+ },
41
+ "./studio": {
42
+ "import": "./dist/studio/studio.js",
43
+ "types": "./dist/studio/studio.d.ts"
44
+ }
45
+ },
46
+ "scripts": {
47
+ "test": "bun test",
48
+ "build": "bun build ./src/index.ts ./src/client.ts ./src/auth/auth.ts ./src/compat/express.ts ./src/studio/studio.ts --outdir ./dist --target node && tsc --module ESNext --moduleResolution bundler --declaration --emitDeclarationOnly --outDir dist",
49
+ "prepublishOnly": "bun run build"
50
+ },
51
+ "keywords": [
52
+ "framework",
53
+ "api",
54
+ "typescript",
55
+ "fast",
56
+ "bun",
57
+ "node",
58
+ "mcp",
59
+ "sqlite",
60
+ "postgres",
61
+ "auth",
62
+ "studio",
63
+ "rest"
64
+ ],
65
+ "license": "MIT",
66
+ "devDependencies": {
67
+ "@types/node": "^22.13.0",
68
+ "typescript": "^5.7.3"
69
+ }
70
+ }
package/src/ai/mcp.ts ADDED
@@ -0,0 +1,140 @@
1
+ import { Router } from '../core/router';
2
+ import { createContext } from '../core/context';
3
+ import { BaseSchema } from '../schema/schema';
4
+
5
+ export interface MCPToolManifest {
6
+ name: string;
7
+ description: string;
8
+ inputSchema: Record<string, any>;
9
+ }
10
+
11
+ export interface MCPToolResult {
12
+ content: { type: 'text'; text: string }[];
13
+ isError?: boolean;
14
+ }
15
+
16
+ export interface MCPServerOptions {
17
+ name?: string;
18
+ version?: string;
19
+ }
20
+
21
+ export function routeToToolName(method: string, path: string): string {
22
+ const clean = path
23
+ .replace(/^\//, '')
24
+ .replace(/:([a-zA-Z0-9_]+)/g, '$1')
25
+ .replace(/[^a-zA-Z0-9_]/g, '_')
26
+ .replace(/_+/g, '_')
27
+ .replace(/_$/, '');
28
+ return `${method.toLowerCase()}_${clean || 'root'}`;
29
+ }
30
+
31
+ export class MCPServer {
32
+ constructor(private router: Router, private options: MCPServerOptions = {}) {}
33
+
34
+ public listTools(): MCPToolManifest[] {
35
+ const tools: MCPToolManifest[] = [];
36
+
37
+ for (const route of this.router.routesList) {
38
+ if (!route.options.ai) continue;
39
+
40
+ const toolName = route.options.ai.operationId || routeToToolName(route.method, route.path);
41
+ const schema = route.options.schema;
42
+
43
+ let inputSchema: Record<string, any> = { type: 'object', properties: {} };
44
+
45
+ if (schema?.body && schema.body instanceof BaseSchema) {
46
+ inputSchema = schema.body.toOpenAPI();
47
+ } else if (schema?.params && schema.params instanceof BaseSchema) {
48
+ inputSchema = schema.params.toOpenAPI();
49
+ } else if (schema?.query && schema.query instanceof BaseSchema) {
50
+ inputSchema = schema.query.toOpenAPI();
51
+ }
52
+
53
+ tools.push({
54
+ name: toolName,
55
+ description: route.options.ai.description || `${route.method} ${route.path}`,
56
+ inputSchema,
57
+ });
58
+ }
59
+
60
+ return tools;
61
+ }
62
+
63
+ public async callTool(name: string, args: Record<string, any> = {}): Promise<MCPToolResult> {
64
+ for (const route of this.router.routesList) {
65
+ if (!route.options.ai) continue;
66
+ const toolName = route.options.ai.operationId || routeToToolName(route.method, route.path);
67
+
68
+ if (toolName === name) {
69
+ try {
70
+ // Construct simulated request and context
71
+ const req = new Request(`http://localhost${route.path}`, {
72
+ method: route.method,
73
+ headers: { 'Content-Type': 'application/json' },
74
+ body: route.method !== 'GET' && route.method !== 'HEAD' ? JSON.stringify(args) : undefined,
75
+ });
76
+
77
+ const ctx = createContext({
78
+ req,
79
+ params: args,
80
+ body: args,
81
+ });
82
+
83
+ const res = await route.options.handler(ctx);
84
+ let outputText = '';
85
+
86
+ if (res instanceof Response) {
87
+ outputText = await res.text();
88
+ } else {
89
+ outputText = typeof res === 'string' ? res : JSON.stringify(res);
90
+ }
91
+
92
+ return {
93
+ content: [{ type: 'text', text: outputText }],
94
+ isError: false,
95
+ };
96
+ } catch (err: any) {
97
+ return {
98
+ content: [{ type: 'text', text: err?.message || 'Error executing tool' }],
99
+ isError: true,
100
+ };
101
+ }
102
+ }
103
+ }
104
+
105
+ return {
106
+ content: [{ type: 'text', text: `Tool not found: ${name}` }],
107
+ isError: true,
108
+ };
109
+ }
110
+
111
+ public handleMCPProtocol(message: { method: string; params?: any; id?: any }): Promise<any> {
112
+ const { method, params, id } = message;
113
+
114
+ if (method === 'tools/list') {
115
+ return Promise.resolve({
116
+ jsonrpc: '2.0',
117
+ id,
118
+ result: { tools: this.listTools() },
119
+ });
120
+ }
121
+
122
+ if (method === 'tools/call') {
123
+ return this.callTool(params?.name, params?.arguments).then((res) => ({
124
+ jsonrpc: '2.0',
125
+ id,
126
+ result: res,
127
+ }));
128
+ }
129
+
130
+ return Promise.resolve({
131
+ jsonrpc: '2.0',
132
+ id,
133
+ error: { code: -32601, message: 'Method not found' },
134
+ });
135
+ }
136
+ }
137
+
138
+ export function createMCPServer(router: Router, options: MCPServerOptions = {}): MCPServer {
139
+ return new MCPServer(router, options);
140
+ }
@@ -0,0 +1,110 @@
1
+ import { createRouter, Router } from '../core/router';
2
+ import { Middleware } from '../types';
3
+ import { s } from '../schema/schema';
4
+ import { hashPassword, verifyPassword } from './crypto';
5
+ import { signJWT, verifyJWT } from './jwt';
6
+
7
+ export interface CreateAuthOptions {
8
+ jwtSecret: string;
9
+ tableName?: string;
10
+ tokenExpirySeconds?: number;
11
+ }
12
+
13
+ export function createAuth(options: CreateAuthOptions): Router {
14
+ const router = createRouter();
15
+ const tableName = options.tableName || 'users';
16
+ const secret = options.jwtSecret;
17
+ const expiry = options.tokenExpirySeconds || 86400; // 24 hours
18
+
19
+ // POST /register
20
+ router.post('/register', {
21
+ schema: {
22
+ body: s.object({
23
+ email: s.string().email(),
24
+ password: s.string().min(6),
25
+ name: s.string().optional(),
26
+ }),
27
+ },
28
+ ai: { description: 'Register a new user account with credentials', tags: ['Auth'] },
29
+ handler: async (ctx) => {
30
+ const { email, password, name } = ctx.body;
31
+
32
+ // Check if user already exists
33
+ const existing = await ctx.db.table(tableName).where('email', email).findOne();
34
+ if (existing) {
35
+ return ctx.badRequest('A user with this email already exists');
36
+ }
37
+
38
+ const hashedPassword = await hashPassword(password);
39
+ const newUser = await ctx.db.table(tableName).insert({
40
+ email,
41
+ password: hashedPassword,
42
+ name: name || email.split('@')[0],
43
+ });
44
+
45
+ // Strip password from returned user object
46
+ const safeUser = { id: newUser.id, email: newUser.email, name: newUser.name };
47
+ const token = await signJWT(safeUser, secret, expiry);
48
+
49
+ return ctx.created({ user: safeUser, token });
50
+ },
51
+ });
52
+
53
+ // POST /login
54
+ router.post('/login', {
55
+ schema: {
56
+ body: s.object({
57
+ email: s.string().email(),
58
+ password: s.string(),
59
+ }),
60
+ },
61
+ ai: { description: 'Authenticate user with email and password', tags: ['Auth'] },
62
+ handler: async (ctx) => {
63
+ const { email, password } = ctx.body;
64
+
65
+ const user = await ctx.db.table(tableName).where('email', email).findOne();
66
+ if (!user) {
67
+ return ctx.unauthorized('Invalid email or password');
68
+ }
69
+
70
+ const isValid = await verifyPassword(password, user.password);
71
+ if (!isValid) {
72
+ return ctx.unauthorized('Invalid email or password');
73
+ }
74
+
75
+ const safeUser = { id: user.id, email: user.email, name: user.name };
76
+ const token = await signJWT(safeUser, secret, expiry);
77
+
78
+ return ctx.ok({ user: safeUser, token });
79
+ },
80
+ });
81
+
82
+ // GET /me
83
+ router.get('/me', {
84
+ middlewares: [requireAuth(secret)],
85
+ ai: { description: 'Get authenticated user profile', tags: ['Auth'] },
86
+ handler: (ctx) => {
87
+ return ctx.ok({ user: ctx.user });
88
+ },
89
+ });
90
+
91
+ return router;
92
+ }
93
+
94
+ export function requireAuth(secret: string): Middleware {
95
+ return async (ctx, next) => {
96
+ const authHeader = ctx.req.headers.get('authorization') || ctx.req.headers.get('Authorization');
97
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
98
+ return ctx.unauthorized('Missing or invalid Authorization header');
99
+ }
100
+
101
+ const token = authHeader.slice(7).trim();
102
+ try {
103
+ const payload = await verifyJWT(token, secret);
104
+ ctx.user = payload;
105
+ return next();
106
+ } catch {
107
+ return ctx.unauthorized('Invalid or expired token');
108
+ }
109
+ };
110
+ }
@@ -0,0 +1,70 @@
1
+ function bufferToHex(buffer: ArrayBuffer | Uint8Array): string {
2
+ return Array.from(new Uint8Array(buffer as any))
3
+ .map((b) => b.toString(16).padStart(2, '0'))
4
+ .join('');
5
+ }
6
+
7
+ function hexToBuffer(hex: string): Uint8Array {
8
+ const bytes = new Uint8Array(hex.length / 2);
9
+ for (let i = 0; i < hex.length; i += 2) {
10
+ bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
11
+ }
12
+ return bytes;
13
+ }
14
+
15
+ export async function hashPassword(password: string): Promise<string> {
16
+ const salt = new Uint8Array(16);
17
+ crypto.getRandomValues(salt);
18
+
19
+ const enc = new TextEncoder();
20
+ const keyMaterial = await crypto.subtle.importKey(
21
+ 'raw',
22
+ enc.encode(password),
23
+ 'PBKDF2',
24
+ false,
25
+ ['deriveBits', 'deriveKey']
26
+ );
27
+
28
+ const derived = await crypto.subtle.deriveBits(
29
+ {
30
+ name: 'PBKDF2',
31
+ salt: salt as any,
32
+ iterations: 100000,
33
+ hash: 'SHA-256',
34
+ },
35
+ keyMaterial,
36
+ 256
37
+ );
38
+
39
+ return `${bufferToHex(salt)}:${bufferToHex(derived)}`;
40
+ }
41
+
42
+ export async function verifyPassword(password: string, storedHash: string): Promise<boolean> {
43
+ const parts = storedHash.split(':');
44
+ if (parts.length !== 2) return false;
45
+
46
+ const [saltHex, originalHashHex] = parts;
47
+ const salt = hexToBuffer(saltHex);
48
+
49
+ const enc = new TextEncoder();
50
+ const keyMaterial = await crypto.subtle.importKey(
51
+ 'raw',
52
+ enc.encode(password),
53
+ 'PBKDF2',
54
+ false,
55
+ ['deriveBits', 'deriveKey']
56
+ );
57
+
58
+ const derived = await crypto.subtle.deriveBits(
59
+ {
60
+ name: 'PBKDF2',
61
+ salt: salt as any,
62
+ iterations: 100000,
63
+ hash: 'SHA-256',
64
+ },
65
+ keyMaterial,
66
+ 256
67
+ );
68
+
69
+ return bufferToHex(derived) === originalHashHex;
70
+ }
@@ -0,0 +1,105 @@
1
+ function base64UrlEncode(str: string): string {
2
+ const bytes = new TextEncoder().encode(str);
3
+ let binary = '';
4
+ for (let i = 0; i < bytes.byteLength; i++) {
5
+ binary += String.fromCharCode(bytes[i]);
6
+ }
7
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
8
+ }
9
+
10
+ function base64UrlDecode(str: string): string {
11
+ let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
12
+ while (base64.length % 4) {
13
+ base64 += '=';
14
+ }
15
+ const binary = atob(base64);
16
+ const bytes = new Uint8Array(binary.length);
17
+ for (let i = 0; i < binary.length; i++) {
18
+ bytes[i] = binary.charCodeAt(i);
19
+ }
20
+ return new TextDecoder().decode(bytes);
21
+ }
22
+
23
+ function arrayBufferToBase64Url(buffer: ArrayBuffer): string {
24
+ const bytes = new Uint8Array(buffer);
25
+ let binary = '';
26
+ for (let i = 0; i < bytes.byteLength; i++) {
27
+ binary += String.fromCharCode(bytes[i]);
28
+ }
29
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
30
+ }
31
+
32
+ export async function signJWT(
33
+ payload: Record<string, any>,
34
+ secret: string,
35
+ expiresInSeconds = 86400
36
+ ): Promise<string> {
37
+ const header = { alg: 'HS256', typ: 'JWT' };
38
+ const now = Math.floor(Date.now() / 1000);
39
+ const fullPayload = {
40
+ ...payload,
41
+ iat: now,
42
+ exp: now + expiresInSeconds,
43
+ };
44
+
45
+ const encodedHeader = base64UrlEncode(JSON.stringify(header));
46
+ const encodedPayload = base64UrlEncode(JSON.stringify(fullPayload));
47
+ const dataToSign = `${encodedHeader}.${encodedPayload}`;
48
+
49
+ const enc = new TextEncoder();
50
+ const key = await crypto.subtle.importKey(
51
+ 'raw',
52
+ enc.encode(secret),
53
+ { name: 'HMAC', hash: 'SHA-256' },
54
+ false,
55
+ ['sign']
56
+ );
57
+
58
+ const signature = await crypto.subtle.sign('HMAC', key, enc.encode(dataToSign));
59
+ const encodedSignature = arrayBufferToBase64Url(signature);
60
+
61
+ return `${dataToSign}.${encodedSignature}`;
62
+ }
63
+
64
+ export async function verifyJWT<T = any>(token: string, secret: string): Promise<T> {
65
+ const parts = token.split('.');
66
+ if (parts.length !== 3) {
67
+ throw new Error('Invalid JWT format');
68
+ }
69
+
70
+ const [encodedHeader, encodedPayload, encodedSignature] = parts;
71
+ const dataToVerify = `${encodedHeader}.${encodedPayload}`;
72
+
73
+ const enc = new TextEncoder();
74
+ const key = await crypto.subtle.importKey(
75
+ 'raw',
76
+ enc.encode(secret),
77
+ { name: 'HMAC', hash: 'SHA-256' },
78
+ false,
79
+ ['verify']
80
+ );
81
+
82
+ // Decode signature to ArrayBuffer
83
+ let base64Sig = encodedSignature.replace(/-/g, '+').replace(/_/g, '/');
84
+ while (base64Sig.length % 4) {
85
+ base64Sig += '=';
86
+ }
87
+ const binarySig = atob(base64Sig);
88
+ const sigBytes = new Uint8Array(binarySig.length);
89
+ for (let i = 0; i < binarySig.length; i++) {
90
+ sigBytes[i] = binarySig.charCodeAt(i);
91
+ }
92
+
93
+ const isValid = await crypto.subtle.verify('HMAC', key, sigBytes, enc.encode(dataToVerify));
94
+ if (!isValid) {
95
+ throw new Error('Invalid JWT signature');
96
+ }
97
+
98
+ const payload: any = JSON.parse(base64UrlDecode(encodedPayload));
99
+ const now = Math.floor(Date.now() / 1000);
100
+ if (payload.exp && payload.exp < now) {
101
+ throw new Error('JWT token has expired');
102
+ }
103
+
104
+ return payload as T;
105
+ }
@@ -0,0 +1,72 @@
1
+ import { FireCacheManager } from '../types';
2
+
3
+ interface CacheEntry {
4
+ value: any;
5
+ expiresAt: number;
6
+ tags: Set<string>;
7
+ }
8
+
9
+ export class MemoryCacheManager implements FireCacheManager {
10
+ private cache: Map<string, CacheEntry> = new Map();
11
+ private tagIndex: Map<string, Set<string>> = new Map(); // tag -> Set of keys
12
+
13
+ public async get<T = any>(key: string): Promise<T | null> {
14
+ const entry = this.cache.get(key);
15
+ if (!entry) return null;
16
+
17
+ if (Date.now() > entry.expiresAt) {
18
+ this.deleteKey(key);
19
+ return null;
20
+ }
21
+
22
+ return entry.value as T;
23
+ }
24
+
25
+ public async set(key: string, value: any, ttlSeconds = 60, tags: string[] = []): Promise<void> {
26
+ const expiresAt = Date.now() + ttlSeconds * 1000;
27
+ const tagSet = new Set(tags);
28
+
29
+ // If key already existed with other tags, clean up old index
30
+ this.deleteKey(key);
31
+
32
+ this.cache.set(key, { value, expiresAt, tags: tagSet });
33
+
34
+ for (const tag of tags) {
35
+ if (!this.tagIndex.has(tag)) {
36
+ this.tagIndex.set(tag, new Set());
37
+ }
38
+ this.tagIndex.get(tag)!.add(key);
39
+ }
40
+ }
41
+
42
+ public async invalidate(tag: string): Promise<void> {
43
+ const keys = this.tagIndex.get(tag);
44
+ if (!keys) return;
45
+
46
+ for (const key of Array.from(keys)) {
47
+ this.deleteKey(key);
48
+ }
49
+ this.tagIndex.delete(tag);
50
+ }
51
+
52
+ public async clear(): Promise<void> {
53
+ this.cache.clear();
54
+ this.tagIndex.clear();
55
+ }
56
+
57
+ private deleteKey(key: string): void {
58
+ const entry = this.cache.get(key);
59
+ if (entry) {
60
+ for (const tag of entry.tags) {
61
+ const tagSet = this.tagIndex.get(tag);
62
+ if (tagSet) {
63
+ tagSet.delete(key);
64
+ if (tagSet.size === 0) {
65
+ this.tagIndex.delete(tag);
66
+ }
67
+ }
68
+ }
69
+ this.cache.delete(key);
70
+ }
71
+ }
72
+ }
@@ -0,0 +1,118 @@
1
+ export interface BenchmarkOptions {
2
+ concurrency?: number;
3
+ totalRequests?: number;
4
+ durationSec?: number;
5
+ customFetch?: (url: string) => Promise<Response>;
6
+ }
7
+
8
+ export interface BenchmarkStats {
9
+ url: string;
10
+ totalRequests: number;
11
+ successfulRequests: number;
12
+ failedRequests: number;
13
+ durationMs: number;
14
+ rps: number;
15
+ latency: {
16
+ min: number;
17
+ max: number;
18
+ avg: number;
19
+ p50: number;
20
+ p90: number;
21
+ p95: number;
22
+ p99: number;
23
+ };
24
+ }
25
+
26
+ function percentile(sorted: number[], p: number): number {
27
+ if (sorted.length === 0) return 0;
28
+ const index = Math.ceil((p / 100) * sorted.length) - 1;
29
+ return sorted[Math.max(0, Math.min(index, sorted.length - 1))];
30
+ }
31
+
32
+ export async function runBenchmark(url: string, options: BenchmarkOptions = {}): Promise<BenchmarkStats> {
33
+ const concurrency = options.concurrency || 10;
34
+ const total = options.totalRequests || 200;
35
+ const fetchFn = options.customFetch || globalThis.fetch;
36
+
37
+ const latencies: number[] = [];
38
+ let successfulRequests = 0;
39
+ let failedRequests = 0;
40
+ let sent = 0;
41
+
42
+ const startTime = performance.now();
43
+
44
+ async function worker() {
45
+ while (sent < total) {
46
+ sent++;
47
+ const reqStart = performance.now();
48
+ try {
49
+ const res = await fetchFn(url);
50
+ const reqDuration = performance.now() - reqStart;
51
+ latencies.push(reqDuration);
52
+ if (res.ok) {
53
+ successfulRequests++;
54
+ } else {
55
+ failedRequests++;
56
+ }
57
+ } catch {
58
+ const reqDuration = performance.now() - reqStart;
59
+ latencies.push(reqDuration);
60
+ failedRequests++;
61
+ }
62
+ }
63
+ }
64
+
65
+ const workers = Array.from({ length: concurrency }, () => worker());
66
+ await Promise.all(workers);
67
+
68
+ const durationMs = performance.now() - startTime;
69
+ const rps = Math.round((latencies.length / (durationMs / 1000)) * 100) / 100;
70
+
71
+ latencies.sort((a, b) => a - b);
72
+ const sum = latencies.reduce((acc, val) => acc + val, 0);
73
+
74
+ return {
75
+ url,
76
+ totalRequests: latencies.length,
77
+ successfulRequests,
78
+ failedRequests,
79
+ durationMs: Math.round(durationMs),
80
+ rps,
81
+ latency: {
82
+ min: Math.round((latencies[0] || 0) * 100) / 100,
83
+ max: Math.round((latencies[latencies.length - 1] || 0) * 100) / 100,
84
+ avg: Math.round((sum / (latencies.length || 1)) * 100) / 100,
85
+ p50: Math.round(percentile(latencies, 50) * 100) / 100,
86
+ p90: Math.round(percentile(latencies, 90) * 100) / 100,
87
+ p95: Math.round(percentile(latencies, 95) * 100) / 100,
88
+ p99: Math.round(percentile(latencies, 99) * 100) / 100,
89
+ },
90
+ };
91
+ }
92
+
93
+ export function formatBenchmarkReport(stats: BenchmarkStats): string {
94
+ const successRate = stats.totalRequests > 0
95
+ ? ((stats.successfulRequests / stats.totalRequests) * 100).toFixed(1)
96
+ : '0';
97
+
98
+ return `
99
+ ==================================================
100
+ 🔥 FireAPI Benchmark Engine
101
+ ==================================================
102
+ Target: ${stats.url}
103
+ Duration: ${stats.durationMs} ms
104
+ Total Reqs: ${stats.totalRequests}
105
+ Success Rate: ${successRate}% (${stats.successfulRequests} ok / ${stats.failedRequests} fail)
106
+
107
+ 🚀 Requests/sec: ${stats.rps.toLocaleString()} req/s
108
+
109
+ Latency (ms):
110
+ Min: ${stats.latency.min} ms
111
+ P50: ${stats.latency.p50} ms
112
+ P90: ${stats.latency.p90} ms
113
+ P95: ${stats.latency.p95} ms
114
+ P99: ${stats.latency.p99} ms
115
+ Max: ${stats.latency.max} ms
116
+ ==================================================
117
+ `;
118
+ }