@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,355 @@
1
+ /**
2
+ * @fileoverview Saga Pattern - Distributed Transaction Orchestration
3
+ * Multi-step workflows with compensation for failure recovery
4
+ */
5
+
6
+ import { z } from 'zod';
7
+ import { generateReceipt } from './receipt.mjs';
8
+
9
+ /**
10
+ * Saga step schema
11
+ */
12
+ export const SagaStepSchema = z.object({
13
+ id: z.string(),
14
+ name: z.string(),
15
+ execute: z.function().args(z.any()).returns(z.promise(z.any())),
16
+ compensate: z.function().args(z.any()).returns(z.promise(z.any())),
17
+ retryable: z.boolean().default(true),
18
+ maxRetries: z.number().nonnegative().default(3),
19
+ });
20
+
21
+ /**
22
+ * Saga configuration schema
23
+ */
24
+ export const SagaConfigSchema = z.object({
25
+ id: z.string(),
26
+ name: z.string(),
27
+ steps: z.array(SagaStepSchema),
28
+ parallel: z.boolean().default(false),
29
+ continueOnError: z.boolean().default(false),
30
+ });
31
+
32
+ /**
33
+ * @typedef {z.infer<typeof SagaStepSchema>} SagaStep
34
+ * @typedef {z.infer<typeof SagaConfigSchema>} SagaConfig
35
+ */
36
+
37
+ /**
38
+ * Saga execution state
39
+ * @typedef {{
40
+ * sagaId: string,
41
+ * status: 'pending' | 'running' | 'completed' | 'compensating' | 'compensated' | 'failed',
42
+ * currentStep: number,
43
+ * completedSteps: Array<{stepId: string, result: any}>,
44
+ * compensatedSteps: Array<{stepId: string, result: any}>,
45
+ * error?: Error,
46
+ * startTime: number,
47
+ * endTime?: number,
48
+ * }} SagaState
49
+ */
50
+
51
+ /**
52
+ * Saga orchestrator for distributed transactions
53
+ * Implements saga pattern with compensation
54
+ */
55
+ export class SagaOrchestrator {
56
+ /**
57
+ * @param {SagaConfig} config - Saga configuration
58
+ */
59
+ constructor(config) {
60
+ this.config = SagaConfigSchema.parse(config);
61
+ /** @type {Map<string, SagaState>} */
62
+ this.executions = new Map();
63
+ }
64
+
65
+ /**
66
+ * Execute saga workflow
67
+ * @param {Record<string, any>} initialContext - Initial context for execution
68
+ * @returns {Promise<{success: boolean, result?: any, error?: Error, state: SagaState}>} Execution result
69
+ */
70
+ async execute(initialContext = {}) {
71
+ const executionId = `${this.config.id}-${Date.now()}`;
72
+
73
+ /** @type {SagaState} */
74
+ const state = {
75
+ sagaId: executionId,
76
+ status: 'running',
77
+ currentStep: 0,
78
+ completedSteps: [],
79
+ compensatedSteps: [],
80
+ startTime: Date.now(),
81
+ };
82
+
83
+ this.executions.set(executionId, state);
84
+
85
+ try {
86
+ let context = { ...initialContext };
87
+
88
+ if (this.config.parallel) {
89
+ // Execute all steps in parallel
90
+ context = await this._executeParallel(state, context);
91
+ } else {
92
+ // Execute steps sequentially
93
+ context = await this._executeSequential(state, context);
94
+ }
95
+
96
+ state.status = 'completed';
97
+ state.endTime = Date.now();
98
+
99
+ return {
100
+ success: true,
101
+ result: context,
102
+ state,
103
+ };
104
+ } catch (error) {
105
+ state.error = error;
106
+ state.status = 'compensating';
107
+
108
+ try {
109
+ await this._compensate(state);
110
+ state.status = 'compensated';
111
+ } catch (compensationError) {
112
+ state.status = 'failed';
113
+ state.error = compensationError;
114
+ }
115
+
116
+ state.endTime = Date.now();
117
+
118
+ return {
119
+ success: false,
120
+ error: state.error,
121
+ state,
122
+ };
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Execute steps sequentially
128
+ * @param {SagaState} state - Saga state
129
+ * @param {Record<string, any>} context - Execution context
130
+ * @returns {Promise<Record<string, any>>} Updated context
131
+ * @private
132
+ */
133
+ async _executeSequential(state, context) {
134
+ for (let i = 0; i < this.config.steps.length; i++) {
135
+ const step = this.config.steps[i];
136
+ state.currentStep = i;
137
+
138
+ const result = await this._executeStepWithRetry(step, context);
139
+
140
+ state.completedSteps.push({
141
+ stepId: step.id,
142
+ result,
143
+ });
144
+
145
+ // Merge result into context
146
+ context = { ...context, [step.name]: result };
147
+ }
148
+
149
+ return context;
150
+ }
151
+
152
+ /**
153
+ * Execute steps in parallel
154
+ * @param {SagaState} state - Saga state
155
+ * @param {Record<string, any>} context - Execution context
156
+ * @returns {Promise<Record<string, any>>} Updated context
157
+ * @private
158
+ */
159
+ async _executeParallel(state, context) {
160
+ const results = await Promise.all(
161
+ this.config.steps.map(async (step, i) => {
162
+ const result = await this._executeStepWithRetry(step, context);
163
+ state.completedSteps.push({
164
+ stepId: step.id,
165
+ result,
166
+ });
167
+ return { name: step.name, result };
168
+ })
169
+ );
170
+
171
+ // Merge all results into context
172
+ for (const { name, result } of results) {
173
+ context[name] = result;
174
+ }
175
+
176
+ return context;
177
+ }
178
+
179
+ /**
180
+ * Execute step with retry logic
181
+ * @param {SagaStep} step - Step to execute
182
+ * @param {Record<string, any>} context - Execution context
183
+ * @returns {Promise<any>} Step result
184
+ * @private
185
+ */
186
+ async _executeStepWithRetry(step, context) {
187
+ let lastError;
188
+ const maxAttempts = step.retryable ? step.maxRetries + 1 : 1;
189
+
190
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
191
+ try {
192
+ return await step.execute(context);
193
+ } catch (error) {
194
+ lastError = error;
195
+
196
+ if (!step.retryable || attempt === maxAttempts - 1) {
197
+ throw error;
198
+ }
199
+
200
+ // Exponential backoff
201
+ const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
202
+ await new Promise(resolve => setTimeout(resolve, delay));
203
+ }
204
+ }
205
+
206
+ throw lastError;
207
+ }
208
+
209
+ /**
210
+ * Compensate completed steps in reverse order
211
+ * @param {SagaState} state - Saga state
212
+ * @returns {Promise<void>}
213
+ * @private
214
+ */
215
+ async _compensate(state) {
216
+ // Compensate in reverse order
217
+ const stepsToCompensate = [...state.completedSteps].reverse();
218
+
219
+ for (const { stepId } of stepsToCompensate) {
220
+ const step = this.config.steps.find(s => s.id === stepId);
221
+ if (!step) continue;
222
+
223
+ try {
224
+ const result = await step.compensate(state);
225
+ state.compensatedSteps.push({
226
+ stepId,
227
+ result,
228
+ });
229
+ } catch (error) {
230
+ // Compensation failure is critical
231
+ throw new Error(
232
+ `Failed to compensate step '${stepId}': ${error.message}`,
233
+ { cause: error }
234
+ );
235
+ }
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Get saga execution state
241
+ * @param {string} executionId - Execution ID
242
+ * @returns {SagaState | undefined} Saga state
243
+ */
244
+ getState(executionId) {
245
+ return this.executions.get(executionId);
246
+ }
247
+
248
+ /**
249
+ * Get all execution states
250
+ * @returns {Array<SagaState>} All states
251
+ */
252
+ getAllStates() {
253
+ return Array.from(this.executions.values());
254
+ }
255
+
256
+ /**
257
+ * Generate receipt for saga execution
258
+ * @param {string} executionId - Execution ID
259
+ * @returns {Promise<import('./receipt.mjs').Receipt>} Receipt
260
+ */
261
+ async generateReceipt(executionId) {
262
+ const state = this.executions.get(executionId);
263
+ if (!state) {
264
+ throw new Error(`Saga execution '${executionId}' not found`);
265
+ }
266
+
267
+ return generateReceipt(
268
+ `saga:${this.config.name}`,
269
+ { sagaId: this.config.id, executionId },
270
+ {
271
+ status: state.status,
272
+ completedSteps: state.completedSteps.length,
273
+ compensatedSteps: state.compensatedSteps.length,
274
+ duration: (state.endTime || Date.now()) - state.startTime,
275
+ success: state.status === 'completed',
276
+ }
277
+ );
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Create a simple saga step
283
+ * @param {string} id - Step ID
284
+ * @param {string} name - Step name
285
+ * @param {(ctx: any) => Promise<any>} executeFn - Execute function
286
+ * @param {(ctx: any) => Promise<any>} compensateFn - Compensate function
287
+ * @param {object} [options] - Step options
288
+ * @returns {SagaStep} Saga step
289
+ */
290
+ export function createSagaStep(id, name, executeFn, compensateFn, options = {}) {
291
+ return SagaStepSchema.parse({
292
+ id,
293
+ name,
294
+ execute: executeFn,
295
+ compensate: compensateFn,
296
+ ...options,
297
+ });
298
+ }
299
+
300
+ /**
301
+ * Create saga builder for fluent API
302
+ * @param {string} id - Saga ID
303
+ * @param {string} name - Saga name
304
+ * @returns {object} Saga builder
305
+ */
306
+ export function createSagaBuilder(id, name) {
307
+ /** @type {SagaStep[]} */
308
+ const steps = [];
309
+ let parallel = false;
310
+ let continueOnError = false;
311
+
312
+ return {
313
+ /**
314
+ * Add step to saga
315
+ * @param {SagaStep} step - Step to add
316
+ * @returns {object} Builder (for chaining)
317
+ */
318
+ step(step) {
319
+ steps.push(step);
320
+ return this;
321
+ },
322
+
323
+ /**
324
+ * Enable parallel execution
325
+ * @returns {object} Builder (for chaining)
326
+ */
327
+ inParallel() {
328
+ parallel = true;
329
+ return this;
330
+ },
331
+
332
+ /**
333
+ * Continue on error
334
+ * @returns {object} Builder (for chaining)
335
+ */
336
+ continueOnError() {
337
+ continueOnError = true;
338
+ return this;
339
+ },
340
+
341
+ /**
342
+ * Build saga orchestrator
343
+ * @returns {SagaOrchestrator} Saga orchestrator
344
+ */
345
+ build() {
346
+ return new SagaOrchestrator({
347
+ id,
348
+ name,
349
+ steps,
350
+ parallel,
351
+ continueOnError,
352
+ });
353
+ },
354
+ };
355
+ }