prepia 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.
- package/LICENSE +21 -0
- package/README.md +312 -0
- package/bin/prepia.mjs +119 -0
- package/package.json +53 -0
- package/skill/SKILL.md +148 -0
- package/skill/config.json +29 -0
- package/src/analytics/dashboard.mjs +84 -0
- package/src/analytics/tracker.mjs +131 -0
- package/src/api/middleware.mjs +219 -0
- package/src/api/routes.mjs +142 -0
- package/src/api/server.mjs +150 -0
- package/src/cache/disk-store.mjs +199 -0
- package/src/cache/manager.mjs +142 -0
- package/src/cache/memory-store.mjs +205 -0
- package/src/chain/dag.mjs +209 -0
- package/src/chain/executor.mjs +103 -0
- package/src/chain/scheduler.mjs +89 -0
- package/src/client/adapters.mjs +483 -0
- package/src/client/connector.mjs +391 -0
- package/src/client/index.mjs +483 -0
- package/src/client/websocket.mjs +353 -0
- package/src/core/context-packager.mjs +169 -0
- package/src/core/engine.mjs +338 -0
- package/src/core/event-bus.mjs +84 -0
- package/src/core/prepimshot.mjs +120 -0
- package/src/core/task-decomposer.mjs +158 -0
- package/src/edge/lite.mjs +90 -0
- package/src/guard/checker.mjs +123 -0
- package/src/guard/fact-checker.mjs +105 -0
- package/src/guard/hallucination.mjs +108 -0
- package/src/index.mjs +67 -0
- package/src/models/local-model.mjs +171 -0
- package/src/models/provider.mjs +192 -0
- package/src/models/router.mjs +156 -0
- package/src/morph/optimizer.mjs +142 -0
- package/src/network/p2p.mjs +146 -0
- package/src/persona/detector.mjs +118 -0
- package/src/plugins/loader.mjs +120 -0
- package/src/plugins/registry.mjs +164 -0
- package/src/plugins/sandbox.mjs +79 -0
- package/src/rate/limiter.mjs +145 -0
- package/src/rate/shield.mjs +150 -0
- package/src/script/executor.mjs +164 -0
- package/src/script/parser.mjs +134 -0
- package/src/security/privacy.mjs +108 -0
- package/src/security/sanitizer.mjs +133 -0
- package/src/shadow/daemon.mjs +128 -0
- package/src/stream/handler.mjs +204 -0
- package/src/tools/calculator.mjs +312 -0
- package/src/tools/file-ops.mjs +138 -0
- package/src/tools/http-client.mjs +127 -0
- package/src/tools/orchestrator.mjs +205 -0
- package/src/tools/web-scraper.mjs +159 -0
- package/src/tools/web-search.mjs +129 -0
- package/src/vault/knowledge-base.mjs +207 -0
- package/src/vault/pattern-learner.mjs +192 -0
- package/workflows/analyze.json +32 -0
- package/workflows/automate.json +32 -0
- package/workflows/research.json +37 -0
- package/workflows/summarize.json +32 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview HTTP API server using node:http.
|
|
3
|
+
* @module api/server
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import http from 'node:http';
|
|
7
|
+
import { parseBody, cors, RateLimitMiddleware, AuthMiddleware, errorHandler, requestLogger } from './middleware.mjs';
|
|
8
|
+
import { createRoutes } from './routes.mjs';
|
|
9
|
+
|
|
10
|
+
export class Server {
|
|
11
|
+
/**
|
|
12
|
+
* @param {Object} engine - Prepia engine instance
|
|
13
|
+
* @param {Object} [options]
|
|
14
|
+
* @param {number} [options.port=3000] - Server port
|
|
15
|
+
* @param {string} [options.host='0.0.0.0'] - Bind address
|
|
16
|
+
* @param {string[]} [options.apiKeys] - Valid API keys
|
|
17
|
+
* @param {boolean} [options.authRequired=false] - Require authentication
|
|
18
|
+
* @param {number} [options.rateLimit=100] - Requests per minute
|
|
19
|
+
*/
|
|
20
|
+
constructor(engine, options = {}) {
|
|
21
|
+
this._engine = engine;
|
|
22
|
+
this._port = options.port ?? 3000;
|
|
23
|
+
this._host = options.host ?? '0.0.0.0';
|
|
24
|
+
this._server = null;
|
|
25
|
+
this._routes = createRoutes(engine);
|
|
26
|
+
this._rateLimiter = new RateLimitMiddleware({ maxRequests: options.rateLimit ?? 100 });
|
|
27
|
+
this._auth = new AuthMiddleware({
|
|
28
|
+
apiKeys: options.apiKeys,
|
|
29
|
+
required: options.authRequired ?? false,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Start the HTTP server.
|
|
35
|
+
* @returns {Promise<void>}
|
|
36
|
+
*/
|
|
37
|
+
start() {
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
this._server = http.createServer(async (req, res) => {
|
|
40
|
+
const logger = requestLogger(req);
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// CORS
|
|
44
|
+
cors(req, res);
|
|
45
|
+
if (req.method === 'OPTIONS') {
|
|
46
|
+
logger.end(res);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Auth
|
|
51
|
+
const authResult = this._auth.authenticate(req);
|
|
52
|
+
if (!authResult.authenticated) {
|
|
53
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
54
|
+
res.end(JSON.stringify({ error: { message: 'Unauthorized' } }));
|
|
55
|
+
logger.end(res);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Rate limiting
|
|
60
|
+
const rateResult = this._rateLimiter.check(authResult.clientId);
|
|
61
|
+
res.setHeader('X-RateLimit-Remaining', rateResult.remaining);
|
|
62
|
+
if (!rateResult.allowed) {
|
|
63
|
+
res.writeHead(429, {
|
|
64
|
+
'Content-Type': 'application/json',
|
|
65
|
+
'Retry-After': Math.ceil(rateResult.retryAfter / 1000),
|
|
66
|
+
});
|
|
67
|
+
res.end(JSON.stringify({ error: { message: 'Rate limit exceeded', retryAfter: rateResult.retryAfter } }));
|
|
68
|
+
logger.end(res);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Parse body
|
|
73
|
+
let body = null;
|
|
74
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
75
|
+
body = await parseBody(req);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Route
|
|
79
|
+
await this._routes.route(req, res, body);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
errorHandler(err, res);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
logger.end(res);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
this._server.listen(this._port, this._host, () => {
|
|
88
|
+
console.log(`Prepia API server running on http://${this._host}:${this._port}`);
|
|
89
|
+
resolve();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Stop the HTTP server.
|
|
96
|
+
* @returns {Promise<void>}
|
|
97
|
+
*/
|
|
98
|
+
stop() {
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
if (!this._server) {
|
|
101
|
+
resolve();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
this._server.close((err) => {
|
|
105
|
+
if (err) reject(err);
|
|
106
|
+
else resolve();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get the server address.
|
|
113
|
+
* @returns {Object|null}
|
|
114
|
+
*/
|
|
115
|
+
address() {
|
|
116
|
+
return this._server?.address() || null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get the port the server is listening on.
|
|
121
|
+
* @returns {number}
|
|
122
|
+
*/
|
|
123
|
+
get port() {
|
|
124
|
+
return this._server?.address()?.port || this._port;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Create and start a Prepia API server.
|
|
130
|
+
* @param {Object} engine - Prepia engine
|
|
131
|
+
* @param {Object} [options]
|
|
132
|
+
* @returns {Promise<Server>}
|
|
133
|
+
*/
|
|
134
|
+
export async function createServer(engine, options = {}) {
|
|
135
|
+
const server = new Server(engine, options);
|
|
136
|
+
await server.start();
|
|
137
|
+
return server;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// If run directly, start a standalone server
|
|
141
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
142
|
+
const { PrepiaEngine } = await import('../core/engine.mjs');
|
|
143
|
+
const engine = new PrepiaEngine();
|
|
144
|
+
const server = await createServer(engine, {
|
|
145
|
+
port: parseInt(process.env.PREPIA_PORT || '3000'),
|
|
146
|
+
});
|
|
147
|
+
console.log(`Server running at http://localhost:${server.port}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export default { Server, createServer };
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Persistent disk cache using JSON files.
|
|
3
|
+
* @module cache/disk-store
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { promises as fs } from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import crypto from 'node:crypto';
|
|
9
|
+
|
|
10
|
+
export class DiskStore {
|
|
11
|
+
/**
|
|
12
|
+
* Create a new DiskStore.
|
|
13
|
+
* @param {Object} [options]
|
|
14
|
+
* @param {string} [options.cacheDir='.prepia/cache'] - Cache directory path
|
|
15
|
+
* @param {number} [options.defaultTTL=3600000] - Default TTL in ms (1 hour)
|
|
16
|
+
* @param {number} [options.maxSize=10000] - Maximum number of entries
|
|
17
|
+
*/
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
this._cacheDir = options.cacheDir ?? '.prepia/cache';
|
|
20
|
+
this._defaultTTL = options.defaultTTL ?? 60 * 60 * 1000;
|
|
21
|
+
this._maxSize = options.maxSize ?? 10000;
|
|
22
|
+
this._initialized = false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Initialize the cache directory.
|
|
27
|
+
*/
|
|
28
|
+
async init() {
|
|
29
|
+
if (this._initialized) return;
|
|
30
|
+
await fs.mkdir(this._cacheDir, { recursive: true });
|
|
31
|
+
this._initialized = true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generate a safe filename from a cache key.
|
|
36
|
+
* @param {string} key
|
|
37
|
+
* @returns {string}
|
|
38
|
+
* @private
|
|
39
|
+
*/
|
|
40
|
+
_keyToFilename(key) {
|
|
41
|
+
const hash = crypto.createHash('sha256').update(key).digest('hex');
|
|
42
|
+
return path.join(this._cacheDir, `${hash}.json`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get a value from disk cache.
|
|
47
|
+
* @param {string} key
|
|
48
|
+
* @returns {Promise<*>} The cached value or undefined
|
|
49
|
+
*/
|
|
50
|
+
async get(key) {
|
|
51
|
+
await this.init();
|
|
52
|
+
const filepath = this._keyToFilename(key);
|
|
53
|
+
try {
|
|
54
|
+
const data = await fs.readFile(filepath, 'utf-8');
|
|
55
|
+
const entry = JSON.parse(data);
|
|
56
|
+
if (Date.now() > entry.expiresAt) {
|
|
57
|
+
await fs.unlink(filepath).catch(() => {});
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
entry.hits = (entry.hits || 0) + 1;
|
|
61
|
+
await fs.writeFile(filepath, JSON.stringify(entry), 'utf-8').catch(() => {});
|
|
62
|
+
return entry.value;
|
|
63
|
+
} catch {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Set a value in disk cache.
|
|
70
|
+
* @param {string} key
|
|
71
|
+
* @param {*} value
|
|
72
|
+
* @param {number} [ttl] - TTL in ms
|
|
73
|
+
*/
|
|
74
|
+
async set(key, value, ttl) {
|
|
75
|
+
await this.init();
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
const entry = {
|
|
78
|
+
key,
|
|
79
|
+
value,
|
|
80
|
+
createdAt: now,
|
|
81
|
+
expiresAt: now + (ttl ?? this._defaultTTL),
|
|
82
|
+
hits: 0,
|
|
83
|
+
};
|
|
84
|
+
const filepath = this._keyToFilename(key);
|
|
85
|
+
await fs.writeFile(filepath, JSON.stringify(entry), 'utf-8');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check if a key exists and is not expired.
|
|
90
|
+
* @param {string} key
|
|
91
|
+
* @returns {Promise<boolean>}
|
|
92
|
+
*/
|
|
93
|
+
async has(key) {
|
|
94
|
+
await this.init();
|
|
95
|
+
const filepath = this._keyToFilename(key);
|
|
96
|
+
try {
|
|
97
|
+
const data = await fs.readFile(filepath, 'utf-8');
|
|
98
|
+
const entry = JSON.parse(data);
|
|
99
|
+
if (Date.now() > entry.expiresAt) {
|
|
100
|
+
await fs.unlink(filepath).catch(() => {});
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Delete a key from disk cache.
|
|
111
|
+
* @param {string} key
|
|
112
|
+
* @returns {Promise<boolean>}
|
|
113
|
+
*/
|
|
114
|
+
async delete(key) {
|
|
115
|
+
await this.init();
|
|
116
|
+
const filepath = this._keyToFilename(key);
|
|
117
|
+
try {
|
|
118
|
+
await fs.unlink(filepath);
|
|
119
|
+
return true;
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Clear all cache entries.
|
|
127
|
+
*/
|
|
128
|
+
async clear() {
|
|
129
|
+
await this.init();
|
|
130
|
+
try {
|
|
131
|
+
const files = await fs.readdir(this._cacheDir);
|
|
132
|
+
await Promise.all(
|
|
133
|
+
files
|
|
134
|
+
.filter(f => f.endsWith('.json'))
|
|
135
|
+
.map(f => fs.unlink(path.join(this._cacheDir, f)).catch(() => {}))
|
|
136
|
+
);
|
|
137
|
+
} catch {
|
|
138
|
+
// Directory may not exist yet
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Remove expired entries.
|
|
144
|
+
* @returns {Promise<number>} Number of entries removed
|
|
145
|
+
*/
|
|
146
|
+
async cleanup() {
|
|
147
|
+
await this.init();
|
|
148
|
+
let removed = 0;
|
|
149
|
+
try {
|
|
150
|
+
const files = await fs.readdir(this._cacheDir);
|
|
151
|
+
const now = Date.now();
|
|
152
|
+
for (const file of files) {
|
|
153
|
+
if (!file.endsWith('.json')) continue;
|
|
154
|
+
const filepath = path.join(this._cacheDir, file);
|
|
155
|
+
try {
|
|
156
|
+
const data = await fs.readFile(filepath, 'utf-8');
|
|
157
|
+
const entry = JSON.parse(data);
|
|
158
|
+
if (now > entry.expiresAt) {
|
|
159
|
+
await fs.unlink(filepath);
|
|
160
|
+
removed++;
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
// Corrupted file, remove it
|
|
164
|
+
await fs.unlink(filepath).catch(() => {});
|
|
165
|
+
removed++;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
// Directory may not exist
|
|
170
|
+
}
|
|
171
|
+
return removed;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get the count of cached entries.
|
|
176
|
+
* @returns {Promise<number>}
|
|
177
|
+
*/
|
|
178
|
+
async size() {
|
|
179
|
+
await this.init();
|
|
180
|
+
try {
|
|
181
|
+
const files = await fs.readdir(this._cacheDir);
|
|
182
|
+
return files.filter(f => f.endsWith('.json')).length;
|
|
183
|
+
} catch {
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Generate a cache key from parameters.
|
|
190
|
+
* @param {Object} params
|
|
191
|
+
* @returns {string}
|
|
192
|
+
*/
|
|
193
|
+
static generateKey(params) {
|
|
194
|
+
const str = typeof params === 'string' ? params : JSON.stringify(params, Object.keys(params).sort());
|
|
195
|
+
return crypto.createHash('sha256').update(str).digest('hex');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export default DiskStore;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Cache orchestration - coordinates memory and disk stores.
|
|
3
|
+
* @module cache/manager
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import crypto from 'node:crypto';
|
|
7
|
+
import { MemoryStore } from './memory-store.mjs';
|
|
8
|
+
import { DiskStore } from './disk-store.mjs';
|
|
9
|
+
|
|
10
|
+
export class CacheManager {
|
|
11
|
+
/**
|
|
12
|
+
* Create a new CacheManager.
|
|
13
|
+
* @param {Object} [options]
|
|
14
|
+
* @param {number} [options.memoryMaxSize=1000] - Memory cache max entries
|
|
15
|
+
* @param {number} [options.memoryTTL=300000] - Memory TTL (5 min)
|
|
16
|
+
* @param {number} [options.diskTTL=3600000] - Disk TTL (1 hour)
|
|
17
|
+
* @param {string} [options.cacheDir='.prepia/cache'] - Disk cache directory
|
|
18
|
+
* @param {boolean} [options.enableDisk=true] - Enable disk persistence
|
|
19
|
+
*/
|
|
20
|
+
constructor(options = {}) {
|
|
21
|
+
this.memory = new MemoryStore({
|
|
22
|
+
maxSize: options.memoryMaxSize ?? 1000,
|
|
23
|
+
defaultTTL: options.memoryTTL ?? 5 * 60 * 1000,
|
|
24
|
+
});
|
|
25
|
+
this.disk = new DiskStore({
|
|
26
|
+
cacheDir: options.cacheDir ?? '.prepia/cache',
|
|
27
|
+
defaultTTL: options.diskTTL ?? 60 * 60 * 1000,
|
|
28
|
+
});
|
|
29
|
+
this._enableDisk = options.enableDisk ?? true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Generate a cache key from task parameters.
|
|
34
|
+
* @param {Object|string} params
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
generateKey(params) {
|
|
38
|
+
const str = typeof params === 'string' ? params : JSON.stringify(params, Object.keys(params).sort());
|
|
39
|
+
return crypto.createHash('sha256').update(str).digest('hex');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get a value - checks memory first, then disk.
|
|
44
|
+
* Populates memory from disk on disk hit.
|
|
45
|
+
* @param {string} key
|
|
46
|
+
* @returns {Promise<*>}
|
|
47
|
+
*/
|
|
48
|
+
async get(key) {
|
|
49
|
+
// Try memory first
|
|
50
|
+
const memResult = this.memory.get(key);
|
|
51
|
+
if (memResult !== undefined) return memResult;
|
|
52
|
+
|
|
53
|
+
// Try disk if enabled
|
|
54
|
+
if (this._enableDisk) {
|
|
55
|
+
const diskResult = await this.disk.get(key);
|
|
56
|
+
if (diskResult !== undefined) {
|
|
57
|
+
// Promote to memory
|
|
58
|
+
this.memory.set(key, diskResult);
|
|
59
|
+
return diskResult;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Set a value in both memory and disk cache.
|
|
68
|
+
* @param {string} key
|
|
69
|
+
* @param {*} value
|
|
70
|
+
* @param {Object} [options]
|
|
71
|
+
* @param {number} [options.memoryTTL] - Memory TTL override
|
|
72
|
+
* @param {number} [options.diskTTL] - Disk TTL override
|
|
73
|
+
* @param {boolean} [options.memoryOnly=false] - Only store in memory
|
|
74
|
+
*/
|
|
75
|
+
async set(key, value, options = {}) {
|
|
76
|
+
this.memory.set(key, value, options.memoryTTL);
|
|
77
|
+
if (this._enableDisk && !options.memoryOnly) {
|
|
78
|
+
await this.disk.set(key, value, options.diskTTL);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check if key exists in either cache.
|
|
84
|
+
* @param {string} key
|
|
85
|
+
* @returns {Promise<boolean>}
|
|
86
|
+
*/
|
|
87
|
+
async has(key) {
|
|
88
|
+
if (this.memory.has(key)) return true;
|
|
89
|
+
if (this._enableDisk) return this.disk.has(key);
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Delete a key from both caches.
|
|
95
|
+
* @param {string} key
|
|
96
|
+
* @returns {Promise<boolean>}
|
|
97
|
+
*/
|
|
98
|
+
async delete(key) {
|
|
99
|
+
const memDeleted = this.memory.delete(key);
|
|
100
|
+
let diskDeleted = false;
|
|
101
|
+
if (this._enableDisk) {
|
|
102
|
+
diskDeleted = await this.disk.delete(key);
|
|
103
|
+
}
|
|
104
|
+
return memDeleted || diskDeleted;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Clear all caches.
|
|
109
|
+
*/
|
|
110
|
+
async clear() {
|
|
111
|
+
this.memory.clear();
|
|
112
|
+
if (this._enableDisk) {
|
|
113
|
+
await this.disk.clear();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Cleanup expired entries from both caches.
|
|
119
|
+
* @returns {Promise<Object>} Cleanup stats
|
|
120
|
+
*/
|
|
121
|
+
async cleanup() {
|
|
122
|
+
const memCleaned = this.memory.cleanup();
|
|
123
|
+
let diskCleaned = 0;
|
|
124
|
+
if (this._enableDisk) {
|
|
125
|
+
diskCleaned = await this.disk.cleanup();
|
|
126
|
+
}
|
|
127
|
+
return { memoryCleaned: memCleaned, diskCleaned };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get combined stats.
|
|
132
|
+
* @returns {Object}
|
|
133
|
+
*/
|
|
134
|
+
stats() {
|
|
135
|
+
return {
|
|
136
|
+
memory: this.memory.stats(),
|
|
137
|
+
diskEnabled: this._enableDisk,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export default CacheManager;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview In-memory LRU cache with TTL support.
|
|
3
|
+
* @module cache/memory-store
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {Object} CacheEntry
|
|
8
|
+
* @property {*} value - Cached value
|
|
9
|
+
* @property {number} expiresAt - Expiration timestamp (ms)
|
|
10
|
+
* @property {number} createdAt - Creation timestamp (ms)
|
|
11
|
+
* @property {number} size - Estimated size in bytes
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} CacheStats
|
|
16
|
+
* @property {number} hits - Cache hit count
|
|
17
|
+
* @property {number} misses - Cache miss count
|
|
18
|
+
* @property {number} evictions - Eviction count
|
|
19
|
+
* @property {number} sets - Set operation count
|
|
20
|
+
* @property {number} deletes - Delete operation count
|
|
21
|
+
* @property {number} size - Current entry count
|
|
22
|
+
* @property {number} maxSize - Maximum entries
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export class MemoryStore {
|
|
26
|
+
/**
|
|
27
|
+
* Create a new MemoryStore.
|
|
28
|
+
* @param {Object} [options]
|
|
29
|
+
* @param {number} [options.maxSize=1000] - Maximum number of entries
|
|
30
|
+
* @param {number} [options.defaultTTL=300000] - Default TTL in ms (5 min)
|
|
31
|
+
*/
|
|
32
|
+
constructor(options = {}) {
|
|
33
|
+
this._maxSize = options.maxSize ?? 1000;
|
|
34
|
+
this._defaultTTL = options.defaultTTL ?? 5 * 60 * 1000;
|
|
35
|
+
/** @type {Map<string, CacheEntry>} */
|
|
36
|
+
this._store = new Map();
|
|
37
|
+
this._stats = { hits: 0, misses: 0, evictions: 0, sets: 0, deletes: 0 };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get a value from cache.
|
|
42
|
+
* @param {string} key
|
|
43
|
+
* @returns {*} The cached value or undefined
|
|
44
|
+
*/
|
|
45
|
+
get(key) {
|
|
46
|
+
const entry = this._store.get(key);
|
|
47
|
+
if (!entry) {
|
|
48
|
+
this._stats.misses++;
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
if (Date.now() > entry.expiresAt) {
|
|
52
|
+
this._store.delete(key);
|
|
53
|
+
this._stats.misses++;
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
// Move to end (most recently used) by re-inserting
|
|
57
|
+
this._store.delete(key);
|
|
58
|
+
this._store.set(key, entry);
|
|
59
|
+
this._stats.hits++;
|
|
60
|
+
return entry.value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Set a value in cache.
|
|
65
|
+
* @param {string} key
|
|
66
|
+
* @param {*} value
|
|
67
|
+
* @param {number} [ttl] - TTL in ms, defaults to store default
|
|
68
|
+
*/
|
|
69
|
+
set(key, value, ttl) {
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
const effectiveTTL = ttl ?? this._defaultTTL;
|
|
72
|
+
const entry = {
|
|
73
|
+
value,
|
|
74
|
+
expiresAt: now + effectiveTTL,
|
|
75
|
+
createdAt: now,
|
|
76
|
+
size: this._estimateSize(value),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// If key exists, remove first to update position
|
|
80
|
+
if (this._store.has(key)) {
|
|
81
|
+
this._store.delete(key);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Evict oldest if at capacity
|
|
85
|
+
while (this._store.size >= this._maxSize) {
|
|
86
|
+
const oldestKey = this._store.keys().next().value;
|
|
87
|
+
this._store.delete(oldestKey);
|
|
88
|
+
this._stats.evictions++;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this._store.set(key, entry);
|
|
92
|
+
this._stats.sets++;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if a key exists and is not expired.
|
|
97
|
+
* @param {string} key
|
|
98
|
+
* @returns {boolean}
|
|
99
|
+
*/
|
|
100
|
+
has(key) {
|
|
101
|
+
const entry = this._store.get(key);
|
|
102
|
+
if (!entry) return false;
|
|
103
|
+
if (Date.now() > entry.expiresAt) {
|
|
104
|
+
this._store.delete(key);
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Delete a key from cache.
|
|
112
|
+
* @param {string} key
|
|
113
|
+
* @returns {boolean} Whether the key existed
|
|
114
|
+
*/
|
|
115
|
+
delete(key) {
|
|
116
|
+
const existed = this._store.delete(key);
|
|
117
|
+
if (existed) this._stats.deletes++;
|
|
118
|
+
return existed;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Clear all entries.
|
|
123
|
+
*/
|
|
124
|
+
clear() {
|
|
125
|
+
this._store.clear();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get the number of entries.
|
|
130
|
+
* @returns {number}
|
|
131
|
+
*/
|
|
132
|
+
get size() {
|
|
133
|
+
return this._store.size;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get cache statistics.
|
|
138
|
+
* @returns {CacheStats}
|
|
139
|
+
*/
|
|
140
|
+
stats() {
|
|
141
|
+
return {
|
|
142
|
+
...this._stats,
|
|
143
|
+
size: this._store.size,
|
|
144
|
+
maxSize: this._maxSize,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Reset statistics.
|
|
150
|
+
*/
|
|
151
|
+
resetStats() {
|
|
152
|
+
this._stats = { hits: 0, misses: 0, evictions: 0, sets: 0, deletes: 0 };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Remove all expired entries.
|
|
157
|
+
* @returns {number} Number of entries removed
|
|
158
|
+
*/
|
|
159
|
+
cleanup() {
|
|
160
|
+
const now = Date.now();
|
|
161
|
+
let removed = 0;
|
|
162
|
+
for (const [key, entry] of this._store) {
|
|
163
|
+
if (now > entry.expiresAt) {
|
|
164
|
+
this._store.delete(key);
|
|
165
|
+
removed++;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return removed;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get all keys (non-expired).
|
|
173
|
+
* @returns {string[]}
|
|
174
|
+
*/
|
|
175
|
+
keys() {
|
|
176
|
+
const now = Date.now();
|
|
177
|
+
const result = [];
|
|
178
|
+
for (const [key, entry] of this._store) {
|
|
179
|
+
if (now <= entry.expiresAt) {
|
|
180
|
+
result.push(key);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Estimate size of a value in bytes.
|
|
188
|
+
* @param {*} value
|
|
189
|
+
* @returns {number}
|
|
190
|
+
* @private
|
|
191
|
+
*/
|
|
192
|
+
_estimateSize(value) {
|
|
193
|
+
if (value === null || value === undefined) return 0;
|
|
194
|
+
if (typeof value === 'string') return value.length * 2;
|
|
195
|
+
if (typeof value === 'number') return 8;
|
|
196
|
+
if (typeof value === 'boolean') return 4;
|
|
197
|
+
try {
|
|
198
|
+
return JSON.stringify(value).length * 2;
|
|
199
|
+
} catch {
|
|
200
|
+
return 100;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export default MemoryStore;
|