@aetherframework/middleware 1.0.2

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.
@@ -0,0 +1,347 @@
1
+ /**
2
+ * @license MIT
3
+ * Copyright (c) 2026-present AetherFramework Contributors.
4
+ * SPDX-License-Identifier: MIT
5
+ * @module @aetherframework/middleware/core/AetherRouter
6
+ */
7
+
8
+ import { EventEmitter } from "events";
9
+
10
+ /**
11
+ * AetherRouter - High-performance routing system for AetherJS
12
+ * Supports versioning, grouping, parameter parsing, and middleware chaining
13
+ */
14
+ class AetherRouter extends EventEmitter {
15
+ constructor(options = {}) {
16
+ super();
17
+ this.routes = new Map(); // Store all routes: method+path -> handler
18
+ this.groups = new Map(); // Store route groups
19
+ this.middlewares = []; // Global middlewares
20
+ this.prefix = options.prefix || ""; // Route prefix
21
+ this.version = options.version || ""; // API version
22
+
23
+ // Supported HTTP methods
24
+ this.methods = [
25
+ "GET", "POST", "PUT", "DELETE",
26
+ "PATCH", "OPTIONS", "HEAD", "ANY"
27
+ ];
28
+
29
+ // Initialize all HTTP method handlers
30
+ this.methods.forEach(method => {
31
+ this[method.toLowerCase()] = this._createRouteHandler(method);
32
+ });
33
+
34
+ // Special method: match any HTTP method
35
+ this.all = this._createRouteHandler("ANY");
36
+ }
37
+
38
+ /**
39
+ * Create route handler for specific HTTP method
40
+ * @private
41
+ */
42
+ _createRouteHandler(method) {
43
+ return (path, ...handlers) => {
44
+ const fullPath = this._buildPath(path);
45
+ const route = {
46
+ method: method === "ANY" ? null : method,
47
+ path: fullPath,
48
+ handlers: this._wrapHandlers(handlers),
49
+ regex: this._pathToRegex(fullPath),
50
+ paramNames: this._extractParamNames(fullPath)
51
+ };
52
+
53
+ const routeKey = `${method}:${fullPath}`;
54
+ this.routes.set(routeKey, route);
55
+
56
+ this.emit("route:added", { method, path: fullPath, handlers: handlers.length });
57
+ return this;
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Build full path with prefix and version
63
+ * @private
64
+ */
65
+ _buildPath(path) {
66
+ let fullPath = "";
67
+
68
+ // Add version prefix
69
+ if (this.version) {
70
+ fullPath += `/v${this.version.replace(/[^0-9.]/g, "")}`;
71
+ }
72
+
73
+ // Add group prefix
74
+ if (this.prefix) {
75
+ fullPath += `/${this.prefix.replace(/^\/|\/$/g, "")}`;
76
+ }
77
+
78
+ // Add route path
79
+ fullPath += `/${path.replace(/^\/|\/$/g, "")}`;
80
+
81
+ // Normalize path
82
+ return fullPath.replace(/\/+/g, "/").replace(/\/$/, "") || "/";
83
+ }
84
+
85
+ /**
86
+ * Convert path pattern to regex
87
+ * @private
88
+ */
89
+ _pathToRegex(path) {
90
+ const pattern = path
91
+ .replace(/:(\w+)/g, "(?<$1>[^/]+)") // Named parameters: :id
92
+ .replace(/\*(\w+)?/g, (_, name) => name ? `(?<${name}>.*)` : "(.*)") // Wildcards
93
+ .replace(/$([^)]+)$/g, "(?:$1)") // Optional groups
94
+ .replace(/\?/g, "\\?"); // Escape question marks
95
+
96
+ return new RegExp(`^${pattern}$`);
97
+ }
98
+
99
+ /**
100
+ * Extract parameter names from path
101
+ * @private
102
+ */
103
+ _extractParamNames(path) {
104
+ const paramNames = [];
105
+ const paramPattern = /:(\w+)/g;
106
+ const wildcardPattern = /\*(\w+)?/g;
107
+
108
+ let match;
109
+ while ((match = paramPattern.exec(path)) !== null) {
110
+ paramNames.push(match);
111
+ }
112
+
113
+ while ((match = wildcardPattern.exec(path)) !== null) {
114
+ if (match) paramNames.push(match);
115
+ }
116
+
117
+ return paramNames;
118
+ }
119
+
120
+ /**
121
+ * Wrap handlers with validation
122
+ * @private
123
+ */
124
+ _wrapHandlers(handlers) {
125
+ return handlers.map(handler => {
126
+ if (typeof handler !== "function") {
127
+ throw new TypeError("Route handler must be a function");
128
+ }
129
+ return handler;
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Route grouping
135
+ * @param {string} prefix - Group prefix
136
+ * @param {Function} callback - Group definition function
137
+ */
138
+ group(prefix, callback) {
139
+ const router = new AetherRouter({
140
+ prefix: `${this.prefix}/${prefix}`.replace(/\/+/g, "/"),
141
+ version: this.version
142
+ });
143
+
144
+ // Inherit global middlewares
145
+ router.middlewares = [...this.middlewares];
146
+
147
+ // Execute group definition
148
+ callback(router);
149
+
150
+ // Merge group routes into main router
151
+ router.routes.forEach((route, key) => {
152
+ this.routes.set(key, route);
153
+ });
154
+
155
+ return this;
156
+ }
157
+
158
+ /**
159
+ * API versioning
160
+ * @param {string} version - Version number (e.g., "1", "2.0")
161
+ * @param {Function} callback - Version definition function
162
+ */
163
+ version(version, callback) {
164
+ const router = new AetherRouter({
165
+ prefix: this.prefix,
166
+ version: version
167
+ });
168
+
169
+ // Inherit global middlewares
170
+ router.middlewares = [...this.middlewares];
171
+
172
+ // Execute version definition
173
+ callback(router);
174
+
175
+ // Merge version routes into main router
176
+ router.routes.forEach((route, key) => {
177
+ this.routes.set(key, route);
178
+ });
179
+
180
+ return this;
181
+ }
182
+
183
+ /**
184
+ * Add middleware to router
185
+ * @param {...Function} middlewares - Middleware functions
186
+ */
187
+ use(...middlewares) {
188
+ this.middlewares.push(...middlewares);
189
+ return this;
190
+ }
191
+
192
+ /**
193
+ * Match route for incoming request
194
+ * @param {string} method - HTTP method
195
+ * @param {string} url - Request URL
196
+ * @returns {Object|null} - Matched route info or null
197
+ */
198
+ match(method, url) {
199
+ // Parse URL and query parameters
200
+ const [pathname, search] = url.split("?");
201
+ const query = this._parseQuery(search);
202
+
203
+ // Find matching route
204
+ for (const [routeKey, route] of this.routes) {
205
+ const [routeMethod, routePath] = routeKey.split(":");
206
+
207
+ // Check method match
208
+ if (routeMethod !== "ANY" && routeMethod !== method) {
209
+ continue;
210
+ }
211
+
212
+ // Check path match
213
+ const match = pathname.match(route.regex);
214
+ if (match) {
215
+ const params = {};
216
+
217
+ // Extract named parameters
218
+ if (match.groups) {
219
+ Object.assign(params, match.groups);
220
+ }
221
+
222
+ // Extract positional parameters
223
+ route.paramNames.forEach((name, index) => {
224
+ if (!params[name] && match[index + 1]) {
225
+ params[name] = match[index + 1];
226
+ }
227
+ });
228
+
229
+ return {
230
+ route,
231
+ params,
232
+ query,
233
+ handlers: [...this.middlewares, ...route.handlers]
234
+ };
235
+ }
236
+ }
237
+
238
+ return null;
239
+ }
240
+
241
+ /**
242
+ * Parse query string parameters
243
+ * @private
244
+ */
245
+ _parseQuery(search) {
246
+ const query = {};
247
+ if (!search) return query;
248
+
249
+ const pairs = search.split("&");
250
+ for (const pair of pairs) {
251
+ const [key, value] = pair.split("=");
252
+ if (key) {
253
+ const decodedKey = decodeURIComponent(key);
254
+ const decodedValue = value ? decodeURIComponent(value) : "";
255
+
256
+ // Support array parameters: key[]=value1&key[]=value2
257
+ if (decodedKey.endsWith("[]")) {
258
+ const arrayKey = decodedKey.slice(0, -2);
259
+ if (!query[arrayKey]) {
260
+ query[arrayKey] = [];
261
+ }
262
+ query[arrayKey].push(decodedValue);
263
+ } else {
264
+ query[decodedKey] = decodedValue;
265
+ }
266
+ }
267
+ }
268
+
269
+ return query;
270
+ }
271
+
272
+ /**
273
+ * Generate router middleware for AetherPipeline
274
+ */
275
+ middleware() {
276
+ return async (context, next) => {
277
+ const match = this.match(context.method, context.url);
278
+
279
+ if (match) {
280
+ // Set route parameters and query parameters
281
+ context.params = match.params;
282
+ context.setState("query", match.query);
283
+ context.route = match.route;
284
+
285
+ // Execute route handler chain
286
+ await this._executeHandlers(context, match.handlers);
287
+ } else if (typeof next === "function") {
288
+ // No matching route, continue to next middleware
289
+ await next();
290
+ } else {
291
+ // Return 404
292
+ context.setStatus(404).json({
293
+ error: "Not Found",
294
+ message: `Route ${context.method} ${context.url} not found`,
295
+ timestamp: new Date().toISOString()
296
+ });
297
+ }
298
+ };
299
+ }
300
+
301
+ /**
302
+ * Execute handler chain
303
+ * @private
304
+ */
305
+ async _executeHandlers(context, handlers) {
306
+ let index = 0;
307
+
308
+ const executeNext = async () => {
309
+ if (index >= handlers.length || context.isTerminated()) {
310
+ return;
311
+ }
312
+
313
+ const handler = handlers[index++];
314
+ await handler(context, executeNext);
315
+ };
316
+
317
+ await executeNext();
318
+ }
319
+
320
+ /**
321
+ * Get all registered routes (for debugging)
322
+ */
323
+ getRoutes() {
324
+ const routes = [];
325
+ this.routes.forEach((route, key) => {
326
+ const [method, path] = key.split(":");
327
+ routes.push({
328
+ method: method === "ANY" ? "ALL" : method,
329
+ path,
330
+ handlers: route.handlers.length
331
+ });
332
+ });
333
+ return routes;
334
+ }
335
+
336
+ /**
337
+ * Clear all routes and middlewares
338
+ */
339
+ clear() {
340
+ this.routes.clear();
341
+ this.groups.clear();
342
+ this.middlewares = [];
343
+ return this;
344
+ }
345
+ }
346
+
347
+ export default AetherRouter;
@@ -0,0 +1,204 @@
1
+ /**
2
+ * @license MIT
3
+ * Copyright (c) 2026-present AetherFramework Contributors.
4
+ * SPDX-License-Identifier: MIT
5
+ * @module @aetherframework/middleware/core/AetherStore
6
+ */
7
+
8
+ import { EventEmitter } from 'events';
9
+
10
+ /**
11
+ * Memory storage backend with LRU cache
12
+ */
13
+ class MemoryStore extends EventEmitter {
14
+ constructor(options = {}) {
15
+ super();
16
+ this.maxSize = options.maxSize || 10000;
17
+ this.ttl = options.ttl || 3600000; // 1 hour
18
+ this.store = new Map();
19
+ this.lru = []; // List of keys in access order
20
+ this.stats = {
21
+ hits: 0,
22
+ misses: 0,
23
+ sets: 0,
24
+ deletes: 0,
25
+ size: 0
26
+ };
27
+
28
+ // Start cleanup interval
29
+ this.cleanupInterval = setInterval(() => this._cleanup(), 60000).unref();
30
+ }
31
+
32
+ async get(key) {
33
+ const entry = this.store.get(key);
34
+
35
+ if (!entry) {
36
+ this.stats.misses++;
37
+ return null;
38
+ }
39
+
40
+ // Check if expired
41
+ if (entry.expires && Date.now() > entry.expires) {
42
+ this.store.delete(key);
43
+ this._removeFromLRU(key);
44
+ this.stats.misses++;
45
+ return null;
46
+ }
47
+
48
+ // Update LRU
49
+ this._updateLRU(key);
50
+ this.stats.hits++;
51
+
52
+ return entry.value;
53
+ }
54
+
55
+ async set(key, value, ttl = this.ttl) {
56
+ // If key exists, update LRU
57
+ if (this.store.has(key)) {
58
+ this._updateLRU(key);
59
+ } else {
60
+ // Check capacity
61
+ if (this.store.size >= this.maxSize) {
62
+ this._evict();
63
+ }
64
+ this.lru.push(key);
65
+ }
66
+
67
+ const expires = ttl ? Date.now() + ttl : null;
68
+
69
+ this.store.set(key, {
70
+ value,
71
+ expires,
72
+ createdAt: Date.now(),
73
+ accessedAt: Date.now()
74
+ });
75
+
76
+ this.stats.sets++;
77
+ this.stats.size = this.store.size;
78
+
79
+ this.emit('set', { key, value });
80
+ }
81
+
82
+ async delete(key) {
83
+ const deleted = this.store.delete(key);
84
+ if (deleted) {
85
+ this._removeFromLRU(key);
86
+ this.stats.deletes++;
87
+ this.stats.size = this.store.size;
88
+ this.emit('delete', { key });
89
+ }
90
+ return deleted;
91
+ }
92
+
93
+ async clear() {
94
+ this.store.clear();
95
+ this.lru = [];
96
+ this.stats = { hits: 0, misses: 0, sets: 0, deletes: 0, size: 0 };
97
+ this.emit('clear');
98
+ }
99
+
100
+ async has(key) {
101
+ const entry = this.store.get(key);
102
+ if (!entry) return false;
103
+
104
+ if (entry.expires && Date.now() > entry.expires) {
105
+ this.store.delete(key);
106
+ this._removeFromLRU(key);
107
+ return false;
108
+ }
109
+
110
+ this._updateLRU(key);
111
+ return true;
112
+ }
113
+
114
+ async keys() {
115
+ return Array.from(this.store.keys());
116
+ }
117
+
118
+ async size() {
119
+ return this.store.size;
120
+ }
121
+
122
+ /**
123
+ * Update LRU order
124
+ * @param {string} key
125
+ */
126
+ _updateLRU(key) {
127
+ const index = this.lru.indexOf(key);
128
+ if (index > -1) {
129
+ this.lru.splice(index, 1);
130
+ }
131
+ this.lru.push(key);
132
+
133
+ // Update accessed time
134
+ const entry = this.store.get(key);
135
+ if (entry) {
136
+ entry.accessedAt = Date.now();
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Remove key from LRU list
142
+ * @param {string} key
143
+ */
144
+ _removeFromLRU(key) {
145
+ const index = this.lru.indexOf(key);
146
+ if (index > -1) {
147
+ this.lru.splice(index, 1);
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Evict least recently used item
153
+ */
154
+ _evict() {
155
+ if (this.lru.length === 0) return;
156
+
157
+ const oldestKey = this.lru.shift();
158
+ this.store.delete(oldestKey);
159
+ this.stats.size = this.store.size;
160
+ this.emit('evict', { key: oldestKey });
161
+ }
162
+
163
+ /**
164
+ * Cleanup expired items
165
+ */
166
+ _cleanup() {
167
+ const now = Date.now();
168
+ const keysToDelete = [];
169
+
170
+ for (const [key, entry] of this.store.entries()) {
171
+ if (entry.expires && now > entry.expires) {
172
+ keysToDelete.push(key);
173
+ }
174
+ }
175
+
176
+ for (const key of keysToDelete) {
177
+ this.store.delete(key);
178
+ this._removeFromLRU(key);
179
+ }
180
+
181
+ if (keysToDelete.length > 0) {
182
+ this.stats.size = this.store.size;
183
+ this.emit('cleanup', { count: keysToDelete.length });
184
+ }
185
+ }
186
+
187
+ destroy() {
188
+ clearInterval(this.cleanupInterval);
189
+ this.clear();
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Factory function to create store instances
195
+ * @param {Object} options - Store configuration
196
+ * @returns {MemoryStore} - Store instance
197
+ */
198
+ function createAetherStore(options = {}) {
199
+ // In a full implementation, this would switch between Memory, Redis, etc.
200
+ // For now, we return the high-performance MemoryStore
201
+ return new MemoryStore(options);
202
+ }
203
+
204
+ export default createAetherStore;