@unrdf/kgc-runtime 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/IMPLEMENTATION_SUMMARY.json +150 -0
- package/PLUGIN_SYSTEM_SUMMARY.json +149 -0
- package/README.md +98 -0
- package/TRANSACTION_IMPLEMENTATION.json +119 -0
- package/capability-map.md +93 -0
- package/docs/api-stability.md +269 -0
- package/docs/extensions/plugin-development.md +382 -0
- package/package.json +40 -0
- package/plugins/registry.json +35 -0
- package/src/admission-gate.mjs +414 -0
- package/src/api-version.mjs +373 -0
- package/src/atomic-admission.mjs +310 -0
- package/src/bounds.mjs +289 -0
- package/src/bulkhead-manager.mjs +280 -0
- package/src/capsule.mjs +524 -0
- package/src/crdt.mjs +361 -0
- package/src/enhanced-bounds.mjs +614 -0
- package/src/executor.mjs +73 -0
- package/src/freeze-restore.mjs +521 -0
- package/src/index.mjs +62 -0
- package/src/materialized-views.mjs +371 -0
- package/src/merge.mjs +472 -0
- package/src/plugin-isolation.mjs +392 -0
- package/src/plugin-manager.mjs +441 -0
- package/src/projections-api.mjs +336 -0
- package/src/projections-cli.mjs +238 -0
- package/src/projections-docs.mjs +300 -0
- package/src/projections-ide.mjs +278 -0
- package/src/receipt.mjs +340 -0
- package/src/rollback.mjs +258 -0
- package/src/saga-orchestrator.mjs +355 -0
- package/src/schemas.mjs +1330 -0
- package/src/storage-optimization.mjs +359 -0
- package/src/tool-registry.mjs +272 -0
- package/src/transaction.mjs +466 -0
- package/src/validators.mjs +485 -0
- package/src/work-item.mjs +449 -0
- package/templates/plugin-template/README.md +58 -0
- package/templates/plugin-template/index.mjs +162 -0
- package/templates/plugin-template/plugin.json +19 -0
- package/test/admission-gate.test.mjs +583 -0
- package/test/api-version.test.mjs +74 -0
- package/test/atomic-admission.test.mjs +155 -0
- package/test/bounds.test.mjs +341 -0
- package/test/bulkhead-manager.test.mjs +236 -0
- package/test/capsule.test.mjs +625 -0
- package/test/crdt.test.mjs +215 -0
- package/test/enhanced-bounds.test.mjs +487 -0
- package/test/freeze-restore.test.mjs +472 -0
- package/test/materialized-views.test.mjs +243 -0
- package/test/merge.test.mjs +665 -0
- package/test/plugin-isolation.test.mjs +109 -0
- package/test/plugin-manager.test.mjs +208 -0
- package/test/projections-api.test.mjs +293 -0
- package/test/projections-cli.test.mjs +204 -0
- package/test/projections-docs.test.mjs +173 -0
- package/test/projections-ide.test.mjs +230 -0
- package/test/receipt.test.mjs +295 -0
- package/test/rollback.test.mjs +132 -0
- package/test/saga-orchestrator.test.mjs +279 -0
- package/test/schemas.test.mjs +716 -0
- package/test/storage-optimization.test.mjs +503 -0
- package/test/tool-registry.test.mjs +341 -0
- package/test/transaction.test.mjs +189 -0
- package/test/validators.test.mjs +463 -0
- package/test/work-item.test.mjs +548 -0
- package/test/work-item.test.mjs.bak +548 -0
- package/var/kgc/test-atomic-log.json +519 -0
- package/var/kgc/test-cascading-log.json +145 -0
- package/vitest.config.mjs +18 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Materialized Views - Cached SPARQL Query Results
|
|
3
|
+
* Cache query results as projections with TTL and invalidation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { generateReceipt } from './receipt.mjs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Materialized view schema
|
|
11
|
+
*/
|
|
12
|
+
export const MaterializedViewSchema = z.object({
|
|
13
|
+
id: z.string(),
|
|
14
|
+
query: z.string(),
|
|
15
|
+
projection: z.any(),
|
|
16
|
+
createdAt: z.number(),
|
|
17
|
+
expiresAt: z.number(),
|
|
18
|
+
hits: z.number().default(0),
|
|
19
|
+
lastAccessedAt: z.number().optional(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {z.infer<typeof MaterializedViewSchema>} MaterializedView
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* View cache configuration
|
|
28
|
+
* @typedef {{
|
|
29
|
+
* defaultTTL?: number,
|
|
30
|
+
* maxSize?: number,
|
|
31
|
+
* evictionPolicy?: 'lru' | 'lfu' | 'fifo',
|
|
32
|
+
* }} ViewCacheConfig
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* View cache for materialized SPARQL query results
|
|
37
|
+
*/
|
|
38
|
+
export class ViewCache {
|
|
39
|
+
/**
|
|
40
|
+
* @param {ViewCacheConfig} [config={}] - Cache configuration
|
|
41
|
+
*/
|
|
42
|
+
constructor(config = {}) {
|
|
43
|
+
this.config = {
|
|
44
|
+
defaultTTL: config.defaultTTL ?? 300000, // 5 minutes
|
|
45
|
+
maxSize: config.maxSize ?? 1000,
|
|
46
|
+
evictionPolicy: config.evictionPolicy ?? 'lru',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** @type {Map<string, MaterializedView>} */
|
|
50
|
+
this.views = new Map();
|
|
51
|
+
|
|
52
|
+
/** @type {Map<string, Set<string>>} */
|
|
53
|
+
this.dependencies = new Map();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create or update materialized view
|
|
58
|
+
* @param {string} id - View ID
|
|
59
|
+
* @param {string} query - SPARQL query
|
|
60
|
+
* @param {any} projection - Query result projection
|
|
61
|
+
* @param {object} [options] - View options
|
|
62
|
+
* @param {number} [options.ttl] - Time to live in milliseconds
|
|
63
|
+
* @param {string[]} [options.dependsOn] - Resource dependencies for invalidation
|
|
64
|
+
* @returns {MaterializedView} Created view
|
|
65
|
+
*/
|
|
66
|
+
materialize(id, query, projection, options = {}) {
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
const ttl = options.ttl ?? this.config.defaultTTL;
|
|
69
|
+
|
|
70
|
+
// Check capacity and evict if needed
|
|
71
|
+
if (this.views.size >= this.config.maxSize && !this.views.has(id)) {
|
|
72
|
+
this._evict();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const view = MaterializedViewSchema.parse({
|
|
76
|
+
id,
|
|
77
|
+
query,
|
|
78
|
+
projection,
|
|
79
|
+
createdAt: now,
|
|
80
|
+
expiresAt: now + ttl,
|
|
81
|
+
hits: 0,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
this.views.set(id, view);
|
|
85
|
+
|
|
86
|
+
// Register dependencies
|
|
87
|
+
if (options.dependsOn) {
|
|
88
|
+
for (const resource of options.dependsOn) {
|
|
89
|
+
if (!this.dependencies.has(resource)) {
|
|
90
|
+
this.dependencies.set(resource, new Set());
|
|
91
|
+
}
|
|
92
|
+
this.dependencies.get(resource).add(id);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return view;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get materialized view
|
|
101
|
+
* @param {string} id - View ID
|
|
102
|
+
* @returns {MaterializedView | null} View or null if not found/expired
|
|
103
|
+
*/
|
|
104
|
+
get(id) {
|
|
105
|
+
const view = this.views.get(id);
|
|
106
|
+
|
|
107
|
+
if (!view) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
|
|
113
|
+
// Check expiration
|
|
114
|
+
if (now >= view.expiresAt) {
|
|
115
|
+
this.views.delete(id);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Update access statistics
|
|
120
|
+
view.hits++;
|
|
121
|
+
view.lastAccessedAt = now;
|
|
122
|
+
|
|
123
|
+
return view;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get projection from view
|
|
128
|
+
* @param {string} id - View ID
|
|
129
|
+
* @returns {any | null} Projection or null
|
|
130
|
+
*/
|
|
131
|
+
getProjection(id) {
|
|
132
|
+
const view = this.get(id);
|
|
133
|
+
return view ? view.projection : null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Invalidate view by ID
|
|
138
|
+
* @param {string} id - View ID
|
|
139
|
+
* @returns {boolean} True if view was invalidated
|
|
140
|
+
*/
|
|
141
|
+
invalidate(id) {
|
|
142
|
+
return this.views.delete(id);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Invalidate views by resource dependency
|
|
147
|
+
* @param {string} resource - Resource that changed
|
|
148
|
+
* @returns {number} Number of views invalidated
|
|
149
|
+
*/
|
|
150
|
+
invalidateByResource(resource) {
|
|
151
|
+
const viewIds = this.dependencies.get(resource);
|
|
152
|
+
|
|
153
|
+
if (!viewIds) {
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let count = 0;
|
|
158
|
+
for (const viewId of viewIds) {
|
|
159
|
+
if (this.views.delete(viewId)) {
|
|
160
|
+
count++;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this.dependencies.delete(resource);
|
|
165
|
+
return count;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Invalidate all views
|
|
170
|
+
* @returns {number} Number of views invalidated
|
|
171
|
+
*/
|
|
172
|
+
invalidateAll() {
|
|
173
|
+
const count = this.views.size;
|
|
174
|
+
this.views.clear();
|
|
175
|
+
this.dependencies.clear();
|
|
176
|
+
return count;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Refresh view with new projection
|
|
181
|
+
* @param {string} id - View ID
|
|
182
|
+
* @param {any} projection - New projection
|
|
183
|
+
* @param {object} [options] - Refresh options
|
|
184
|
+
* @param {number} [options.ttl] - New TTL
|
|
185
|
+
* @returns {MaterializedView | null} Refreshed view or null if not found
|
|
186
|
+
*/
|
|
187
|
+
refresh(id, projection, options = {}) {
|
|
188
|
+
const view = this.views.get(id);
|
|
189
|
+
|
|
190
|
+
if (!view) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const now = Date.now();
|
|
195
|
+
const ttl = options.ttl ?? this.config.defaultTTL;
|
|
196
|
+
|
|
197
|
+
view.projection = projection;
|
|
198
|
+
view.createdAt = now;
|
|
199
|
+
view.expiresAt = now + ttl;
|
|
200
|
+
|
|
201
|
+
return view;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Evict view according to eviction policy
|
|
206
|
+
* @returns {string | null} Evicted view ID or null
|
|
207
|
+
* @private
|
|
208
|
+
*/
|
|
209
|
+
_evict() {
|
|
210
|
+
if (this.views.size === 0) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let victimId = null;
|
|
215
|
+
|
|
216
|
+
switch (this.config.evictionPolicy) {
|
|
217
|
+
case 'lru': // Least Recently Used
|
|
218
|
+
victimId = this._evictLRU();
|
|
219
|
+
break;
|
|
220
|
+
case 'lfu': // Least Frequently Used
|
|
221
|
+
victimId = this._evictLFU();
|
|
222
|
+
break;
|
|
223
|
+
case 'fifo': // First In First Out
|
|
224
|
+
victimId = this._evictFIFO();
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (victimId) {
|
|
229
|
+
this.views.delete(victimId);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return victimId;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Evict least recently used view
|
|
237
|
+
* @returns {string | null} Evicted view ID
|
|
238
|
+
* @private
|
|
239
|
+
*/
|
|
240
|
+
_evictLRU() {
|
|
241
|
+
let oldestTime = Infinity;
|
|
242
|
+
let victimId = null;
|
|
243
|
+
|
|
244
|
+
for (const [id, view] of this.views.entries()) {
|
|
245
|
+
const lastAccess = view.lastAccessedAt ?? view.createdAt;
|
|
246
|
+
if (lastAccess < oldestTime) {
|
|
247
|
+
oldestTime = lastAccess;
|
|
248
|
+
victimId = id;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return victimId;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Evict least frequently used view
|
|
257
|
+
* @returns {string | null} Evicted view ID
|
|
258
|
+
* @private
|
|
259
|
+
*/
|
|
260
|
+
_evictLFU() {
|
|
261
|
+
let lowestHits = Infinity;
|
|
262
|
+
let victimId = null;
|
|
263
|
+
|
|
264
|
+
for (const [id, view] of this.views.entries()) {
|
|
265
|
+
if (view.hits < lowestHits) {
|
|
266
|
+
lowestHits = view.hits;
|
|
267
|
+
victimId = id;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return victimId;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Evict oldest view (FIFO)
|
|
276
|
+
* @returns {string | null} Evicted view ID
|
|
277
|
+
* @private
|
|
278
|
+
*/
|
|
279
|
+
_evictFIFO() {
|
|
280
|
+
let oldestTime = Infinity;
|
|
281
|
+
let victimId = null;
|
|
282
|
+
|
|
283
|
+
for (const [id, view] of this.views.entries()) {
|
|
284
|
+
if (view.createdAt < oldestTime) {
|
|
285
|
+
oldestTime = view.createdAt;
|
|
286
|
+
victimId = id;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return victimId;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Clean up expired views
|
|
295
|
+
* @returns {number} Number of expired views removed
|
|
296
|
+
*/
|
|
297
|
+
cleanupExpired() {
|
|
298
|
+
const now = Date.now();
|
|
299
|
+
let count = 0;
|
|
300
|
+
|
|
301
|
+
for (const [id, view] of this.views.entries()) {
|
|
302
|
+
if (now >= view.expiresAt) {
|
|
303
|
+
this.views.delete(id);
|
|
304
|
+
count++;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return count;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Get cache statistics
|
|
313
|
+
* @returns {object} Cache statistics
|
|
314
|
+
*/
|
|
315
|
+
getStats() {
|
|
316
|
+
const now = Date.now();
|
|
317
|
+
let totalHits = 0;
|
|
318
|
+
let expired = 0;
|
|
319
|
+
|
|
320
|
+
for (const view of this.views.values()) {
|
|
321
|
+
totalHits += view.hits;
|
|
322
|
+
if (now >= view.expiresAt) {
|
|
323
|
+
expired++;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
size: this.views.size,
|
|
329
|
+
maxSize: this.config.maxSize,
|
|
330
|
+
totalHits,
|
|
331
|
+
expired,
|
|
332
|
+
evictionPolicy: this.config.evictionPolicy,
|
|
333
|
+
defaultTTL: this.config.defaultTTL,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Generate receipt for cache operation
|
|
339
|
+
* @param {string} operation - Operation name
|
|
340
|
+
* @param {Record<string, any>} inputs - Operation inputs
|
|
341
|
+
* @param {Record<string, any>} outputs - Operation outputs
|
|
342
|
+
* @returns {Promise<import('./receipt.mjs').Receipt>} Receipt
|
|
343
|
+
*/
|
|
344
|
+
async generateReceipt(operation, inputs, outputs) {
|
|
345
|
+
return generateReceipt(
|
|
346
|
+
`view-cache:${operation}`,
|
|
347
|
+
inputs,
|
|
348
|
+
{ ...outputs, stats: this.getStats() }
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Create view cache with automatic cleanup
|
|
355
|
+
* @param {ViewCacheConfig} [config] - Cache configuration
|
|
356
|
+
* @param {number} [cleanupInterval=60000] - Cleanup interval in ms
|
|
357
|
+
* @returns {ViewCache} View cache with auto-cleanup
|
|
358
|
+
*/
|
|
359
|
+
export function createAutoCleanupCache(config, cleanupInterval = 60000) {
|
|
360
|
+
const cache = new ViewCache(config);
|
|
361
|
+
|
|
362
|
+
// Set up automatic cleanup
|
|
363
|
+
const intervalId = setInterval(() => {
|
|
364
|
+
cache.cleanupExpired();
|
|
365
|
+
}, cleanupInterval);
|
|
366
|
+
|
|
367
|
+
// Allow cleanup to be stopped
|
|
368
|
+
cache.stopAutoCleanup = () => clearInterval(intervalId);
|
|
369
|
+
|
|
370
|
+
return cache;
|
|
371
|
+
}
|