te.js 2.2.0 → 2.2.1
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.
- package/README.md +1 -12
- package/docs/README.md +1 -2
- package/docs/api-reference.md +124 -186
- package/docs/configuration.md +0 -13
- package/docs/getting-started.md +19 -21
- package/docs/rate-limiting.md +59 -58
- package/lib/llm/client.js +1 -1
- package/package.json +1 -1
- package/radar/index.js +191 -90
- package/rate-limit/base.js +12 -15
- package/rate-limit/index.js +12 -12
- package/rate-limit/index.test.js +45 -16
- package/rate-limit/storage/memory.js +13 -13
- package/rate-limit/storage/redis-install.js +70 -0
- package/rate-limit/storage/redis.js +94 -52
- package/server/ammo.js +3 -2
- package/te.js +64 -143
- package/utils/errors-llm-config.js +63 -0
- package/utils/startup.js +80 -0
- package/database/index.js +0 -167
- package/database/mongodb.js +0 -152
- package/database/redis.js +0 -210
- package/docs/database.md +0 -390
package/rate-limit/base.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import TejError from '../server/error.js';
|
|
2
2
|
import MemoryStorage from './storage/memory.js';
|
|
3
3
|
import RedisStorage from './storage/redis.js';
|
|
4
|
-
import dbManager from '../database/index.js';
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Base rate limiter class implementing common functionality for rate limiting algorithms
|
|
@@ -15,11 +14,9 @@ import dbManager from '../database/index.js';
|
|
|
15
14
|
* object is provided (tokenBucketConfig, slidingWindowConfig, or fixedWindowConfig).
|
|
16
15
|
*
|
|
17
16
|
* @example
|
|
18
|
-
* // Using with Redis storage and token bucket algorithm
|
|
19
17
|
* const limiter = new TokenBucketRateLimiter({
|
|
20
18
|
* maxRequests: 10,
|
|
21
19
|
* timeWindowSeconds: 60,
|
|
22
|
-
* store: 'redis',
|
|
23
20
|
* tokenBucketConfig: {
|
|
24
21
|
* refillRate: 0.5,
|
|
25
22
|
* burstSize: 15
|
|
@@ -40,7 +37,9 @@ class RateLimiter {
|
|
|
40
37
|
* For token bucket, this affects the default refill rate calculation.
|
|
41
38
|
* @param {string} [options.keyPrefix='rl:'] - Prefix for storage keys. Useful when implementing different rate limit
|
|
42
39
|
* rules with different prefixes (e.g., 'rl:api:', 'rl:web:').
|
|
43
|
-
* @param {string} [options.store='memory'] - Storage backend
|
|
40
|
+
* @param {string|Object} [options.store='memory'] - Storage backend: 'memory' (default) or
|
|
41
|
+
* { type: 'redis', url: 'redis://...', ...redisOptions }.
|
|
42
|
+
* In-memory storage is not shared across processes; use Redis for distributed deployments.
|
|
44
43
|
* @param {Object} [options.tokenBucketConfig] - Token bucket algorithm specific options
|
|
45
44
|
* @param {Object} [options.slidingWindowConfig] - Sliding window algorithm specific options
|
|
46
45
|
* @param {Object} [options.fixedWindowConfig] - Fixed window algorithm specific options
|
|
@@ -101,18 +100,16 @@ class RateLimiter {
|
|
|
101
100
|
}
|
|
102
101
|
: null;
|
|
103
102
|
|
|
104
|
-
|
|
105
|
-
if (
|
|
106
|
-
if (!dbManager.hasConnection('redis')) {
|
|
107
|
-
throw new TejError(
|
|
108
|
-
500,
|
|
109
|
-
'Redis store selected but no Redis connection available. Call withRedis() first.',
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
const redisClient = dbManager.getConnection('redis');
|
|
113
|
-
this.storage = new RedisStorage(redisClient);
|
|
114
|
-
} else {
|
|
103
|
+
const store = this.options.store;
|
|
104
|
+
if (!store || store === 'memory') {
|
|
115
105
|
this.storage = new MemoryStorage();
|
|
106
|
+
} else if (typeof store === 'object' && store.type === 'redis') {
|
|
107
|
+
this.storage = new RedisStorage(store);
|
|
108
|
+
} else {
|
|
109
|
+
throw new TejError(
|
|
110
|
+
400,
|
|
111
|
+
`Invalid store config. Use 'memory' or { type: 'redis', url: '...' }.`,
|
|
112
|
+
);
|
|
116
113
|
}
|
|
117
114
|
}
|
|
118
115
|
|
package/rate-limit/index.js
CHANGED
|
@@ -2,7 +2,6 @@ import TejError from '../server/error.js';
|
|
|
2
2
|
import FixedWindowRateLimiter from './algorithms/fixed-window.js';
|
|
3
3
|
import SlidingWindowRateLimiter from './algorithms/sliding-window.js';
|
|
4
4
|
import TokenBucketRateLimiter from './algorithms/token-bucket.js';
|
|
5
|
-
import dbManager from '../database/index.js';
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* Creates a rate limiting middleware function with the specified algorithm and storage
|
|
@@ -14,9 +13,11 @@ import dbManager from '../database/index.js';
|
|
|
14
13
|
* - 'token-bucket': Best for handling traffic bursts
|
|
15
14
|
* - 'sliding-window': Best for smooth rate limiting
|
|
16
15
|
* - 'fixed-window': Simplest approach
|
|
17
|
-
* @param {string} [options.store='memory'] - Storage backend to use:
|
|
18
|
-
* - 'memory': In-memory storage (default)
|
|
19
|
-
* - 'redis': Redis
|
|
16
|
+
* @param {string|Object} [options.store='memory'] - Storage backend to use:
|
|
17
|
+
* - 'memory': In-memory storage (default, single-instance only)
|
|
18
|
+
* - { type: 'redis', url: 'redis://...' }: Redis storage for distributed deployments.
|
|
19
|
+
* The redis npm package is auto-installed on first use.
|
|
20
|
+
* Any extra properties are forwarded to node-redis createClient.
|
|
20
21
|
* @param {Object} [options.algorithmOptions] - Algorithm-specific options
|
|
21
22
|
* @param {Function} [options.keyGenerator] - Optional function to generate unique identifiers
|
|
22
23
|
* @param {Object} [options.headerFormat] - Rate limit header format configuration
|
|
@@ -39,14 +40,6 @@ function rateLimiter(options) {
|
|
|
39
40
|
...limiterOptions
|
|
40
41
|
} = options;
|
|
41
42
|
|
|
42
|
-
// Check Redis connectivity if Redis store is selected
|
|
43
|
-
if (store === 'redis' && !dbManager.hasConnection('redis', {})) {
|
|
44
|
-
throw new TejError(
|
|
45
|
-
400,
|
|
46
|
-
'Redis store selected but no Redis connection found. Please use withRedis() before using withRateLimit()',
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
43
|
const configMap = Object.create(null);
|
|
51
44
|
configMap['token-bucket'] = 'tokenBucketConfig';
|
|
52
45
|
configMap['sliding-window'] = 'slidingWindowConfig';
|
|
@@ -60,6 +53,13 @@ function rateLimiter(options) {
|
|
|
60
53
|
);
|
|
61
54
|
}
|
|
62
55
|
|
|
56
|
+
if (typeof store === 'object' && store.type === 'redis' && !store.url) {
|
|
57
|
+
throw new TejError(
|
|
58
|
+
400,
|
|
59
|
+
`Redis store requires a url. Provide store: { type: "redis", url: "redis://..." }`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
63
|
const limiterConfig = Object.freeze({
|
|
64
64
|
maxRequests: limiterOptions.maxRequests,
|
|
65
65
|
timeWindowSeconds: limiterOptions.timeWindowSeconds,
|
package/rate-limit/index.test.js
CHANGED
|
@@ -4,16 +4,7 @@
|
|
|
4
4
|
import { describe, it, expect, vi } from 'vitest';
|
|
5
5
|
import rateLimiter from './index.js';
|
|
6
6
|
|
|
7
|
-
// Mock dbManager.hasConnection so we can test without a real DB
|
|
8
|
-
vi.mock('../database/index.js', () => ({
|
|
9
|
-
default: {
|
|
10
|
-
hasConnection: vi.fn(() => false),
|
|
11
|
-
initializeConnection: vi.fn(),
|
|
12
|
-
},
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
7
|
function makeAmmo(ip = '127.0.0.1') {
|
|
16
|
-
const headers = {};
|
|
17
8
|
return {
|
|
18
9
|
ip,
|
|
19
10
|
res: {
|
|
@@ -24,13 +15,6 @@ function makeAmmo(ip = '127.0.0.1') {
|
|
|
24
15
|
}
|
|
25
16
|
|
|
26
17
|
describe('rateLimiter', () => {
|
|
27
|
-
it('should throw TejError when redis store selected but no connection', async () => {
|
|
28
|
-
const TejError = (await import('../server/error.js')).default;
|
|
29
|
-
expect(() =>
|
|
30
|
-
rateLimiter({ maxRequests: 10, timeWindowSeconds: 60, store: 'redis' }),
|
|
31
|
-
).toThrow();
|
|
32
|
-
});
|
|
33
|
-
|
|
34
18
|
it('should throw on invalid algorithm', () => {
|
|
35
19
|
expect(() =>
|
|
36
20
|
rateLimiter({
|
|
@@ -61,4 +45,49 @@ describe('rateLimiter', () => {
|
|
|
61
45
|
await mw(ammo, next);
|
|
62
46
|
expect(next).toHaveBeenCalled();
|
|
63
47
|
});
|
|
48
|
+
|
|
49
|
+
it('should throw on invalid store config', () => {
|
|
50
|
+
expect(() =>
|
|
51
|
+
rateLimiter({
|
|
52
|
+
maxRequests: 10,
|
|
53
|
+
timeWindowSeconds: 60,
|
|
54
|
+
store: 'postgres',
|
|
55
|
+
}),
|
|
56
|
+
).toThrow(/Invalid store config/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should throw when redis store has no url', () => {
|
|
60
|
+
expect(() =>
|
|
61
|
+
rateLimiter({
|
|
62
|
+
maxRequests: 10,
|
|
63
|
+
timeWindowSeconds: 60,
|
|
64
|
+
store: { type: 'redis' },
|
|
65
|
+
}),
|
|
66
|
+
).toThrow(/requires a url/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should accept redis store config without throwing on creation', () => {
|
|
70
|
+
vi.mock('./storage/redis.js', () => ({
|
|
71
|
+
default: class MockRedisStorage {
|
|
72
|
+
async get() {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
async set() {}
|
|
76
|
+
async increment() {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
async delete() {}
|
|
80
|
+
},
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
expect(() =>
|
|
84
|
+
rateLimiter({
|
|
85
|
+
maxRequests: 10,
|
|
86
|
+
timeWindowSeconds: 60,
|
|
87
|
+
store: { type: 'redis', url: 'redis://localhost:6379' },
|
|
88
|
+
}),
|
|
89
|
+
).not.toThrow();
|
|
90
|
+
|
|
91
|
+
vi.restoreAllMocks();
|
|
92
|
+
});
|
|
64
93
|
});
|
|
@@ -2,25 +2,25 @@ import RateLimitStorage from './base.js';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* In-memory storage implementation for rate limiting
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
6
|
* @extends RateLimitStorage
|
|
7
7
|
* @description
|
|
8
8
|
* This storage backend uses a JavaScript Map to store rate limit data in memory.
|
|
9
|
-
* It's suitable for single-instance applications or testing environments, but not
|
|
9
|
+
* It's suitable for single-instance applications or testing environments, but not
|
|
10
10
|
* recommended for production use in distributed systems as data is not shared
|
|
11
11
|
* between instances.
|
|
12
|
-
*
|
|
12
|
+
*
|
|
13
13
|
* Key features:
|
|
14
14
|
* - Fast access (all data in memory)
|
|
15
15
|
* - Automatic cleanup of expired entries
|
|
16
16
|
* - No external dependencies
|
|
17
17
|
* - Data is lost on process restart
|
|
18
18
|
* - Not suitable for distributed systems
|
|
19
|
-
*
|
|
19
|
+
*
|
|
20
20
|
* @example
|
|
21
21
|
* import { TokenBucketRateLimiter, MemoryStorage } from 'te.js/rate-limit';
|
|
22
|
-
*
|
|
23
|
-
* // Memory storage is
|
|
22
|
+
*
|
|
23
|
+
* // Memory storage is the default storage backend
|
|
24
24
|
* const limiter = new TokenBucketRateLimiter({
|
|
25
25
|
* maxRequests: 60,
|
|
26
26
|
* timeWindowSeconds: 60,
|
|
@@ -29,7 +29,7 @@ import RateLimitStorage from './base.js';
|
|
|
29
29
|
* burstSize: 60
|
|
30
30
|
* }
|
|
31
31
|
* });
|
|
32
|
-
*
|
|
32
|
+
*
|
|
33
33
|
* // Or create storage instance explicitly
|
|
34
34
|
* const storage = new MemoryStorage();
|
|
35
35
|
* await storage.set('key', { counter: 5 }, 60); // Store for 60 seconds
|
|
@@ -45,7 +45,7 @@ class MemoryStorage extends RateLimitStorage {
|
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* Get stored data for a key, handling expiration
|
|
48
|
-
*
|
|
48
|
+
*
|
|
49
49
|
* @param {string} key - The storage key to retrieve data for
|
|
50
50
|
* @returns {Promise<Object|null>} The stored data, or null if not found or expired
|
|
51
51
|
*/
|
|
@@ -61,7 +61,7 @@ class MemoryStorage extends RateLimitStorage {
|
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* Store data with expiration time
|
|
64
|
-
*
|
|
64
|
+
*
|
|
65
65
|
* @param {string} key - The storage key
|
|
66
66
|
* @param {Object} value - The data to store
|
|
67
67
|
* @param {number} ttl - Time-to-live in seconds
|
|
@@ -70,13 +70,13 @@ class MemoryStorage extends RateLimitStorage {
|
|
|
70
70
|
async set(key, value, ttl) {
|
|
71
71
|
this.store.set(key, {
|
|
72
72
|
value,
|
|
73
|
-
expireAt: Date.now() + ttl * 1000
|
|
73
|
+
expireAt: Date.now() + ttl * 1000,
|
|
74
74
|
});
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
78
|
* Increment a numeric value in storage
|
|
79
|
-
*
|
|
79
|
+
*
|
|
80
80
|
* @param {string} key - The storage key to increment
|
|
81
81
|
* @returns {Promise<number|null>} New value after increment, or null if key not found/expired
|
|
82
82
|
*/
|
|
@@ -90,7 +90,7 @@ class MemoryStorage extends RateLimitStorage {
|
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
92
|
* Delete data for a key
|
|
93
|
-
*
|
|
93
|
+
*
|
|
94
94
|
* @param {string} key - The storage key to delete
|
|
95
95
|
* @returns {Promise<void>}
|
|
96
96
|
*/
|
|
@@ -99,4 +99,4 @@ class MemoryStorage extends RateLimitStorage {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
export default MemoryStorage;
|
|
102
|
+
export default MemoryStorage;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import TejLogger from 'tej-logger';
|
|
5
|
+
|
|
6
|
+
const logger = new TejLogger('RedisAutoInstall');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Checks whether the `redis` npm package is available in the consuming
|
|
10
|
+
* project's package.json and node_modules.
|
|
11
|
+
*
|
|
12
|
+
* @returns {{ needsInstall: boolean, reason: string }}
|
|
13
|
+
*/
|
|
14
|
+
export function checkRedisInstallation() {
|
|
15
|
+
const cwd = process.cwd();
|
|
16
|
+
|
|
17
|
+
const pkgPath = join(cwd, 'package.json');
|
|
18
|
+
if (!existsSync(pkgPath)) {
|
|
19
|
+
return {
|
|
20
|
+
needsInstall: true,
|
|
21
|
+
reason: 'No package.json found in project root',
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let pkg;
|
|
26
|
+
try {
|
|
27
|
+
pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
28
|
+
} catch {
|
|
29
|
+
return { needsInstall: true, reason: 'Unable to read package.json' };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
33
|
+
const inPkgJson = 'redis' in deps;
|
|
34
|
+
|
|
35
|
+
const modulePath = join(cwd, 'node_modules', 'redis');
|
|
36
|
+
const inNodeModules = existsSync(modulePath);
|
|
37
|
+
|
|
38
|
+
if (inPkgJson && inNodeModules) {
|
|
39
|
+
return { needsInstall: false, reason: 'redis is already installed' };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (inPkgJson && !inNodeModules) {
|
|
43
|
+
return {
|
|
44
|
+
needsInstall: true,
|
|
45
|
+
reason: 'redis is in package.json but not installed',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { needsInstall: true, reason: 'redis is not in package.json' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Synchronously installs the `redis` npm package into the consuming project.
|
|
54
|
+
* Throws if the installation fails.
|
|
55
|
+
*/
|
|
56
|
+
export function installRedisSync() {
|
|
57
|
+
logger.info('Installing redis package …');
|
|
58
|
+
try {
|
|
59
|
+
execSync('npm install redis', {
|
|
60
|
+
cwd: process.cwd(),
|
|
61
|
+
stdio: 'pipe',
|
|
62
|
+
timeout: 60_000,
|
|
63
|
+
});
|
|
64
|
+
logger.info('redis package installed successfully.');
|
|
65
|
+
} catch (err) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Failed to auto-install redis package. Install it manually with: npm install redis\n${err.message}`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -1,87 +1,129 @@
|
|
|
1
1
|
import RateLimitStorage from './base.js';
|
|
2
|
+
import { checkRedisInstallation, installRedisSync } from './redis-install.js';
|
|
3
|
+
import TejLogger from 'tej-logger';
|
|
4
|
+
|
|
5
|
+
const logger = new TejLogger('RedisStorage');
|
|
2
6
|
|
|
3
7
|
/**
|
|
4
|
-
* Redis storage implementation for rate limiting
|
|
5
|
-
*
|
|
6
|
-
* @extends RateLimitStorage
|
|
7
|
-
* @description
|
|
8
|
-
* This storage backend uses Redis for distributed rate limiting across multiple application instances.
|
|
9
|
-
* It's the recommended storage backend for production use in distributed systems as it provides
|
|
10
|
-
* reliable rate limiting across all application instances.
|
|
8
|
+
* Redis-backed storage implementation for rate limiting.
|
|
11
9
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* - Atomic operations for race condition prevention
|
|
15
|
-
* - Automatic key expiration using Redis TTL
|
|
16
|
-
* - Persistence options available through Redis configuration
|
|
17
|
-
* - Clustering support for high availability
|
|
10
|
+
* Suitable for distributed / multi-instance deployments where rate-limit
|
|
11
|
+
* counters must be shared across processes.
|
|
18
12
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
13
|
+
* The `redis` npm package is auto-installed into the consuming project
|
|
14
|
+
* on first use if it is not already present.
|
|
21
15
|
*
|
|
22
|
-
*
|
|
23
|
-
* const limiter = new TokenBucketRateLimiter({
|
|
24
|
-
* maxRequests: 100,
|
|
25
|
-
* timeWindowSeconds: 60,
|
|
26
|
-
* store: 'redis', // Use Redis storage
|
|
27
|
-
* tokenBucketConfig: {
|
|
28
|
-
* refillRate: 2,
|
|
29
|
-
* burstSize: 100
|
|
30
|
-
* }
|
|
31
|
-
* });
|
|
16
|
+
* @extends RateLimitStorage
|
|
32
17
|
*/
|
|
33
18
|
class RedisStorage extends RateLimitStorage {
|
|
34
19
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
20
|
+
* @param {Object} config - Redis connection configuration
|
|
21
|
+
* @param {string} config.url - Redis connection URL (required)
|
|
22
|
+
* Remaining properties are forwarded to the node-redis `createClient` call.
|
|
38
23
|
*/
|
|
39
|
-
constructor(
|
|
24
|
+
constructor(config) {
|
|
40
25
|
super();
|
|
41
|
-
|
|
26
|
+
|
|
27
|
+
if (!config?.url) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
'RedisStorage requires a url. Provide store: { type: "redis", url: "redis://..." }',
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { type: _type, url, ...redisOptions } = config;
|
|
34
|
+
this._url = url;
|
|
35
|
+
this._redisOptions = redisOptions;
|
|
36
|
+
|
|
37
|
+
this._client = null;
|
|
38
|
+
this._connectPromise = this._init();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Bootstrap: ensure redis is installed, create client, connect. */
|
|
42
|
+
async _init() {
|
|
43
|
+
const { needsInstall } = checkRedisInstallation();
|
|
44
|
+
if (needsInstall) {
|
|
45
|
+
installRedisSync();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Dynamic import so the module is only resolved after auto-install
|
|
49
|
+
const { createClient } = await import('redis');
|
|
50
|
+
|
|
51
|
+
this._client = createClient({ url: this._url, ...this._redisOptions });
|
|
52
|
+
|
|
53
|
+
this._client.on('error', (err) => {
|
|
54
|
+
logger.error(`Redis client error: ${err.message}`);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await this._client.connect();
|
|
58
|
+
logger.info(`Connected to Redis at ${this._url}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Wait until the client is ready before any operation. */
|
|
62
|
+
async _ready() {
|
|
63
|
+
await this._connectPromise;
|
|
64
|
+
return this._client;
|
|
42
65
|
}
|
|
43
66
|
|
|
44
67
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* @param {string} key - The storage key to retrieve
|
|
48
|
-
* @returns {Promise<Object|null>} Stored value if found, null otherwise
|
|
68
|
+
* @param {string} key
|
|
69
|
+
* @returns {Promise<Object|null>}
|
|
49
70
|
*/
|
|
50
71
|
async get(key) {
|
|
51
|
-
const
|
|
52
|
-
|
|
72
|
+
const client = await this._ready();
|
|
73
|
+
const raw = await client.get(key);
|
|
74
|
+
if (raw === null) return null;
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(raw);
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
53
80
|
}
|
|
54
81
|
|
|
55
82
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* @param {
|
|
59
|
-
* @param {Object} value - The data to store
|
|
60
|
-
* @param {number} ttl - Time-to-live in seconds
|
|
61
|
-
* @returns {Promise<void>}
|
|
83
|
+
* @param {string} key
|
|
84
|
+
* @param {Object} value
|
|
85
|
+
* @param {number} ttl - seconds
|
|
62
86
|
*/
|
|
63
87
|
async set(key, value, ttl) {
|
|
64
|
-
await this.
|
|
88
|
+
const client = await this._ready();
|
|
89
|
+
const seconds = Math.max(1, Math.ceil(ttl));
|
|
90
|
+
await client.setEx(key, seconds, JSON.stringify(value));
|
|
65
91
|
}
|
|
66
92
|
|
|
67
93
|
/**
|
|
68
|
-
*
|
|
94
|
+
* Atomically increments the `counter` field inside the stored JSON value.
|
|
95
|
+
* Uses a Lua script to read-modify-write in a single round-trip.
|
|
69
96
|
*
|
|
70
|
-
* @param {string} key
|
|
71
|
-
* @returns {Promise<number>}
|
|
97
|
+
* @param {string} key
|
|
98
|
+
* @returns {Promise<number|null>}
|
|
72
99
|
*/
|
|
73
100
|
async increment(key) {
|
|
74
|
-
|
|
101
|
+
const client = await this._ready();
|
|
102
|
+
|
|
103
|
+
const script = `
|
|
104
|
+
local raw = redis.call('GET', KEYS[1])
|
|
105
|
+
if not raw then return nil end
|
|
106
|
+
local data = cjson.decode(raw)
|
|
107
|
+
data.counter = (data.counter or 0) + 1
|
|
108
|
+
local ttl = redis.call('TTL', KEYS[1])
|
|
109
|
+
if ttl > 0 then
|
|
110
|
+
redis.call('SETEX', KEYS[1], ttl, cjson.encode(data))
|
|
111
|
+
else
|
|
112
|
+
redis.call('SET', KEYS[1], cjson.encode(data))
|
|
113
|
+
end
|
|
114
|
+
return data.counter
|
|
115
|
+
`;
|
|
116
|
+
|
|
117
|
+
const result = await client.eval(script, { keys: [key] });
|
|
118
|
+
return result === null ? null : Number(result);
|
|
75
119
|
}
|
|
76
120
|
|
|
77
121
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* @param {string} key - The storage key to delete
|
|
81
|
-
* @returns {Promise<void>}
|
|
122
|
+
* @param {string} key
|
|
82
123
|
*/
|
|
83
124
|
async delete(key) {
|
|
84
|
-
await this.
|
|
125
|
+
const client = await this._ready();
|
|
126
|
+
await client.del(key);
|
|
85
127
|
}
|
|
86
128
|
}
|
|
87
129
|
|
package/server/ammo.js
CHANGED
|
@@ -436,11 +436,12 @@ class Ammo {
|
|
|
436
436
|
codeContext: null,
|
|
437
437
|
};
|
|
438
438
|
|
|
439
|
-
//
|
|
439
|
+
// Run LLM in the background; expose the promise so the Radar middleware
|
|
440
|
+
// can await it before flushing events (ensures LLM data is captured).
|
|
440
441
|
const method = this.method;
|
|
441
442
|
const path = this.path;
|
|
442
443
|
const self = this;
|
|
443
|
-
captureCodeContext(stack)
|
|
444
|
+
this._llmPromise = captureCodeContext(stack)
|
|
444
445
|
.then((codeContext) => {
|
|
445
446
|
// Update _errorInfo with captured code context
|
|
446
447
|
if (self._errorInfo) self._errorInfo.codeContext = codeContext;
|