@unrdf/knowledge-engine 5.0.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/LICENSE +21 -0
- package/README.md +84 -0
- package/package.json +64 -0
- package/src/browser-shims.mjs +343 -0
- package/src/browser.mjs +910 -0
- package/src/canonicalize.mjs +414 -0
- package/src/condition-cache.mjs +109 -0
- package/src/condition-evaluator.mjs +722 -0
- package/src/dark-matter-core.mjs +742 -0
- package/src/define-hook.mjs +213 -0
- package/src/effect-sandbox-browser.mjs +283 -0
- package/src/effect-sandbox-worker.mjs +170 -0
- package/src/effect-sandbox.mjs +517 -0
- package/src/engines/index.mjs +11 -0
- package/src/engines/rdf-engine.mjs +299 -0
- package/src/file-resolver.mjs +387 -0
- package/src/hook-executor-batching.mjs +277 -0
- package/src/hook-executor.mjs +870 -0
- package/src/hook-management.mjs +150 -0
- package/src/index.mjs +93 -0
- package/src/ken-parliment.mjs +119 -0
- package/src/ken.mjs +149 -0
- package/src/knowledge-engine/builtin-rules.mjs +190 -0
- package/src/knowledge-engine/inference-engine.mjs +418 -0
- package/src/knowledge-engine/knowledge-engine.mjs +317 -0
- package/src/knowledge-engine/pattern-dsl.mjs +142 -0
- package/src/knowledge-engine/pattern-matcher.mjs +215 -0
- package/src/knowledge-engine/rules.mjs +184 -0
- package/src/knowledge-engine.mjs +319 -0
- package/src/knowledge-hook-engine.mjs +360 -0
- package/src/knowledge-hook-manager.mjs +469 -0
- package/src/knowledge-substrate-core.mjs +927 -0
- package/src/lite.mjs +222 -0
- package/src/lockchain-writer-browser.mjs +414 -0
- package/src/lockchain-writer.mjs +602 -0
- package/src/monitoring/andon-signals.mjs +775 -0
- package/src/observability.mjs +531 -0
- package/src/parse.mjs +290 -0
- package/src/performance-optimizer.mjs +678 -0
- package/src/policy-pack.mjs +572 -0
- package/src/query-cache.mjs +116 -0
- package/src/query-optimizer.mjs +1051 -0
- package/src/query.mjs +306 -0
- package/src/reason.mjs +350 -0
- package/src/resolution-layer.mjs +506 -0
- package/src/schemas.mjs +1063 -0
- package/src/security/error-sanitizer.mjs +257 -0
- package/src/security/path-validator.mjs +194 -0
- package/src/security/sandbox-restrictions.mjs +331 -0
- package/src/security-validator.mjs +389 -0
- package/src/store-cache.mjs +137 -0
- package/src/telemetry.mjs +167 -0
- package/src/transaction.mjs +810 -0
- package/src/utils/adaptive-monitor.mjs +746 -0
- package/src/utils/circuit-breaker.mjs +513 -0
- package/src/utils/edge-case-handler.mjs +503 -0
- package/src/utils/memory-manager.mjs +498 -0
- package/src/utils/ring-buffer.mjs +282 -0
- package/src/validate.mjs +319 -0
- package/src/validators/index.mjs +338 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Canonicalization and isomorphism checks for RDF graphs.
|
|
3
|
+
* @module canonicalize
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import rdfCanonize from 'rdf-canonize';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Canonicalize a store into URDNA2015 canonical N-Quads.
|
|
10
|
+
* @param {import('n3').Store} store - The store to canonicalize
|
|
11
|
+
* @param {Object} [options] - Canonicalization options
|
|
12
|
+
* @param {string} [options.algorithm='URDNA2015'] - Canonicalization algorithm
|
|
13
|
+
* @param {boolean} [options.produceGeneralizedRdf=false] - Produce generalized RDF
|
|
14
|
+
* @param {number} [options.timeoutMs=30000] - Timeout in milliseconds
|
|
15
|
+
* @returns {Promise<string>} Promise resolving to canonical N-Quads string
|
|
16
|
+
*
|
|
17
|
+
* @throws {Error} If canonicalization fails
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* const store = new Store();
|
|
21
|
+
* // ... add quads to store
|
|
22
|
+
*
|
|
23
|
+
* const canonical = await canonicalize(store);
|
|
24
|
+
* console.log('Canonical N-Quads:', canonical);
|
|
25
|
+
*/
|
|
26
|
+
export async function canonicalize(store, options = {}) {
|
|
27
|
+
if (!store || typeof store.getQuads !== 'function') {
|
|
28
|
+
throw new TypeError('canonicalize: store must be a valid Store instance');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { algorithm = 'URDNA2015', produceGeneralizedRdf = false, timeoutMs = 30000 } = options;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// Get quads from store and validate
|
|
35
|
+
const quads = store.getQuads();
|
|
36
|
+
if (!Array.isArray(quads)) {
|
|
37
|
+
throw new TypeError('store.getQuads() must return an array');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// If store is empty, return empty canonical form
|
|
41
|
+
if (quads.length === 0) {
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Convert store to N-Quads format using Oxigraph dump
|
|
46
|
+
const nquads = store.dump({ format: 'application/n-quads' });
|
|
47
|
+
|
|
48
|
+
// Validate nquads output
|
|
49
|
+
if (typeof nquads !== 'string' || nquads.trim().length === 0) {
|
|
50
|
+
throw new TypeError('Serialization produced empty or invalid N-Quads');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Parse N-Quads string to RDF.js dataset format
|
|
54
|
+
// rdf-canonize expects an array of quad objects, not a string
|
|
55
|
+
const parsedDataset = rdfCanonize.NQuads.parse(nquads);
|
|
56
|
+
|
|
57
|
+
// Set up timeout
|
|
58
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
59
|
+
setTimeout(() => reject(new Error('Canonicalization timeout')), timeoutMs);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Perform canonicalization with parsed dataset
|
|
63
|
+
const canonicalPromise = rdfCanonize.canonize(parsedDataset, {
|
|
64
|
+
algorithm,
|
|
65
|
+
produceGeneralizedRdf,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return await Promise.race([canonicalPromise, timeoutPromise]);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
throw new Error(`Canonicalization failed: ${error.message}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Check if two stores are isomorphic (logically equivalent).
|
|
76
|
+
* @param {import('n3').Store} storeA - First store to compare
|
|
77
|
+
* @param {import('n3').Store} storeB - Second store to compare
|
|
78
|
+
* @param {Object} [options] - Comparison options
|
|
79
|
+
* @param {string} [options.algorithm='URDNA2015'] - Canonicalization algorithm
|
|
80
|
+
* @param {number} [options.timeoutMs=30000] - Timeout in milliseconds
|
|
81
|
+
* @returns {Promise<boolean>} Promise resolving to true if stores are isomorphic
|
|
82
|
+
*
|
|
83
|
+
* @throws {Error} If comparison fails
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* const store1 = new Store();
|
|
87
|
+
* const store2 = new Store();
|
|
88
|
+
* // ... add quads to both stores
|
|
89
|
+
*
|
|
90
|
+
* const isomorphic = await isIsomorphic(store1, store2);
|
|
91
|
+
* console.log('Stores are isomorphic:', isomorphic);
|
|
92
|
+
*/
|
|
93
|
+
export async function isIsomorphic(storeA, storeB, options = {}) {
|
|
94
|
+
if (!storeA || typeof storeA.getQuads !== 'function') {
|
|
95
|
+
throw new TypeError('isIsomorphic: storeA must be a valid Store instance');
|
|
96
|
+
}
|
|
97
|
+
if (!storeB || typeof storeB.getQuads !== 'function') {
|
|
98
|
+
throw new TypeError('isIsomorphic: storeB must be a valid Store instance');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
// Quick size check first
|
|
103
|
+
if (storeA.size !== storeB.size) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// If both stores are empty, they are isomorphic
|
|
108
|
+
if (storeA.size === 0) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Canonicalize both stores and compare
|
|
113
|
+
const [canonicalA, canonicalB] = await Promise.all([
|
|
114
|
+
canonicalize(storeA, options),
|
|
115
|
+
canonicalize(storeB, options),
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
return canonicalA === canonicalB;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
throw new Error(`Isomorphism check failed: ${error.message}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get canonical hash of a store.
|
|
126
|
+
* @param {import('n3').Store} store - The store to hash
|
|
127
|
+
* @param {Object} [options] - Hashing options
|
|
128
|
+
* @param {string} [options.hashAlgorithm='SHA-256'] - Hash algorithm
|
|
129
|
+
* @param {string} [options.algorithm='URDNA2015'] - Canonicalization algorithm
|
|
130
|
+
* @returns {Promise<string>} Promise resolving to hexadecimal hash string
|
|
131
|
+
*
|
|
132
|
+
* @throws {Error} If hashing fails
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* const store = new Store();
|
|
136
|
+
* // ... add quads to store
|
|
137
|
+
*
|
|
138
|
+
* const hash = await getCanonicalHash(store);
|
|
139
|
+
* console.log('Canonical hash:', hash);
|
|
140
|
+
*/
|
|
141
|
+
export async function getCanonicalHash(store, options = {}) {
|
|
142
|
+
if (!store || typeof store.getQuads !== 'function') {
|
|
143
|
+
throw new TypeError('getCanonicalHash: store must be a valid Store instance');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const { hashAlgorithm = 'SHA-256', algorithm = 'URDNA2015' } = options;
|
|
147
|
+
|
|
148
|
+
// Normalize hash algorithm name for Web Crypto API
|
|
149
|
+
const normalizeHashAlgorithm = alg => {
|
|
150
|
+
const normalized = alg.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
151
|
+
const map = {
|
|
152
|
+
sha1: 'SHA-1',
|
|
153
|
+
sha256: 'SHA-256',
|
|
154
|
+
sha384: 'SHA-384',
|
|
155
|
+
sha512: 'SHA-512',
|
|
156
|
+
};
|
|
157
|
+
return map[normalized] || alg;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const canonical = await canonicalize(store, { algorithm });
|
|
162
|
+
|
|
163
|
+
// Use Web Crypto API if available, otherwise fall back to Node.js crypto
|
|
164
|
+
if (typeof crypto !== 'undefined' && crypto.subtle) {
|
|
165
|
+
const encoder = new TextEncoder();
|
|
166
|
+
const data = encoder.encode(canonical);
|
|
167
|
+
const webCryptoAlg = normalizeHashAlgorithm(hashAlgorithm);
|
|
168
|
+
const hashBuffer = await crypto.subtle.digest(webCryptoAlg, data);
|
|
169
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
170
|
+
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
171
|
+
} else {
|
|
172
|
+
// Node.js fallback
|
|
173
|
+
const crypto = await import('node:crypto');
|
|
174
|
+
const nodeCryptoAlg = hashAlgorithm.toLowerCase().replace('-', '');
|
|
175
|
+
const hash = crypto.createHash(nodeCryptoAlg);
|
|
176
|
+
hash.update(canonical);
|
|
177
|
+
return hash.digest('hex');
|
|
178
|
+
}
|
|
179
|
+
} catch (error) {
|
|
180
|
+
throw new Error(`Canonical hashing failed: ${error.message}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Compare multiple stores and group them by isomorphism.
|
|
186
|
+
* @param {Array<import('n3').Store>} stores - Array of stores to compare
|
|
187
|
+
* @param {Object} [options] - Comparison options
|
|
188
|
+
* @returns {Promise<Array<Object>>} Promise resolving to array of group objects with stores property
|
|
189
|
+
*
|
|
190
|
+
* @throws {Error} If comparison fails
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* const stores = [store1, store2, store3, store4];
|
|
194
|
+
* const groups = await groupByIsomorphism(stores);
|
|
195
|
+
* console.log('Isomorphic groups:', groups);
|
|
196
|
+
* // Output: [{stores: [store1, store3]}, {stores: [store2]}, {stores: [store4]}]
|
|
197
|
+
*/
|
|
198
|
+
export async function groupByIsomorphism(stores, options = {}) {
|
|
199
|
+
if (!Array.isArray(stores)) {
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (stores.length === 0) {
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Validate all stores are valid before processing
|
|
208
|
+
for (let i = 0; i < stores.length; i++) {
|
|
209
|
+
if (!stores[i] || typeof stores[i].getQuads !== 'function') {
|
|
210
|
+
throw new TypeError(`groupByIsomorphism: store at index ${i} must be a valid Store instance`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const groups = [];
|
|
216
|
+
const processed = new Set();
|
|
217
|
+
|
|
218
|
+
for (let i = 0; i < stores.length; i++) {
|
|
219
|
+
if (processed.has(i)) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const currentGroupStores = [stores[i]];
|
|
224
|
+
processed.add(i);
|
|
225
|
+
|
|
226
|
+
for (let j = i + 1; j < stores.length; j++) {
|
|
227
|
+
if (processed.has(j)) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const isomorphic = await isIsomorphic(stores[i], stores[j], options);
|
|
233
|
+
if (isomorphic) {
|
|
234
|
+
currentGroupStores.push(stores[j]);
|
|
235
|
+
processed.add(j);
|
|
236
|
+
}
|
|
237
|
+
} catch (error) {
|
|
238
|
+
// If comparison fails, treat as non-isomorphic
|
|
239
|
+
console.warn(`Failed to compare stores ${i} and ${j}: ${error.message}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
groups.push({ stores: currentGroupStores });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return groups;
|
|
247
|
+
} catch (error) {
|
|
248
|
+
throw new Error(`Isomorphism grouping failed: ${error.message}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Find duplicate stores in an array.
|
|
254
|
+
* @param {Array<import('n3').Store>} stores - Array of stores to check
|
|
255
|
+
* @param {Object} [options] - Comparison options
|
|
256
|
+
* @returns {Promise<Array<Object>>} Promise resolving to array of duplicate objects with stores and canonicalHash
|
|
257
|
+
*
|
|
258
|
+
* @throws {Error} If comparison fails
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* const stores = [store1, store2, store3];
|
|
262
|
+
* const duplicates = await findDuplicates(stores);
|
|
263
|
+
* if (duplicates.length > 0) {
|
|
264
|
+
* console.log('Found duplicate stores:', duplicates);
|
|
265
|
+
* }
|
|
266
|
+
*/
|
|
267
|
+
export async function findDuplicates(stores, options = {}) {
|
|
268
|
+
const groups = await groupByIsomorphism(stores, options);
|
|
269
|
+
const duplicates = [];
|
|
270
|
+
|
|
271
|
+
for (const group of groups) {
|
|
272
|
+
if (group.stores.length > 1) {
|
|
273
|
+
// Get canonical hash for the group
|
|
274
|
+
const hash = await getCanonicalHash(group.stores[0], options);
|
|
275
|
+
duplicates.push({
|
|
276
|
+
stores: group.stores,
|
|
277
|
+
canonicalHash: hash,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return duplicates;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Get canonicalization statistics.
|
|
287
|
+
* @param {import('n3').Store} store - The store to analyze
|
|
288
|
+
* @param {Object} [options] - Analysis options
|
|
289
|
+
* @returns {Promise<Object>} Promise resolving to canonicalization statistics
|
|
290
|
+
*
|
|
291
|
+
* @example
|
|
292
|
+
* const stats = await getCanonicalizationStats(store);
|
|
293
|
+
* console.log('Store size:', stats.quads);
|
|
294
|
+
* console.log('Canonical size:', stats.canonicalLength);
|
|
295
|
+
* console.log('Time:', stats.canonicalizationTime);
|
|
296
|
+
*/
|
|
297
|
+
export async function getCanonicalizationStats(store, options = {}) {
|
|
298
|
+
if (!store || typeof store.getQuads !== 'function') {
|
|
299
|
+
throw new TypeError('getCanonicalizationStats: store must be a valid Store instance');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const { algorithm = 'URDNA2015' } = options;
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
const startTime = performance.now();
|
|
306
|
+
const canonical = await canonicalize(store, { algorithm });
|
|
307
|
+
const endTime = performance.now();
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
quads: store.size,
|
|
311
|
+
canonicalLength: canonical.length,
|
|
312
|
+
canonicalizationTime: Math.max(endTime - startTime, 0.01), // Ensure non-zero time
|
|
313
|
+
algorithm,
|
|
314
|
+
};
|
|
315
|
+
} catch (error) {
|
|
316
|
+
throw new Error(`Canonicalization statistics failed: ${error.message}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Create a canonicalization session for batch operations.
|
|
322
|
+
* @param {Object} [options] - Session options
|
|
323
|
+
* @returns {Promise<Object>} Canonicalization session
|
|
324
|
+
*
|
|
325
|
+
* @example
|
|
326
|
+
* const session = await createCanonicalizationSession();
|
|
327
|
+
*
|
|
328
|
+
* // Direct canonicalization
|
|
329
|
+
* const canonical = await session.canonicalize(store);
|
|
330
|
+
*
|
|
331
|
+
* // Isomorphism check
|
|
332
|
+
* const isomorphic = await session.isIsomorphic(store1, store2);
|
|
333
|
+
*
|
|
334
|
+
* // Get canonical hash
|
|
335
|
+
* const hash = await session.getCanonicalHash(store);
|
|
336
|
+
*
|
|
337
|
+
* // Get session statistics
|
|
338
|
+
* const stats = session.getStats();
|
|
339
|
+
*/
|
|
340
|
+
export async function createCanonicalizationSession(options = {}) {
|
|
341
|
+
const sessionOptions = options;
|
|
342
|
+
let canonicalizationCount = 0;
|
|
343
|
+
let isomorphismCheckCount = 0;
|
|
344
|
+
let totalTime = 0;
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
/**
|
|
348
|
+
* Canonicalize a store.
|
|
349
|
+
* @param {import('n3').Store} store - Store to canonicalize
|
|
350
|
+
* @returns {Promise<string>} Canonical N-Quads
|
|
351
|
+
*/
|
|
352
|
+
async canonicalize(store) {
|
|
353
|
+
const startTime = Date.now();
|
|
354
|
+
try {
|
|
355
|
+
const result = await canonicalize(store, sessionOptions);
|
|
356
|
+
canonicalizationCount++;
|
|
357
|
+
totalTime += Date.now() - startTime;
|
|
358
|
+
return result;
|
|
359
|
+
} catch (error) {
|
|
360
|
+
totalTime += Date.now() - startTime;
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Check if two stores are isomorphic.
|
|
367
|
+
* @param {import('n3').Store} storeA - First store
|
|
368
|
+
* @param {import('n3').Store} storeB - Second store
|
|
369
|
+
* @returns {Promise<boolean>} True if isomorphic
|
|
370
|
+
*/
|
|
371
|
+
async isIsomorphic(storeA, storeB) {
|
|
372
|
+
const startTime = Date.now();
|
|
373
|
+
try {
|
|
374
|
+
const result = await isIsomorphic(storeA, storeB, sessionOptions);
|
|
375
|
+
isomorphismCheckCount++;
|
|
376
|
+
totalTime += Date.now() - startTime;
|
|
377
|
+
return result;
|
|
378
|
+
} catch (error) {
|
|
379
|
+
totalTime += Date.now() - startTime;
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Get canonical hash of a store.
|
|
386
|
+
* @param {import('n3').Store} store - Store to hash
|
|
387
|
+
* @returns {Promise<string>} Canonical hash
|
|
388
|
+
*/
|
|
389
|
+
async getCanonicalHash(store) {
|
|
390
|
+
const startTime = Date.now();
|
|
391
|
+
try {
|
|
392
|
+
const result = await getCanonicalHash(store, sessionOptions);
|
|
393
|
+
canonicalizationCount++;
|
|
394
|
+
totalTime += Date.now() - startTime;
|
|
395
|
+
return result;
|
|
396
|
+
} catch (error) {
|
|
397
|
+
totalTime += Date.now() - startTime;
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Get session statistics.
|
|
404
|
+
* @returns {Object} Session statistics
|
|
405
|
+
*/
|
|
406
|
+
getStats() {
|
|
407
|
+
return {
|
|
408
|
+
canonicalizations: canonicalizationCount,
|
|
409
|
+
isomorphismChecks: isomorphismCheckCount,
|
|
410
|
+
totalTime,
|
|
411
|
+
};
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Condition Cache - Eliminates duplicate condition evaluation
|
|
3
|
+
*
|
|
4
|
+
* @description
|
|
5
|
+
* Caches SPARQL-ASK condition evaluation results by (hookId, storeVersion)
|
|
6
|
+
* to eliminate redundant re-evaluation of the same condition.
|
|
7
|
+
*
|
|
8
|
+
* Cache Strategy:
|
|
9
|
+
* - Key: `${hookId}-${storeVersion}`
|
|
10
|
+
* - Value: boolean (condition satisfied)
|
|
11
|
+
* - TTL: 60s default (invalidates on store version change)
|
|
12
|
+
* - Invalidation: Manual clear on store version change
|
|
13
|
+
*
|
|
14
|
+
* Expected Impact: 40-50% latency reduction
|
|
15
|
+
*
|
|
16
|
+
* @module knowledge-engine/condition-cache
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Condition Cache - Caches SPARQL condition evaluation results
|
|
21
|
+
*
|
|
22
|
+
* @class ConditionCache
|
|
23
|
+
*/
|
|
24
|
+
export class ConditionCache {
|
|
25
|
+
/**
|
|
26
|
+
* Create a new condition cache
|
|
27
|
+
* @param {object} options - Configuration options
|
|
28
|
+
* @param {number} options.ttl - Time-to-live in milliseconds (default: 60000)
|
|
29
|
+
*/
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
this.cache = new Map(); // `${hookId}-${storeVersion}` → { value, timestamp }
|
|
32
|
+
this.ttl = options.ttl || 60000; // 60 seconds default
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get cached condition result
|
|
37
|
+
*
|
|
38
|
+
* Returns undefined if:
|
|
39
|
+
* - Entry doesn't exist
|
|
40
|
+
* - Entry has expired (TTL exceeded)
|
|
41
|
+
*
|
|
42
|
+
* @param {string} hookId - Hook identifier
|
|
43
|
+
* @param {string} storeVersion - Store version hash
|
|
44
|
+
* @returns {boolean|undefined} Cached result or undefined
|
|
45
|
+
*/
|
|
46
|
+
get(hookId, storeVersion) {
|
|
47
|
+
if (!hookId || !storeVersion) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const key = `${hookId}-${storeVersion}`;
|
|
52
|
+
const entry = this.cache.get(key);
|
|
53
|
+
|
|
54
|
+
if (!entry) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// TTL check - if expired, remove and return undefined
|
|
59
|
+
if (Date.now() - entry.timestamp > this.ttl) {
|
|
60
|
+
this.cache.delete(key);
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return entry.value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Store condition result in cache
|
|
69
|
+
*
|
|
70
|
+
* @param {string} hookId - Hook identifier
|
|
71
|
+
* @param {string} storeVersion - Store version hash
|
|
72
|
+
* @param {boolean} value - Evaluation result (true/false)
|
|
73
|
+
*/
|
|
74
|
+
set(hookId, storeVersion, value) {
|
|
75
|
+
if (!hookId || !storeVersion) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const key = `${hookId}-${storeVersion}`;
|
|
80
|
+
this.cache.set(key, {
|
|
81
|
+
value: Boolean(value),
|
|
82
|
+
timestamp: Date.now(),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Clear entire cache (call on store version change)
|
|
88
|
+
*
|
|
89
|
+
* This should be called whenever the store is modified
|
|
90
|
+
* (delta applied, new transaction, etc.)
|
|
91
|
+
*/
|
|
92
|
+
clear() {
|
|
93
|
+
this.cache.clear();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get cache statistics
|
|
98
|
+
* @returns {object} Cache stats (size, ttl, entries)
|
|
99
|
+
*/
|
|
100
|
+
stats() {
|
|
101
|
+
return {
|
|
102
|
+
size: this.cache.size,
|
|
103
|
+
ttl: this.ttl,
|
|
104
|
+
entries: Array.from(this.cache.keys()),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default ConditionCache;
|