navis.js 3.1.0 → 5.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.
@@ -0,0 +1,157 @@
1
+ /**
2
+ * In-Memory Cache
3
+ * v5: Simple in-memory caching with TTL support
4
+ */
5
+
6
+ class Cache {
7
+ constructor(options = {}) {
8
+ this.store = new Map();
9
+ this.maxSize = options.maxSize || 1000;
10
+ this.defaultTTL = options.defaultTTL || 3600000; // 1 hour in ms
11
+ this.cleanupInterval = options.cleanupInterval || 60000; // 1 minute
12
+
13
+ // Start cleanup interval
14
+ this.intervalId = setInterval(() => {
15
+ this._cleanup();
16
+ }, this.cleanupInterval);
17
+ }
18
+
19
+ /**
20
+ * Set a value in cache
21
+ * @param {string} key - Cache key
22
+ * @param {*} value - Value to cache
23
+ * @param {number} ttl - Time to live in milliseconds
24
+ */
25
+ set(key, value, ttl = null) {
26
+ const expiration = Date.now() + (ttl || this.defaultTTL);
27
+
28
+ // Remove oldest entries if at max size
29
+ if (this.store.size >= this.maxSize && !this.store.has(key)) {
30
+ this._evictOldest();
31
+ }
32
+
33
+ this.store.set(key, {
34
+ value,
35
+ expiration,
36
+ });
37
+ }
38
+
39
+ /**
40
+ * Get a value from cache
41
+ * @param {string} key - Cache key
42
+ * @returns {*} - Cached value or null
43
+ */
44
+ get(key) {
45
+ const entry = this.store.get(key);
46
+
47
+ if (!entry) {
48
+ return null;
49
+ }
50
+
51
+ // Check if expired
52
+ if (Date.now() > entry.expiration) {
53
+ this.store.delete(key);
54
+ return null;
55
+ }
56
+
57
+ return entry.value;
58
+ }
59
+
60
+ /**
61
+ * Check if key exists in cache
62
+ * @param {string} key - Cache key
63
+ * @returns {boolean} - True if key exists and not expired
64
+ */
65
+ has(key) {
66
+ const entry = this.store.get(key);
67
+
68
+ if (!entry) {
69
+ return false;
70
+ }
71
+
72
+ if (Date.now() > entry.expiration) {
73
+ this.store.delete(key);
74
+ return false;
75
+ }
76
+
77
+ return true;
78
+ }
79
+
80
+ /**
81
+ * Delete a key from cache
82
+ * @param {string} key - Cache key
83
+ * @returns {boolean} - True if key was deleted
84
+ */
85
+ delete(key) {
86
+ return this.store.delete(key);
87
+ }
88
+
89
+ /**
90
+ * Clear all cache entries
91
+ */
92
+ clear() {
93
+ this.store.clear();
94
+ }
95
+
96
+ /**
97
+ * Get cache size
98
+ * @returns {number} - Number of entries
99
+ */
100
+ size() {
101
+ return this.store.size;
102
+ }
103
+
104
+ /**
105
+ * Get all keys
106
+ * @returns {Array} - Array of cache keys
107
+ */
108
+ keys() {
109
+ return Array.from(this.store.keys());
110
+ }
111
+
112
+ /**
113
+ * Cleanup expired entries
114
+ * @private
115
+ */
116
+ _cleanup() {
117
+ const now = Date.now();
118
+ for (const [key, entry] of this.store.entries()) {
119
+ if (now > entry.expiration) {
120
+ this.store.delete(key);
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Evict oldest entry (LRU-like)
127
+ * @private
128
+ */
129
+ _evictOldest() {
130
+ let oldestKey = null;
131
+ let oldestExpiration = Infinity;
132
+
133
+ for (const [key, entry] of this.store.entries()) {
134
+ if (entry.expiration < oldestExpiration) {
135
+ oldestExpiration = entry.expiration;
136
+ oldestKey = key;
137
+ }
138
+ }
139
+
140
+ if (oldestKey) {
141
+ this.store.delete(oldestKey);
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Destroy cache (cleanup)
147
+ */
148
+ destroy() {
149
+ if (this.intervalId) {
150
+ clearInterval(this.intervalId);
151
+ }
152
+ this.clear();
153
+ }
154
+ }
155
+
156
+ module.exports = Cache;
157
+
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Redis Cache Adapter
3
+ * v5: Redis-based caching (requires redis package)
4
+ */
5
+
6
+ class RedisCache {
7
+ constructor(options = {}) {
8
+ this.client = null;
9
+ this.defaultTTL = options.defaultTTL || 3600; // 1 hour in seconds
10
+ this.prefix = options.prefix || 'navis:';
11
+ this.connected = false;
12
+
13
+ // Try to require redis (optional dependency)
14
+ try {
15
+ const redis = require('redis');
16
+ this.redis = redis;
17
+ } catch (error) {
18
+ throw new Error('Redis package not installed. Install with: npm install redis');
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Connect to Redis
24
+ * @param {Object} options - Redis connection options
25
+ */
26
+ async connect(options = {}) {
27
+ if (this.connected && this.client) {
28
+ return;
29
+ }
30
+
31
+ const {
32
+ url = process.env.REDIS_URL || 'redis://localhost:6379',
33
+ socket = {},
34
+ ...otherOptions
35
+ } = options;
36
+
37
+ this.client = this.redis.createClient({
38
+ url,
39
+ socket: {
40
+ reconnectStrategy: (retries) => {
41
+ if (retries > 10) {
42
+ return new Error('Too many reconnection attempts');
43
+ }
44
+ return Math.min(retries * 100, 3000);
45
+ },
46
+ ...socket,
47
+ },
48
+ ...otherOptions,
49
+ });
50
+
51
+ this.client.on('error', (err) => {
52
+ console.error('Redis Client Error:', err);
53
+ });
54
+
55
+ await this.client.connect();
56
+ this.connected = true;
57
+ }
58
+
59
+ /**
60
+ * Disconnect from Redis
61
+ */
62
+ async disconnect() {
63
+ if (this.client && this.connected) {
64
+ await this.client.quit();
65
+ this.connected = false;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Set a value in cache
71
+ * @param {string} key - Cache key
72
+ * @param {*} value - Value to cache
73
+ * @param {number} ttl - Time to live in seconds
74
+ */
75
+ async set(key, value, ttl = null) {
76
+ if (!this.connected) {
77
+ throw new Error('Redis not connected. Call connect() first.');
78
+ }
79
+
80
+ const fullKey = this.prefix + key;
81
+ const serialized = JSON.stringify(value);
82
+ const expiration = ttl || this.defaultTTL;
83
+
84
+ if (expiration > 0) {
85
+ await this.client.setEx(fullKey, expiration, serialized);
86
+ } else {
87
+ await this.client.set(fullKey, serialized);
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Get a value from cache
93
+ * @param {string} key - Cache key
94
+ * @returns {*} - Cached value or null
95
+ */
96
+ async get(key) {
97
+ if (!this.connected) {
98
+ throw new Error('Redis not connected. Call connect() first.');
99
+ }
100
+
101
+ const fullKey = this.prefix + key;
102
+ const serialized = await this.client.get(fullKey);
103
+
104
+ if (!serialized) {
105
+ return null;
106
+ }
107
+
108
+ try {
109
+ return JSON.parse(serialized);
110
+ } catch (error) {
111
+ return null;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Check if key exists in cache
117
+ * @param {string} key - Cache key
118
+ * @returns {boolean} - True if key exists
119
+ */
120
+ async has(key) {
121
+ if (!this.connected) {
122
+ return false;
123
+ }
124
+
125
+ const fullKey = this.prefix + key;
126
+ const exists = await this.client.exists(fullKey);
127
+ return exists === 1;
128
+ }
129
+
130
+ /**
131
+ * Delete a key from cache
132
+ * @param {string} key - Cache key
133
+ * @returns {boolean} - True if key was deleted
134
+ */
135
+ async delete(key) {
136
+ if (!this.connected) {
137
+ return false;
138
+ }
139
+
140
+ const fullKey = this.prefix + key;
141
+ const result = await this.client.del(fullKey);
142
+ return result > 0;
143
+ }
144
+
145
+ /**
146
+ * Clear all cache entries with prefix
147
+ */
148
+ async clear() {
149
+ if (!this.connected) {
150
+ return;
151
+ }
152
+
153
+ const keys = await this.client.keys(this.prefix + '*');
154
+ if (keys.length > 0) {
155
+ await this.client.del(keys);
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Get cache size (approximate)
161
+ * @returns {number} - Number of keys with prefix
162
+ */
163
+ async size() {
164
+ if (!this.connected) {
165
+ return 0;
166
+ }
167
+
168
+ const keys = await this.client.keys(this.prefix + '*');
169
+ return keys.length;
170
+ }
171
+ }
172
+
173
+ module.exports = RedisCache;
174
+
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Advanced Router with Parameters and Path Matching
3
+ * v4: Support for route parameters, wildcards, and path matching
4
+ */
5
+
6
+ class AdvancedRouter {
7
+ constructor() {
8
+ this.routes = {
9
+ GET: [],
10
+ POST: [],
11
+ PUT: [],
12
+ DELETE: [],
13
+ PATCH: [],
14
+ };
15
+ }
16
+
17
+ /**
18
+ * Register a route handler
19
+ * @param {string} method - HTTP method
20
+ * @param {string} path - Route path (supports :param and * wildcards)
21
+ * @param {Function} handler - Route handler function
22
+ */
23
+ register(method, path, handler) {
24
+ const normalizedMethod = method.toUpperCase();
25
+ if (!this.routes[normalizedMethod]) {
26
+ throw new Error(`Unsupported HTTP method: ${method}`);
27
+ }
28
+
29
+ // Parse route pattern
30
+ const pattern = this._parsePattern(path);
31
+
32
+ this.routes[normalizedMethod].push({
33
+ pattern: path,
34
+ regex: pattern.regex,
35
+ params: pattern.params,
36
+ handler,
37
+ });
38
+
39
+ // Sort routes by specificity (more specific first)
40
+ this.routes[normalizedMethod].sort((a, b) => {
41
+ const aSpecificity = this._calculateSpecificity(a.pattern);
42
+ const bSpecificity = this._calculateSpecificity(b.pattern);
43
+ return bSpecificity - aSpecificity;
44
+ });
45
+ }
46
+
47
+ /**
48
+ * Find route handler for a method and path
49
+ * @param {string} method - HTTP method
50
+ * @param {string} path - Request path
51
+ * @returns {Object|null} - { handler, params } or null if not found
52
+ */
53
+ find(method, path) {
54
+ const normalizedMethod = method.toUpperCase();
55
+ const methodRoutes = this.routes[normalizedMethod] || [];
56
+
57
+ for (const route of methodRoutes) {
58
+ const match = path.match(route.regex);
59
+ if (match) {
60
+ // Extract parameters
61
+ const params = {};
62
+ route.params.forEach((param, index) => {
63
+ params[param] = match[index + 1];
64
+ });
65
+
66
+ return {
67
+ handler: route.handler,
68
+ params,
69
+ };
70
+ }
71
+ }
72
+
73
+ return null;
74
+ }
75
+
76
+ /**
77
+ * Parse route pattern into regex and parameter names
78
+ * @private
79
+ */
80
+ _parsePattern(pattern) {
81
+ const params = [];
82
+ const parts = pattern.split('/').filter(p => p !== ''); // Remove empty parts
83
+ const regexParts = [];
84
+
85
+ for (const part of parts) {
86
+ if (part.startsWith(':')) {
87
+ // Parameter
88
+ const paramName = part.substring(1);
89
+ params.push(paramName);
90
+ regexParts.push('([^/]+)');
91
+ } else if (part === '*') {
92
+ // Wildcard
93
+ regexParts.push('.*');
94
+ } else {
95
+ // Literal - escape regex special chars
96
+ regexParts.push(part.replace(/[.+?^${}()|[\]\\]/g, '\\$&'));
97
+ }
98
+ }
99
+
100
+ // Build regex pattern
101
+ const regexPattern = '^/' + regexParts.join('/') + '/?$';
102
+
103
+ return {
104
+ regex: new RegExp(regexPattern),
105
+ params,
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Calculate route specificity (higher = more specific)
111
+ * @private
112
+ */
113
+ _calculateSpecificity(pattern) {
114
+ let specificity = 0;
115
+
116
+ // Count static segments
117
+ const staticSegments = pattern.split('/').filter(seg =>
118
+ seg && !seg.startsWith(':') && seg !== '*'
119
+ );
120
+ specificity += staticSegments.length * 10;
121
+
122
+ // Count parameters (less specific)
123
+ const params = (pattern.match(/:[^/]+/g) || []).length;
124
+ specificity += params * 5;
125
+
126
+ // Wildcards are least specific
127
+ if (pattern.includes('*')) {
128
+ specificity -= 10;
129
+ }
130
+
131
+ return specificity;
132
+ }
133
+
134
+ /**
135
+ * Get all registered routes
136
+ * @returns {Object} - All routes grouped by method
137
+ */
138
+ getAllRoutes() {
139
+ const allRoutes = {};
140
+ for (const method in this.routes) {
141
+ allRoutes[method] = this.routes[method].map(route => ({
142
+ pattern: route.pattern,
143
+ params: route.params,
144
+ }));
145
+ }
146
+ return allRoutes;
147
+ }
148
+
149
+ /**
150
+ * Register GET route
151
+ */
152
+ get(path, handler) {
153
+ this.register('GET', path, handler);
154
+ }
155
+
156
+ /**
157
+ * Register POST route
158
+ */
159
+ post(path, handler) {
160
+ this.register('POST', path, handler);
161
+ }
162
+
163
+ /**
164
+ * Register PUT route
165
+ */
166
+ put(path, handler) {
167
+ this.register('PUT', path, handler);
168
+ }
169
+
170
+ /**
171
+ * Register DELETE route
172
+ */
173
+ delete(path, handler) {
174
+ this.register('DELETE', path, handler);
175
+ }
176
+
177
+ /**
178
+ * Register PATCH route
179
+ */
180
+ patch(path, handler) {
181
+ this.register('PATCH', path, handler);
182
+ }
183
+ }
184
+
185
+ module.exports = AdvancedRouter;
186
+