@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,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
|
+
}
|