@unrdf/kgc-probe 26.4.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.
- package/README.md +414 -0
- package/package.json +81 -0
- package/src/agents/index.mjs +1402 -0
- package/src/artifact.mjs +405 -0
- package/src/cli.mjs +932 -0
- package/src/config.mjs +115 -0
- package/src/guards.mjs +1213 -0
- package/src/index.mjs +347 -0
- package/src/merge.mjs +196 -0
- package/src/observation.mjs +193 -0
- package/src/orchestrator.mjs +315 -0
- package/src/probe.mjs +58 -0
- package/src/probes/CONCURRENCY-PROBE.md +256 -0
- package/src/probes/README.md +275 -0
- package/src/probes/concurrency.mjs +1175 -0
- package/src/probes/filesystem.mjs +731 -0
- package/src/probes/filesystem.test.mjs +244 -0
- package/src/probes/network.mjs +503 -0
- package/src/probes/performance.mjs +816 -0
- package/src/probes/persistence.mjs +785 -0
- package/src/probes/runtime.mjs +589 -0
- package/src/probes/tooling.mjs +454 -0
- package/src/probes/tooling.test.mjs +372 -0
- package/src/probes/verify-execution.mjs +131 -0
- package/src/probes/verify-guards.mjs +73 -0
- package/src/probes/wasm.mjs +715 -0
- package/src/receipt.mjs +197 -0
- package/src/receipts/index.mjs +813 -0
- package/src/reporter.example.mjs +223 -0
- package/src/reporter.mjs +555 -0
- package/src/reporters/markdown.mjs +355 -0
- package/src/reporters/rdf.mjs +383 -0
- package/src/storage/index.mjs +827 -0
- package/src/types.mjs +1028 -0
- package/src/utils/errors.mjs +397 -0
- package/src/utils/index.mjs +32 -0
- package/src/utils/logger.mjs +236 -0
- package/src/vocabulary.ttl +169 -0
|
@@ -0,0 +1,827 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview KGC Probe - Storage Backends
|
|
3
|
+
*
|
|
4
|
+
* Three storage implementations:
|
|
5
|
+
* - MemoryStorage: In-process hash map (development)
|
|
6
|
+
* - FileStorage: Filesystem-based (testing)
|
|
7
|
+
* - DatabaseStorage: Structured storage with query support (production)
|
|
8
|
+
*
|
|
9
|
+
* Interface: { set(key, value), get(key), delete(key), query(pattern) }
|
|
10
|
+
*
|
|
11
|
+
* @module @unrdf/kgc-probe/storage
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { promises as fs, existsSync, mkdirSync } from 'fs';
|
|
15
|
+
import { join, dirname } from 'path';
|
|
16
|
+
import { randomUUID } from 'crypto';
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// STORAGE INTERFACE
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} StorageInterface
|
|
24
|
+
* @property {string} type - Storage type identifier
|
|
25
|
+
* @property {(key: string, value: any) => Promise<void>} set - Set a value
|
|
26
|
+
* @property {(key: string) => Promise<any>} get - Get a value
|
|
27
|
+
* @property {(key: string) => Promise<boolean>} delete - Delete a value
|
|
28
|
+
* @property {(pattern: string) => Promise<Array>} query - Query values by pattern
|
|
29
|
+
* @property {() => Promise<string[]>} keys - List all keys
|
|
30
|
+
* @property {() => Promise<number>} count - Count entries
|
|
31
|
+
* @property {() => Promise<void>} clear - Clear all entries
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// MEMORY STORAGE
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* MemoryStorage - In-process storage using Map
|
|
40
|
+
*
|
|
41
|
+
* Use for: Development, testing, single-process deployments
|
|
42
|
+
* @implements {StorageInterface}
|
|
43
|
+
*/
|
|
44
|
+
export class MemoryStorage {
|
|
45
|
+
/**
|
|
46
|
+
*
|
|
47
|
+
*/
|
|
48
|
+
constructor() {
|
|
49
|
+
/** @type {string} */
|
|
50
|
+
this.type = 'memory';
|
|
51
|
+
/** @type {Map<string, any>} */
|
|
52
|
+
this.store = new Map();
|
|
53
|
+
/** @type {Map<string, number>} */
|
|
54
|
+
this.timestamps = new Map();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Set a value with key
|
|
59
|
+
* @param {string} key - Storage key
|
|
60
|
+
* @param {any} value - Value to store
|
|
61
|
+
* @returns {Promise<void>}
|
|
62
|
+
*/
|
|
63
|
+
async set(key, value) {
|
|
64
|
+
if (typeof key !== 'string' || key.length === 0) {
|
|
65
|
+
throw new Error('Key must be a non-empty string');
|
|
66
|
+
}
|
|
67
|
+
this.store.set(key, structuredClone(value));
|
|
68
|
+
this.timestamps.set(key, Date.now());
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get a value by key
|
|
73
|
+
* @param {string} key - Storage key
|
|
74
|
+
* @returns {Promise<any>} Stored value or undefined
|
|
75
|
+
*/
|
|
76
|
+
async get(key) {
|
|
77
|
+
if (!this.store.has(key)) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
return structuredClone(this.store.get(key));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Delete a value by key
|
|
85
|
+
* @param {string} key - Storage key
|
|
86
|
+
* @returns {Promise<boolean>} True if deleted
|
|
87
|
+
*/
|
|
88
|
+
async delete(key) {
|
|
89
|
+
this.timestamps.delete(key);
|
|
90
|
+
return this.store.delete(key);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Query values by pattern (glob-style matching)
|
|
95
|
+
* @param {string} pattern - Pattern to match (supports * wildcard)
|
|
96
|
+
* @returns {Promise<Array<{key: string, value: any}>>} Matching entries
|
|
97
|
+
*/
|
|
98
|
+
async query(pattern) {
|
|
99
|
+
const results = [];
|
|
100
|
+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
|
101
|
+
|
|
102
|
+
for (const [key, value] of this.store) {
|
|
103
|
+
if (regex.test(key)) {
|
|
104
|
+
results.push({ key, value: structuredClone(value) });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return results;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* List all keys
|
|
113
|
+
* @returns {Promise<string[]>}
|
|
114
|
+
*/
|
|
115
|
+
async keys() {
|
|
116
|
+
return Array.from(this.store.keys());
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Count entries
|
|
121
|
+
* @returns {Promise<number>}
|
|
122
|
+
*/
|
|
123
|
+
async count() {
|
|
124
|
+
return this.store.size;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Clear all entries
|
|
129
|
+
* @returns {Promise<void>}
|
|
130
|
+
*/
|
|
131
|
+
async clear() {
|
|
132
|
+
this.store.clear();
|
|
133
|
+
this.timestamps.clear();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Check if key exists
|
|
138
|
+
* @param {string} key - Storage key
|
|
139
|
+
* @returns {Promise<boolean>}
|
|
140
|
+
*/
|
|
141
|
+
async has(key) {
|
|
142
|
+
return this.store.has(key);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Artifact API (backward compatibility)
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Save artifact to memory
|
|
149
|
+
* @param {Object} artifact - Artifact to save
|
|
150
|
+
* @returns {Promise<void>}
|
|
151
|
+
*/
|
|
152
|
+
async saveArtifact(artifact) {
|
|
153
|
+
const key = artifact.probe_run_id || artifact.id || randomUUID();
|
|
154
|
+
await this.set(`artifact:${key}`, artifact);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Load artifact from memory
|
|
159
|
+
* @param {string} artifactId - Artifact ID
|
|
160
|
+
* @returns {Promise<Object>}
|
|
161
|
+
*/
|
|
162
|
+
async loadArtifact(artifactId) {
|
|
163
|
+
const artifact = await this.get(`artifact:${artifactId}`);
|
|
164
|
+
if (!artifact) {
|
|
165
|
+
throw new Error(`Artifact not found: ${artifactId}`);
|
|
166
|
+
}
|
|
167
|
+
return artifact;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Fetch all artifacts (shards)
|
|
172
|
+
* @returns {Promise<Array>}
|
|
173
|
+
*/
|
|
174
|
+
async fetchShards() {
|
|
175
|
+
const results = await this.query('artifact:*');
|
|
176
|
+
return results.map(r => r.value);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* List artifact IDs
|
|
181
|
+
* @returns {Promise<string[]>}
|
|
182
|
+
*/
|
|
183
|
+
async listArtifacts() {
|
|
184
|
+
const keys = await this.keys();
|
|
185
|
+
return keys
|
|
186
|
+
.filter(k => k.startsWith('artifact:'))
|
|
187
|
+
.map(k => k.replace('artifact:', ''));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Delete artifact
|
|
192
|
+
* @param {string} artifactId - Artifact ID
|
|
193
|
+
* @returns {Promise<boolean>}
|
|
194
|
+
*/
|
|
195
|
+
async deleteArtifact(artifactId) {
|
|
196
|
+
return this.delete(`artifact:${artifactId}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ============================================================================
|
|
201
|
+
// FILE STORAGE
|
|
202
|
+
// ============================================================================
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* FileStorage - Filesystem-based storage
|
|
206
|
+
*
|
|
207
|
+
* Use for: Testing, single-node deployment, audit trail
|
|
208
|
+
* Structure:
|
|
209
|
+
* <rootDir>/
|
|
210
|
+
* <key>.json
|
|
211
|
+
* ...
|
|
212
|
+
* @implements {StorageInterface}
|
|
213
|
+
*/
|
|
214
|
+
export class FileStorage {
|
|
215
|
+
/**
|
|
216
|
+
* Create file storage
|
|
217
|
+
* @param {string} [rootDir] - Root directory for storage
|
|
218
|
+
*/
|
|
219
|
+
constructor(rootDir = './storage') {
|
|
220
|
+
/** @type {string} */
|
|
221
|
+
this.type = 'file';
|
|
222
|
+
/** @type {string} */
|
|
223
|
+
this.rootDir = rootDir;
|
|
224
|
+
this._ensureDir();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Ensure root directory exists
|
|
229
|
+
* @private
|
|
230
|
+
*/
|
|
231
|
+
_ensureDir() {
|
|
232
|
+
if (!existsSync(this.rootDir)) {
|
|
233
|
+
mkdirSync(this.rootDir, { recursive: true });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Get file path for key
|
|
239
|
+
* @param {string} key - Storage key
|
|
240
|
+
* @returns {string}
|
|
241
|
+
* @private
|
|
242
|
+
*/
|
|
243
|
+
_getPath(key) {
|
|
244
|
+
// Sanitize key for filesystem
|
|
245
|
+
const sanitized = key.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
246
|
+
return join(this.rootDir, `${sanitized}.json`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Set a value with key
|
|
251
|
+
* @param {string} key - Storage key
|
|
252
|
+
* @param {any} value - Value to store
|
|
253
|
+
* @returns {Promise<void>}
|
|
254
|
+
*/
|
|
255
|
+
async set(key, value) {
|
|
256
|
+
if (typeof key !== 'string' || key.length === 0) {
|
|
257
|
+
throw new Error('Key must be a non-empty string');
|
|
258
|
+
}
|
|
259
|
+
this._ensureDir();
|
|
260
|
+
const path = this._getPath(key);
|
|
261
|
+
const data = {
|
|
262
|
+
key,
|
|
263
|
+
value,
|
|
264
|
+
timestamp: Date.now(),
|
|
265
|
+
version: 1
|
|
266
|
+
};
|
|
267
|
+
await fs.writeFile(path, JSON.stringify(data, null, 2), 'utf8');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Get a value by key
|
|
272
|
+
* @param {string} key - Storage key
|
|
273
|
+
* @returns {Promise<any>}
|
|
274
|
+
*/
|
|
275
|
+
async get(key) {
|
|
276
|
+
const path = this._getPath(key);
|
|
277
|
+
try {
|
|
278
|
+
const content = await fs.readFile(path, 'utf8');
|
|
279
|
+
const data = JSON.parse(content);
|
|
280
|
+
return data.value;
|
|
281
|
+
} catch (err) {
|
|
282
|
+
if (err.code === 'ENOENT') {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
throw err;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Delete a value by key
|
|
291
|
+
* @param {string} key - Storage key
|
|
292
|
+
* @returns {Promise<boolean>}
|
|
293
|
+
*/
|
|
294
|
+
async delete(key) {
|
|
295
|
+
const path = this._getPath(key);
|
|
296
|
+
try {
|
|
297
|
+
await fs.unlink(path);
|
|
298
|
+
return true;
|
|
299
|
+
} catch (err) {
|
|
300
|
+
if (err.code === 'ENOENT') {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
throw err;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Query values by pattern
|
|
309
|
+
* @param {string} pattern - Pattern to match
|
|
310
|
+
* @returns {Promise<Array<{key: string, value: any}>>}
|
|
311
|
+
*/
|
|
312
|
+
async query(pattern) {
|
|
313
|
+
const results = [];
|
|
314
|
+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
const files = await fs.readdir(this.rootDir);
|
|
318
|
+
for (const file of files) {
|
|
319
|
+
if (!file.endsWith('.json')) continue;
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
const content = await fs.readFile(join(this.rootDir, file), 'utf8');
|
|
323
|
+
const data = JSON.parse(content);
|
|
324
|
+
if (regex.test(data.key)) {
|
|
325
|
+
results.push({ key: data.key, value: data.value });
|
|
326
|
+
}
|
|
327
|
+
} catch {
|
|
328
|
+
// Skip invalid files
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
} catch (err) {
|
|
332
|
+
if (err.code !== 'ENOENT') throw err;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return results;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* List all keys
|
|
340
|
+
* @returns {Promise<string[]>}
|
|
341
|
+
*/
|
|
342
|
+
async keys() {
|
|
343
|
+
const allKeys = [];
|
|
344
|
+
try {
|
|
345
|
+
const files = await fs.readdir(this.rootDir);
|
|
346
|
+
for (const file of files) {
|
|
347
|
+
if (!file.endsWith('.json')) continue;
|
|
348
|
+
try {
|
|
349
|
+
const content = await fs.readFile(join(this.rootDir, file), 'utf8');
|
|
350
|
+
const data = JSON.parse(content);
|
|
351
|
+
allKeys.push(data.key);
|
|
352
|
+
} catch {
|
|
353
|
+
// Skip invalid files
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
} catch (err) {
|
|
357
|
+
if (err.code !== 'ENOENT') throw err;
|
|
358
|
+
}
|
|
359
|
+
return allKeys;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Count entries
|
|
364
|
+
* @returns {Promise<number>}
|
|
365
|
+
*/
|
|
366
|
+
async count() {
|
|
367
|
+
const keys = await this.keys();
|
|
368
|
+
return keys.length;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Clear all entries
|
|
373
|
+
* @returns {Promise<void>}
|
|
374
|
+
*/
|
|
375
|
+
async clear() {
|
|
376
|
+
try {
|
|
377
|
+
const files = await fs.readdir(this.rootDir);
|
|
378
|
+
for (const file of files) {
|
|
379
|
+
if (file.endsWith('.json')) {
|
|
380
|
+
await fs.unlink(join(this.rootDir, file));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
} catch (err) {
|
|
384
|
+
if (err.code !== 'ENOENT') throw err;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Check if key exists
|
|
390
|
+
* @param {string} key - Storage key
|
|
391
|
+
* @returns {Promise<boolean>}
|
|
392
|
+
*/
|
|
393
|
+
async has(key) {
|
|
394
|
+
const path = this._getPath(key);
|
|
395
|
+
return existsSync(path);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Artifact API (backward compatibility)
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Save artifact to file
|
|
402
|
+
* @param {Object} artifact - Artifact to save
|
|
403
|
+
* @returns {Promise<void>}
|
|
404
|
+
*/
|
|
405
|
+
async saveArtifact(artifact) {
|
|
406
|
+
const key = artifact.probe_run_id || artifact.id || randomUUID();
|
|
407
|
+
await this.set(`artifact:${key}`, artifact);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Load artifact from file
|
|
412
|
+
* @param {string} artifactId - Artifact ID
|
|
413
|
+
* @returns {Promise<Object>}
|
|
414
|
+
*/
|
|
415
|
+
async loadArtifact(artifactId) {
|
|
416
|
+
const artifact = await this.get(`artifact:${artifactId}`);
|
|
417
|
+
if (!artifact) {
|
|
418
|
+
throw new Error(`Artifact not found: ${artifactId}`);
|
|
419
|
+
}
|
|
420
|
+
return artifact;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Fetch all artifacts
|
|
425
|
+
* @returns {Promise<Array>}
|
|
426
|
+
*/
|
|
427
|
+
async fetchShards() {
|
|
428
|
+
const results = await this.query('artifact:*');
|
|
429
|
+
return results.map(r => r.value);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* List artifact IDs
|
|
434
|
+
* @returns {Promise<string[]>}
|
|
435
|
+
*/
|
|
436
|
+
async listArtifacts() {
|
|
437
|
+
const keys = await this.keys();
|
|
438
|
+
return keys
|
|
439
|
+
.filter(k => k.startsWith('artifact:'))
|
|
440
|
+
.map(k => k.replace('artifact:', ''));
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Delete artifact
|
|
445
|
+
* @param {string} artifactId - Artifact ID
|
|
446
|
+
* @returns {Promise<boolean>}
|
|
447
|
+
*/
|
|
448
|
+
async deleteArtifact(artifactId) {
|
|
449
|
+
return this.delete(`artifact:${artifactId}`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ============================================================================
|
|
454
|
+
// DATABASE STORAGE
|
|
455
|
+
// ============================================================================
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* DatabaseStorage - In-memory database simulation with advanced querying
|
|
459
|
+
*
|
|
460
|
+
* Use for: Production, distributed deployments
|
|
461
|
+
* Provides: Indexing, range queries, transactions
|
|
462
|
+
* @implements {StorageInterface}
|
|
463
|
+
*/
|
|
464
|
+
export class DatabaseStorage {
|
|
465
|
+
/**
|
|
466
|
+
* Create database storage
|
|
467
|
+
* @param {Object} [options] - Configuration
|
|
468
|
+
* @param {string} [options.namespace] - Namespace prefix
|
|
469
|
+
*/
|
|
470
|
+
constructor(options = {}) {
|
|
471
|
+
/** @type {string} */
|
|
472
|
+
this.type = 'database';
|
|
473
|
+
/** @type {string} */
|
|
474
|
+
this.namespace = options.namespace || 'probe';
|
|
475
|
+
/** @type {Map<string, any>} */
|
|
476
|
+
this._data = new Map();
|
|
477
|
+
/** @type {Map<string, Map<string, Set<string>>>} */
|
|
478
|
+
this._indices = new Map();
|
|
479
|
+
/** @type {Map<string, number>} */
|
|
480
|
+
this._timestamps = new Map();
|
|
481
|
+
/** @type {number} */
|
|
482
|
+
this._version = 0;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Create index on field
|
|
487
|
+
* @param {string} field - Field name to index
|
|
488
|
+
* @returns {void}
|
|
489
|
+
*/
|
|
490
|
+
createIndex(field) {
|
|
491
|
+
if (!this._indices.has(field)) {
|
|
492
|
+
this._indices.set(field, new Map());
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Update indices for a record
|
|
498
|
+
* @param {string} key - Record key
|
|
499
|
+
* @param {any} value - Record value
|
|
500
|
+
* @private
|
|
501
|
+
*/
|
|
502
|
+
_updateIndices(key, value) {
|
|
503
|
+
if (typeof value !== 'object' || value === null) return;
|
|
504
|
+
|
|
505
|
+
for (const [field, index] of this._indices) {
|
|
506
|
+
const fieldValue = value[field];
|
|
507
|
+
if (fieldValue !== undefined) {
|
|
508
|
+
const strValue = String(fieldValue);
|
|
509
|
+
if (!index.has(strValue)) {
|
|
510
|
+
index.set(strValue, new Set());
|
|
511
|
+
}
|
|
512
|
+
index.get(strValue).add(key);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Remove from indices
|
|
519
|
+
* @param {string} key - Record key
|
|
520
|
+
* @private
|
|
521
|
+
*/
|
|
522
|
+
_removeFromIndices(key) {
|
|
523
|
+
for (const index of this._indices.values()) {
|
|
524
|
+
for (const keySet of index.values()) {
|
|
525
|
+
keySet.delete(key);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Set a value with key
|
|
532
|
+
* @param {string} key - Storage key
|
|
533
|
+
* @param {any} value - Value to store
|
|
534
|
+
* @returns {Promise<void>}
|
|
535
|
+
*/
|
|
536
|
+
async set(key, value) {
|
|
537
|
+
if (typeof key !== 'string' || key.length === 0) {
|
|
538
|
+
throw new Error('Key must be a non-empty string');
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const prefixedKey = `${this.namespace}:${key}`;
|
|
542
|
+
this._removeFromIndices(prefixedKey);
|
|
543
|
+
this._data.set(prefixedKey, structuredClone(value));
|
|
544
|
+
this._timestamps.set(prefixedKey, Date.now());
|
|
545
|
+
this._updateIndices(prefixedKey, value);
|
|
546
|
+
this._version++;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Get a value by key
|
|
551
|
+
* @param {string} key - Storage key
|
|
552
|
+
* @returns {Promise<any>}
|
|
553
|
+
*/
|
|
554
|
+
async get(key) {
|
|
555
|
+
const prefixedKey = `${this.namespace}:${key}`;
|
|
556
|
+
if (!this._data.has(prefixedKey)) {
|
|
557
|
+
return undefined;
|
|
558
|
+
}
|
|
559
|
+
return structuredClone(this._data.get(prefixedKey));
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Delete a value by key
|
|
564
|
+
* @param {string} key - Storage key
|
|
565
|
+
* @returns {Promise<boolean>}
|
|
566
|
+
*/
|
|
567
|
+
async delete(key) {
|
|
568
|
+
const prefixedKey = `${this.namespace}:${key}`;
|
|
569
|
+
this._removeFromIndices(prefixedKey);
|
|
570
|
+
this._timestamps.delete(prefixedKey);
|
|
571
|
+
this._version++;
|
|
572
|
+
return this._data.delete(prefixedKey);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Query values by pattern or criteria
|
|
577
|
+
* @param {string|Object} patternOrCriteria - Pattern string or criteria object
|
|
578
|
+
* @returns {Promise<Array<{key: string, value: any}>>}
|
|
579
|
+
*/
|
|
580
|
+
async query(patternOrCriteria) {
|
|
581
|
+
const results = [];
|
|
582
|
+
const prefix = `${this.namespace}:`;
|
|
583
|
+
|
|
584
|
+
if (typeof patternOrCriteria === 'string') {
|
|
585
|
+
// Pattern query
|
|
586
|
+
const regex = new RegExp('^' + prefix + patternOrCriteria.replace(/\*/g, '.*') + '$');
|
|
587
|
+
for (const [key, value] of this._data) {
|
|
588
|
+
if (regex.test(key)) {
|
|
589
|
+
results.push({
|
|
590
|
+
key: key.replace(prefix, ''),
|
|
591
|
+
value: structuredClone(value)
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
} else if (typeof patternOrCriteria === 'object') {
|
|
596
|
+
// Criteria query
|
|
597
|
+
const criteria = patternOrCriteria;
|
|
598
|
+
|
|
599
|
+
// Check if we can use an index
|
|
600
|
+
let candidateKeys = null;
|
|
601
|
+
for (const [field, value] of Object.entries(criteria)) {
|
|
602
|
+
if (this._indices.has(field)) {
|
|
603
|
+
const index = this._indices.get(field);
|
|
604
|
+
const matchingKeys = index.get(String(value));
|
|
605
|
+
if (matchingKeys) {
|
|
606
|
+
if (candidateKeys === null) {
|
|
607
|
+
candidateKeys = new Set(matchingKeys);
|
|
608
|
+
} else {
|
|
609
|
+
// Intersection
|
|
610
|
+
candidateKeys = new Set([...candidateKeys].filter(k => matchingKeys.has(k)));
|
|
611
|
+
}
|
|
612
|
+
} else {
|
|
613
|
+
candidateKeys = new Set();
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Filter candidates or scan all
|
|
620
|
+
const keysToCheck = candidateKeys || this._data.keys();
|
|
621
|
+
for (const key of keysToCheck) {
|
|
622
|
+
const value = this._data.get(key);
|
|
623
|
+
if (!value) continue;
|
|
624
|
+
|
|
625
|
+
let matches = true;
|
|
626
|
+
for (const [field, expected] of Object.entries(criteria)) {
|
|
627
|
+
if (value[field] !== expected) {
|
|
628
|
+
matches = false;
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (matches) {
|
|
634
|
+
results.push({
|
|
635
|
+
key: key.replace(prefix, ''),
|
|
636
|
+
value: structuredClone(value)
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return results;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* List all keys
|
|
647
|
+
* @returns {Promise<string[]>}
|
|
648
|
+
*/
|
|
649
|
+
async keys() {
|
|
650
|
+
const prefix = `${this.namespace}:`;
|
|
651
|
+
return Array.from(this._data.keys())
|
|
652
|
+
.filter(k => k.startsWith(prefix))
|
|
653
|
+
.map(k => k.replace(prefix, ''));
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Count entries
|
|
658
|
+
* @returns {Promise<number>}
|
|
659
|
+
*/
|
|
660
|
+
async count() {
|
|
661
|
+
const keys = await this.keys();
|
|
662
|
+
return keys.length;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Clear all entries
|
|
667
|
+
* @returns {Promise<void>}
|
|
668
|
+
*/
|
|
669
|
+
async clear() {
|
|
670
|
+
const prefix = `${this.namespace}:`;
|
|
671
|
+
for (const key of this._data.keys()) {
|
|
672
|
+
if (key.startsWith(prefix)) {
|
|
673
|
+
this._data.delete(key);
|
|
674
|
+
this._timestamps.delete(key);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
for (const index of this._indices.values()) {
|
|
678
|
+
index.clear();
|
|
679
|
+
}
|
|
680
|
+
this._version++;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Check if key exists
|
|
685
|
+
* @param {string} key - Storage key
|
|
686
|
+
* @returns {Promise<boolean>}
|
|
687
|
+
*/
|
|
688
|
+
async has(key) {
|
|
689
|
+
const prefixedKey = `${this.namespace}:${key}`;
|
|
690
|
+
return this._data.has(prefixedKey);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Get database version (for change detection)
|
|
695
|
+
* @returns {number}
|
|
696
|
+
*/
|
|
697
|
+
getVersion() {
|
|
698
|
+
return this._version;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Batch set multiple values
|
|
703
|
+
* @param {Array<{key: string, value: any}>} entries - Entries to set
|
|
704
|
+
* @returns {Promise<void>}
|
|
705
|
+
*/
|
|
706
|
+
async batchSet(entries) {
|
|
707
|
+
for (const { key, value } of entries) {
|
|
708
|
+
await this.set(key, value);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Batch get multiple values
|
|
714
|
+
* @param {string[]} keys - Keys to get
|
|
715
|
+
* @returns {Promise<Map<string, any>>}
|
|
716
|
+
*/
|
|
717
|
+
async batchGet(keys) {
|
|
718
|
+
const results = new Map();
|
|
719
|
+
for (const key of keys) {
|
|
720
|
+
results.set(key, await this.get(key));
|
|
721
|
+
}
|
|
722
|
+
return results;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Artifact API (backward compatibility)
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Save artifact
|
|
729
|
+
* @param {Object} artifact - Artifact to save
|
|
730
|
+
* @returns {Promise<void>}
|
|
731
|
+
*/
|
|
732
|
+
async saveArtifact(artifact) {
|
|
733
|
+
const key = artifact.probe_run_id || artifact.id || randomUUID();
|
|
734
|
+
await this.set(`artifact:${key}`, artifact);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Load artifact
|
|
739
|
+
* @param {string} artifactId - Artifact ID
|
|
740
|
+
* @returns {Promise<Object>}
|
|
741
|
+
*/
|
|
742
|
+
async loadArtifact(artifactId) {
|
|
743
|
+
const artifact = await this.get(`artifact:${artifactId}`);
|
|
744
|
+
if (!artifact) {
|
|
745
|
+
throw new Error(`Artifact not found: ${artifactId}`);
|
|
746
|
+
}
|
|
747
|
+
return artifact;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Fetch all artifacts
|
|
752
|
+
* @returns {Promise<Array>}
|
|
753
|
+
*/
|
|
754
|
+
async fetchShards() {
|
|
755
|
+
const results = await this.query('artifact:*');
|
|
756
|
+
return results.map(r => r.value);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* List artifact IDs
|
|
761
|
+
* @returns {Promise<string[]>}
|
|
762
|
+
*/
|
|
763
|
+
async listArtifacts() {
|
|
764
|
+
const keys = await this.keys();
|
|
765
|
+
return keys
|
|
766
|
+
.filter(k => k.startsWith('artifact:'))
|
|
767
|
+
.map(k => k.replace('artifact:', ''));
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Delete artifact
|
|
772
|
+
* @param {string} artifactId - Artifact ID
|
|
773
|
+
* @returns {Promise<boolean>}
|
|
774
|
+
*/
|
|
775
|
+
async deleteArtifact(artifactId) {
|
|
776
|
+
return this.delete(`artifact:${artifactId}`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// ============================================================================
|
|
781
|
+
// FACTORY FUNCTIONS
|
|
782
|
+
// ============================================================================
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Create memory storage
|
|
786
|
+
* @returns {MemoryStorage}
|
|
787
|
+
*/
|
|
788
|
+
export function createMemoryStorage() {
|
|
789
|
+
return new MemoryStorage();
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Create file storage
|
|
794
|
+
* @param {string} [rootDir] - Root directory
|
|
795
|
+
* @returns {FileStorage}
|
|
796
|
+
*/
|
|
797
|
+
export function createFileStorage(rootDir = './storage') {
|
|
798
|
+
return new FileStorage(rootDir);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Create database storage
|
|
803
|
+
* @param {Object} [options] - Configuration
|
|
804
|
+
* @returns {DatabaseStorage}
|
|
805
|
+
*/
|
|
806
|
+
export function createDatabaseStorage(options = {}) {
|
|
807
|
+
return new DatabaseStorage(options);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Create storage by type
|
|
812
|
+
* @param {'memory' | 'file' | 'database'} type - Storage type
|
|
813
|
+
* @param {Object} [options] - Configuration
|
|
814
|
+
* @returns {MemoryStorage | FileStorage | DatabaseStorage}
|
|
815
|
+
*/
|
|
816
|
+
export function createStorage(type, options = {}) {
|
|
817
|
+
switch (type) {
|
|
818
|
+
case 'memory':
|
|
819
|
+
return createMemoryStorage();
|
|
820
|
+
case 'file':
|
|
821
|
+
return createFileStorage(options.rootDir);
|
|
822
|
+
case 'database':
|
|
823
|
+
return createDatabaseStorage(options);
|
|
824
|
+
default:
|
|
825
|
+
throw new Error(`Unknown storage type: ${type}`);
|
|
826
|
+
}
|
|
827
|
+
}
|