navis.js 4.0.0 → 5.2.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
+
package/src/core/app.js CHANGED
@@ -238,6 +238,7 @@ class NavisApp {
238
238
  * Start HTTP server (Node.js)
239
239
  * @param {number} port - Port number
240
240
  * @param {Function} callback - Optional callback
241
+ * @returns {Object} - HTTP server instance
241
242
  */
242
243
  listen(port = 3000, callback) {
243
244
  this.server = http.createServer((req, res) => {
@@ -251,6 +252,14 @@ class NavisApp {
251
252
 
252
253
  return this.server;
253
254
  }
255
+
256
+ /**
257
+ * Get server instance
258
+ * @returns {Object|null} - HTTP server instance
259
+ */
260
+ getServer() {
261
+ return this.server;
262
+ }
254
263
  }
255
264
 
256
265
  module.exports = NavisApp;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Graceful Shutdown Handler
3
+ * v5: Clean shutdown handling for Node.js servers
4
+ */
5
+
6
+ /**
7
+ * Graceful shutdown handler
8
+ * @param {Object} server - HTTP server instance
9
+ * @param {Object} options - Shutdown options
10
+ */
11
+ function gracefulShutdown(server, options = {}) {
12
+ const {
13
+ timeout = 10000, // 10 seconds default
14
+ onShutdown = async () => {}, // Cleanup function
15
+ signals = ['SIGTERM', 'SIGINT'],
16
+ log = console.log,
17
+ } = options;
18
+
19
+ let isShuttingDown = false;
20
+
21
+ const shutdown = async (signal) => {
22
+ if (isShuttingDown) {
23
+ return;
24
+ }
25
+
26
+ isShuttingDown = true;
27
+ log(`Received ${signal}, starting graceful shutdown...`);
28
+
29
+ // Stop accepting new connections
30
+ server.close(() => {
31
+ log('HTTP server closed');
32
+ });
33
+
34
+ // Set timeout for forced shutdown
35
+ const shutdownTimer = setTimeout(() => {
36
+ log('Forced shutdown after timeout');
37
+ process.exit(1);
38
+ }, timeout);
39
+
40
+ try {
41
+ // Run cleanup
42
+ await onShutdown();
43
+ clearTimeout(shutdownTimer);
44
+ log('Graceful shutdown completed');
45
+ process.exit(0);
46
+ } catch (error) {
47
+ clearTimeout(shutdownTimer);
48
+ log('Error during shutdown:', error);
49
+ process.exit(1);
50
+ }
51
+ };
52
+
53
+ // Register signal handlers
54
+ for (const signal of signals) {
55
+ process.on(signal, () => shutdown(signal));
56
+ }
57
+
58
+ // Handle uncaught exceptions
59
+ process.on('uncaughtException', async (error) => {
60
+ log('Uncaught exception:', error);
61
+ await shutdown('uncaughtException');
62
+ });
63
+
64
+ // Handle unhandled promise rejections
65
+ process.on('unhandledRejection', async (reason, promise) => {
66
+ log('Unhandled rejection:', reason);
67
+ await shutdown('unhandledRejection');
68
+ });
69
+
70
+ return {
71
+ shutdown: () => shutdown('manual'),
72
+ isShuttingDown: () => isShuttingDown,
73
+ };
74
+ }
75
+
76
+ module.exports = gracefulShutdown;
77
+
@@ -0,0 +1,124 @@
1
+ /**
2
+ * API Versioning
3
+ * v5.1: URL-based and header-based API versioning
4
+ */
5
+
6
+ class VersionManager {
7
+ constructor() {
8
+ this.versions = new Map();
9
+ this.defaultVersion = null;
10
+ }
11
+
12
+ /**
13
+ * Create a version router
14
+ * @param {string} version - Version identifier (e.g., 'v1', 'v2')
15
+ * @returns {Object} - Version router with route methods
16
+ */
17
+ version(version) {
18
+ if (!this.versions.has(version)) {
19
+ const router = {
20
+ version,
21
+ routes: {
22
+ GET: [],
23
+ POST: [],
24
+ PUT: [],
25
+ DELETE: [],
26
+ PATCH: [],
27
+ },
28
+ };
29
+ this.versions.set(version, router);
30
+ }
31
+
32
+ const router = this.versions.get(version);
33
+
34
+ return {
35
+ get: (path, handler) => this._register(router, 'GET', path, handler),
36
+ post: (path, handler) => this._register(router, 'POST', path, handler),
37
+ put: (path, handler) => this._register(router, 'PUT', path, handler),
38
+ delete: (path, handler) => this._register(router, 'DELETE', path, handler),
39
+ patch: (path, handler) => this._register(router, 'PATCH', path, handler),
40
+ use: (middleware) => {
41
+ if (!router.middlewares) {
42
+ router.middlewares = [];
43
+ }
44
+ router.middlewares.push(middleware);
45
+ },
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Register a route for a version
51
+ * @private
52
+ */
53
+ _register(router, method, path, handler) {
54
+ const versionedPath = `/${router.version}${path}`;
55
+ router.routes[method].push({
56
+ path: versionedPath,
57
+ originalPath: path,
58
+ handler,
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Set default version
64
+ * @param {string} version - Default version
65
+ */
66
+ setDefaultVersion(version) {
67
+ this.defaultVersion = version;
68
+ }
69
+
70
+ /**
71
+ * Get routes for a version
72
+ * @param {string} version - Version identifier
73
+ * @returns {Object} - Routes for the version
74
+ */
75
+ getRoutes(version) {
76
+ const router = this.versions.get(version);
77
+ return router ? router.routes : null;
78
+ }
79
+
80
+ /**
81
+ * Get all versions
82
+ * @returns {Array} - Array of version identifiers
83
+ */
84
+ getVersions() {
85
+ return Array.from(this.versions.keys());
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Create version manager
91
+ * @returns {VersionManager} - Version manager instance
92
+ */
93
+ function createVersionManager() {
94
+ return new VersionManager();
95
+ }
96
+
97
+ /**
98
+ * Header-based versioning middleware
99
+ * @param {Object} options - Versioning options
100
+ * @returns {Function} - Middleware function
101
+ */
102
+ function headerVersioning(options = {}) {
103
+ const {
104
+ header = 'X-API-Version',
105
+ defaultVersion = null,
106
+ } = options;
107
+
108
+ return async (req, res, next) => {
109
+ const version = req.headers[header] || req.headers[header.toLowerCase()] || defaultVersion;
110
+
111
+ if (version) {
112
+ req.apiVersion = version;
113
+ }
114
+
115
+ next();
116
+ };
117
+ }
118
+
119
+ module.exports = {
120
+ VersionManager,
121
+ createVersionManager,
122
+ headerVersioning,
123
+ };
124
+