@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.
Files changed (70) hide show
  1. package/IMPLEMENTATION_SUMMARY.json +150 -0
  2. package/PLUGIN_SYSTEM_SUMMARY.json +149 -0
  3. package/README.md +98 -0
  4. package/TRANSACTION_IMPLEMENTATION.json +119 -0
  5. package/capability-map.md +93 -0
  6. package/docs/api-stability.md +269 -0
  7. package/docs/extensions/plugin-development.md +382 -0
  8. package/package.json +40 -0
  9. package/plugins/registry.json +35 -0
  10. package/src/admission-gate.mjs +414 -0
  11. package/src/api-version.mjs +373 -0
  12. package/src/atomic-admission.mjs +310 -0
  13. package/src/bounds.mjs +289 -0
  14. package/src/bulkhead-manager.mjs +280 -0
  15. package/src/capsule.mjs +524 -0
  16. package/src/crdt.mjs +361 -0
  17. package/src/enhanced-bounds.mjs +614 -0
  18. package/src/executor.mjs +73 -0
  19. package/src/freeze-restore.mjs +521 -0
  20. package/src/index.mjs +62 -0
  21. package/src/materialized-views.mjs +371 -0
  22. package/src/merge.mjs +472 -0
  23. package/src/plugin-isolation.mjs +392 -0
  24. package/src/plugin-manager.mjs +441 -0
  25. package/src/projections-api.mjs +336 -0
  26. package/src/projections-cli.mjs +238 -0
  27. package/src/projections-docs.mjs +300 -0
  28. package/src/projections-ide.mjs +278 -0
  29. package/src/receipt.mjs +340 -0
  30. package/src/rollback.mjs +258 -0
  31. package/src/saga-orchestrator.mjs +355 -0
  32. package/src/schemas.mjs +1330 -0
  33. package/src/storage-optimization.mjs +359 -0
  34. package/src/tool-registry.mjs +272 -0
  35. package/src/transaction.mjs +466 -0
  36. package/src/validators.mjs +485 -0
  37. package/src/work-item.mjs +449 -0
  38. package/templates/plugin-template/README.md +58 -0
  39. package/templates/plugin-template/index.mjs +162 -0
  40. package/templates/plugin-template/plugin.json +19 -0
  41. package/test/admission-gate.test.mjs +583 -0
  42. package/test/api-version.test.mjs +74 -0
  43. package/test/atomic-admission.test.mjs +155 -0
  44. package/test/bounds.test.mjs +341 -0
  45. package/test/bulkhead-manager.test.mjs +236 -0
  46. package/test/capsule.test.mjs +625 -0
  47. package/test/crdt.test.mjs +215 -0
  48. package/test/enhanced-bounds.test.mjs +487 -0
  49. package/test/freeze-restore.test.mjs +472 -0
  50. package/test/materialized-views.test.mjs +243 -0
  51. package/test/merge.test.mjs +665 -0
  52. package/test/plugin-isolation.test.mjs +109 -0
  53. package/test/plugin-manager.test.mjs +208 -0
  54. package/test/projections-api.test.mjs +293 -0
  55. package/test/projections-cli.test.mjs +204 -0
  56. package/test/projections-docs.test.mjs +173 -0
  57. package/test/projections-ide.test.mjs +230 -0
  58. package/test/receipt.test.mjs +295 -0
  59. package/test/rollback.test.mjs +132 -0
  60. package/test/saga-orchestrator.test.mjs +279 -0
  61. package/test/schemas.test.mjs +716 -0
  62. package/test/storage-optimization.test.mjs +503 -0
  63. package/test/tool-registry.test.mjs +341 -0
  64. package/test/transaction.test.mjs +189 -0
  65. package/test/validators.test.mjs +463 -0
  66. package/test/work-item.test.mjs +548 -0
  67. package/test/work-item.test.mjs.bak +548 -0
  68. package/var/kgc/test-atomic-log.json +519 -0
  69. package/var/kgc/test-cascading-log.json +145 -0
  70. 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
+ }