@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,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file API Version - Semantic versioning and deprecation tracking
|
|
3
|
+
* @module @unrdf/kgc-runtime/api-version
|
|
4
|
+
* @description Manages API versioning with deprecation policy and compatibility checking
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Current KGC Runtime API version
|
|
11
|
+
*/
|
|
12
|
+
export const CURRENT_API_VERSION = '5.0.1';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* API version status
|
|
16
|
+
*/
|
|
17
|
+
export const API_STATUS = {
|
|
18
|
+
STABLE: 'stable',
|
|
19
|
+
BETA: 'beta',
|
|
20
|
+
DEPRECATED: 'deprecated',
|
|
21
|
+
REMOVED: 'removed',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Version metadata schema
|
|
26
|
+
*/
|
|
27
|
+
const VersionMetadataSchema = z.object({
|
|
28
|
+
version: z.string().regex(/^\d+\.\d+\.\d+$/),
|
|
29
|
+
status: z.enum(['stable', 'beta', 'deprecated', 'removed']),
|
|
30
|
+
releaseDate: z.string().optional(),
|
|
31
|
+
deprecationDate: z.string().optional(),
|
|
32
|
+
removalDate: z.string().optional(),
|
|
33
|
+
deprecationReason: z.string().optional(),
|
|
34
|
+
migrationGuide: z.string().optional(),
|
|
35
|
+
breakingChanges: z.array(z.string()).optional(),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* API versions registry
|
|
40
|
+
* Each version tracks its status, deprecation, and compatibility
|
|
41
|
+
*/
|
|
42
|
+
const API_VERSIONS = [
|
|
43
|
+
{
|
|
44
|
+
version: '5.0.1',
|
|
45
|
+
status: API_STATUS.BETA,
|
|
46
|
+
releaseDate: '2024-12-27',
|
|
47
|
+
breakingChanges: [
|
|
48
|
+
'Plugin system introduced',
|
|
49
|
+
'Enhanced isolation and capability management',
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
version: '5.0.0',
|
|
54
|
+
status: API_STATUS.BETA,
|
|
55
|
+
releaseDate: '2024-12-26',
|
|
56
|
+
breakingChanges: [
|
|
57
|
+
'Work item system refactored',
|
|
58
|
+
'Receipt format updated to include parent hash',
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
version: '4.0.0',
|
|
63
|
+
status: API_STATUS.DEPRECATED,
|
|
64
|
+
releaseDate: '2024-11-01',
|
|
65
|
+
deprecationDate: '2024-12-01',
|
|
66
|
+
removalDate: '2025-03-01',
|
|
67
|
+
deprecationReason: 'Replaced by v5 with enhanced governance features',
|
|
68
|
+
migrationGuide: 'See docs/migration/v4-to-v5.md',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
version: '3.0.0',
|
|
72
|
+
status: API_STATUS.REMOVED,
|
|
73
|
+
releaseDate: '2024-06-01',
|
|
74
|
+
deprecationDate: '2024-09-01',
|
|
75
|
+
removalDate: '2024-12-01',
|
|
76
|
+
deprecationReason: 'Legacy API completely removed',
|
|
77
|
+
migrationGuide: 'See docs/migration/v3-to-v5.md',
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Deprecation policy:
|
|
83
|
+
* - APIs are marked deprecated 2 releases before removal
|
|
84
|
+
* - Deprecated APIs remain functional but emit warnings
|
|
85
|
+
* - Removal happens after minimum 3 months deprecation period
|
|
86
|
+
*/
|
|
87
|
+
export const DEPRECATION_POLICY = {
|
|
88
|
+
RELEASES_BEFORE_REMOVAL: 2,
|
|
89
|
+
MIN_DEPRECATION_PERIOD_DAYS: 90,
|
|
90
|
+
WARNING_ENABLED: true,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* API Version Manager - Tracks versions, deprecations, and compatibility
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* import { APIVersionManager } from '@unrdf/kgc-runtime/api-version';
|
|
98
|
+
* const versionManager = new APIVersionManager();
|
|
99
|
+
* const compatible = versionManager.isCompatible('5.0.0', '5.0.1');
|
|
100
|
+
* console.log(compatible); // true
|
|
101
|
+
*/
|
|
102
|
+
export class APIVersionManager {
|
|
103
|
+
constructor() {
|
|
104
|
+
this.versions = new Map();
|
|
105
|
+
this.deprecationWarnings = new Set();
|
|
106
|
+
|
|
107
|
+
// Load versions
|
|
108
|
+
for (const versionData of API_VERSIONS) {
|
|
109
|
+
this.versions.set(versionData.version, versionData);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get current API version
|
|
115
|
+
*
|
|
116
|
+
* @returns {string} Current version
|
|
117
|
+
*/
|
|
118
|
+
getCurrentVersion() {
|
|
119
|
+
return CURRENT_API_VERSION;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get version metadata
|
|
124
|
+
*
|
|
125
|
+
* @param {string} version - Version to query
|
|
126
|
+
* @returns {Object|null} Version metadata or null if not found
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* const metadata = versionManager.getVersionMetadata('5.0.1');
|
|
130
|
+
* console.log(metadata.status); // 'beta'
|
|
131
|
+
*/
|
|
132
|
+
getVersionMetadata(version) {
|
|
133
|
+
return this.versions.get(version) || null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Check if version is deprecated
|
|
138
|
+
*
|
|
139
|
+
* @param {string} version - Version to check
|
|
140
|
+
* @returns {boolean} True if deprecated
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* const deprecated = versionManager.isDeprecated('4.0.0');
|
|
144
|
+
* console.log(deprecated); // true
|
|
145
|
+
*/
|
|
146
|
+
isDeprecated(version) {
|
|
147
|
+
const metadata = this.getVersionMetadata(version);
|
|
148
|
+
return metadata?.status === API_STATUS.DEPRECATED;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Check if version is removed
|
|
153
|
+
*
|
|
154
|
+
* @param {string} version - Version to check
|
|
155
|
+
* @returns {boolean} True if removed
|
|
156
|
+
*/
|
|
157
|
+
isRemoved(version) {
|
|
158
|
+
const metadata = this.getVersionMetadata(version);
|
|
159
|
+
return metadata?.status === API_STATUS.REMOVED;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Check if version is stable
|
|
164
|
+
*
|
|
165
|
+
* @param {string} version - Version to check
|
|
166
|
+
* @returns {boolean} True if stable
|
|
167
|
+
*/
|
|
168
|
+
isStable(version) {
|
|
169
|
+
const metadata = this.getVersionMetadata(version);
|
|
170
|
+
return metadata?.status === API_STATUS.STABLE;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Check version compatibility (semver)
|
|
175
|
+
*
|
|
176
|
+
* @param {string} requiredVersion - Required version
|
|
177
|
+
* @param {string} actualVersion - Actual version
|
|
178
|
+
* @returns {boolean} True if compatible
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* const compatible = versionManager.isCompatible('5.0.0', '5.0.1');
|
|
182
|
+
* console.log(compatible); // true (patch compatible)
|
|
183
|
+
*/
|
|
184
|
+
isCompatible(requiredVersion, actualVersion) {
|
|
185
|
+
const [reqMajor, reqMinor] = requiredVersion.split('.').map(Number);
|
|
186
|
+
const [actMajor, actMinor] = actualVersion.split('.').map(Number);
|
|
187
|
+
|
|
188
|
+
// Major version must match
|
|
189
|
+
if (reqMajor !== actMajor) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Minor version must be >= required
|
|
194
|
+
if (actMinor < reqMinor) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Emit deprecation warning
|
|
203
|
+
*
|
|
204
|
+
* @param {string} feature - Deprecated feature
|
|
205
|
+
* @param {string} version - Version it was deprecated
|
|
206
|
+
* @param {string} alternative - Recommended alternative
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* versionManager.warnDeprecation('oldFunction', '4.0.0', 'Use newFunction instead');
|
|
210
|
+
*/
|
|
211
|
+
warnDeprecation(feature, version, alternative) {
|
|
212
|
+
if (!DEPRECATION_POLICY.WARNING_ENABLED) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const warningKey = `${feature}@${version}`;
|
|
217
|
+
|
|
218
|
+
// Only warn once per feature
|
|
219
|
+
if (this.deprecationWarnings.has(warningKey)) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
console.warn(
|
|
224
|
+
`[DEPRECATION WARNING] ${feature} is deprecated since v${version}. ${alternative}`
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
this.deprecationWarnings.add(warningKey);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get all deprecated versions
|
|
232
|
+
*
|
|
233
|
+
* @returns {Array<Object>} Array of deprecated version metadata
|
|
234
|
+
*/
|
|
235
|
+
getDeprecatedVersions() {
|
|
236
|
+
return Array.from(this.versions.values()).filter(
|
|
237
|
+
v => v.status === API_STATUS.DEPRECATED
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Get breaking changes for a version
|
|
243
|
+
*
|
|
244
|
+
* @param {string} version - Version to query
|
|
245
|
+
* @returns {Array<string>} Array of breaking changes
|
|
246
|
+
*/
|
|
247
|
+
getBreakingChanges(version) {
|
|
248
|
+
const metadata = this.getVersionMetadata(version);
|
|
249
|
+
return metadata?.breakingChanges || [];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Calculate days until removal
|
|
254
|
+
*
|
|
255
|
+
* @param {string} version - Deprecated version
|
|
256
|
+
* @returns {number|null} Days until removal or null if not deprecated
|
|
257
|
+
*/
|
|
258
|
+
getDaysUntilRemoval(version) {
|
|
259
|
+
const metadata = this.getVersionMetadata(version);
|
|
260
|
+
|
|
261
|
+
if (!metadata || !metadata.removalDate) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const now = new Date();
|
|
266
|
+
const removalDate = new Date(metadata.removalDate);
|
|
267
|
+
const diffTime = removalDate - now;
|
|
268
|
+
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
269
|
+
|
|
270
|
+
return diffDays;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* List all versions
|
|
275
|
+
*
|
|
276
|
+
* @returns {Array<Object>} All version metadata
|
|
277
|
+
*/
|
|
278
|
+
listVersions() {
|
|
279
|
+
return Array.from(this.versions.values());
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Add new version
|
|
284
|
+
*
|
|
285
|
+
* @param {Object} versionData - Version metadata
|
|
286
|
+
*/
|
|
287
|
+
addVersion(versionData) {
|
|
288
|
+
const validated = VersionMetadataSchema.parse(versionData);
|
|
289
|
+
this.versions.set(validated.version, validated);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Mark version as deprecated
|
|
294
|
+
*
|
|
295
|
+
* @param {string} version - Version to deprecate
|
|
296
|
+
* @param {Object} options - Deprecation options
|
|
297
|
+
* @param {string} options.reason - Deprecation reason
|
|
298
|
+
* @param {string} options.migrationGuide - Migration guide URL
|
|
299
|
+
* @param {string} options.removalDate - Planned removal date (ISO)
|
|
300
|
+
*/
|
|
301
|
+
deprecateVersion(version, options = {}) {
|
|
302
|
+
const metadata = this.getVersionMetadata(version);
|
|
303
|
+
|
|
304
|
+
if (!metadata) {
|
|
305
|
+
throw new Error(`Version not found: ${version}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
metadata.status = API_STATUS.DEPRECATED;
|
|
309
|
+
metadata.deprecationDate = new Date().toISOString().split('T')[0];
|
|
310
|
+
metadata.deprecationReason = options.reason;
|
|
311
|
+
metadata.migrationGuide = options.migrationGuide;
|
|
312
|
+
metadata.removalDate = options.removalDate;
|
|
313
|
+
|
|
314
|
+
this.versions.set(version, metadata);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Singleton instance
|
|
320
|
+
*/
|
|
321
|
+
const versionManager = new APIVersionManager();
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Get the singleton version manager
|
|
325
|
+
*
|
|
326
|
+
* @returns {APIVersionManager} Version manager instance
|
|
327
|
+
*/
|
|
328
|
+
export function getVersionManager() {
|
|
329
|
+
return versionManager;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Check if plugin API version is compatible with runtime
|
|
334
|
+
*
|
|
335
|
+
* @param {string} pluginVersion - Plugin's required API version
|
|
336
|
+
* @returns {boolean} True if compatible
|
|
337
|
+
*
|
|
338
|
+
* @example
|
|
339
|
+
* const compatible = isPluginCompatible('5.0.0');
|
|
340
|
+
* console.log(compatible); // true
|
|
341
|
+
*/
|
|
342
|
+
export function isPluginCompatible(pluginVersion) {
|
|
343
|
+
return versionManager.isCompatible(pluginVersion, CURRENT_API_VERSION);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Validate plugin API version
|
|
348
|
+
*
|
|
349
|
+
* @param {string} pluginVersion - Plugin's API version
|
|
350
|
+
* @throws {Error} If version is incompatible or removed
|
|
351
|
+
*/
|
|
352
|
+
export function validatePluginVersion(pluginVersion) {
|
|
353
|
+
if (versionManager.isRemoved(pluginVersion)) {
|
|
354
|
+
throw new Error(
|
|
355
|
+
`Plugin API version ${pluginVersion} has been removed. Please upgrade plugin.`
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (!isPluginCompatible(pluginVersion)) {
|
|
360
|
+
throw new Error(
|
|
361
|
+
`Plugin API version ${pluginVersion} is incompatible with runtime ${CURRENT_API_VERSION}`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (versionManager.isDeprecated(pluginVersion)) {
|
|
366
|
+
const daysUntilRemoval = versionManager.getDaysUntilRemoval(pluginVersion);
|
|
367
|
+
versionManager.warnDeprecation(
|
|
368
|
+
`Plugin API version ${pluginVersion}`,
|
|
369
|
+
pluginVersion,
|
|
370
|
+
`Upgrade to ${CURRENT_API_VERSION}. Removal in ${daysUntilRemoval} days.`
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Atomic Capsule Admission using Two-Phase Commit
|
|
3
|
+
* Ensures all-or-nothing capsule admission across conflicts
|
|
4
|
+
*
|
|
5
|
+
* Pattern: Transaction wrapper around merge operations
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { TransactionManager } from './transaction.mjs';
|
|
10
|
+
import { mergeCapsules } from './merge.mjs';
|
|
11
|
+
import { RollbackLog } from './rollback.mjs';
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Schemas
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Atomic admission request schema
|
|
19
|
+
*/
|
|
20
|
+
const AtomicAdmissionRequestSchema = z.object({
|
|
21
|
+
capsules: z.array(z.any()),
|
|
22
|
+
totalOrder: z.object({
|
|
23
|
+
rules: z.array(z.any()),
|
|
24
|
+
default_rule: z.any(),
|
|
25
|
+
}),
|
|
26
|
+
bounds: z.record(z.any()).optional(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Atomic admission result schema
|
|
31
|
+
*/
|
|
32
|
+
const AtomicAdmissionResultSchema = z.object({
|
|
33
|
+
success: z.boolean(),
|
|
34
|
+
transaction_id: z.string().optional(),
|
|
35
|
+
admitted: z.array(z.string()),
|
|
36
|
+
denied: z.array(z.string()),
|
|
37
|
+
conflict_receipts: z.array(z.any()),
|
|
38
|
+
receipts: z.array(z.any()).optional(),
|
|
39
|
+
errors: z.array(z.string()).optional(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @typedef {z.infer<typeof AtomicAdmissionRequestSchema>} AtomicAdmissionRequest
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {z.infer<typeof AtomicAdmissionResultSchema>} AtomicAdmissionResult
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// AtomicAdmissionGate Class
|
|
52
|
+
// ============================================================================
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* AtomicAdmissionGate - Atomic capsule admission using transactions
|
|
56
|
+
*
|
|
57
|
+
* Features:
|
|
58
|
+
* - All-or-nothing admission (either all capsules admitted or none)
|
|
59
|
+
* - Automatic rollback on conflict
|
|
60
|
+
* - Receipt generation for all operations
|
|
61
|
+
* - Integration with merge conflict resolution
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* const gate = new AtomicAdmissionGate();
|
|
65
|
+
* const result = await gate.admitCapsules(capsules, totalOrder);
|
|
66
|
+
* if (result.success) {
|
|
67
|
+
* console.log('All capsules admitted:', result.admitted);
|
|
68
|
+
* } else {
|
|
69
|
+
* console.log('Admission failed, rolled back');
|
|
70
|
+
* }
|
|
71
|
+
*/
|
|
72
|
+
export class AtomicAdmissionGate {
|
|
73
|
+
/**
|
|
74
|
+
* @param {Object} options - Configuration options
|
|
75
|
+
* @param {string} options.logPath - Path to rollback log
|
|
76
|
+
*/
|
|
77
|
+
constructor(options = {}) {
|
|
78
|
+
/** @type {TransactionManager} */
|
|
79
|
+
this.txManager = new TransactionManager({
|
|
80
|
+
logPath: options.logPath,
|
|
81
|
+
onRollback: (event) => this._handleRollback(event),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/** @type {RollbackLog} */
|
|
85
|
+
this.rollbackLog = new RollbackLog(options.logPath);
|
|
86
|
+
|
|
87
|
+
/** @type {Map<string, any>} */
|
|
88
|
+
this.admittedCapsules = new Map();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Admit capsules atomically
|
|
93
|
+
*
|
|
94
|
+
* @param {any[]} capsules - Capsules to admit
|
|
95
|
+
* @param {any} totalOrder - Conflict resolution rules
|
|
96
|
+
* @param {any} [bounds] - Optional resource bounds
|
|
97
|
+
* @returns {Promise<AtomicAdmissionResult>} Admission result
|
|
98
|
+
*/
|
|
99
|
+
async admitCapsules(capsules, totalOrder, bounds = {}) {
|
|
100
|
+
try {
|
|
101
|
+
// Validate input
|
|
102
|
+
const request = AtomicAdmissionRequestSchema.parse({
|
|
103
|
+
capsules,
|
|
104
|
+
totalOrder,
|
|
105
|
+
bounds,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Phase 1: Merge and detect conflicts
|
|
109
|
+
const mergeResult = mergeCapsules(request.capsules, request.totalOrder);
|
|
110
|
+
|
|
111
|
+
// Check if all capsules were admitted (no conflicts)
|
|
112
|
+
if (mergeResult.denied.length > 0) {
|
|
113
|
+
// Some capsules denied - reject entire batch
|
|
114
|
+
return AtomicAdmissionResultSchema.parse({
|
|
115
|
+
success: false,
|
|
116
|
+
admitted: [],
|
|
117
|
+
denied: mergeResult.denied,
|
|
118
|
+
conflict_receipts: mergeResult.conflict_receipts,
|
|
119
|
+
errors: ['Conflict detected - admission denied to maintain atomicity'],
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Phase 2: Create transaction for admission
|
|
124
|
+
const operations = mergeResult.admitted.map(capsuleId => {
|
|
125
|
+
const capsule = request.capsules.find(c => c.id === capsuleId);
|
|
126
|
+
return {
|
|
127
|
+
id: `admit_${capsuleId}`,
|
|
128
|
+
type: 'add_capsule',
|
|
129
|
+
data: capsule,
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const tx = this.txManager.begin(operations);
|
|
134
|
+
|
|
135
|
+
// Phase 3: Prepare transaction
|
|
136
|
+
const prepareResult = await this.txManager.prepare(tx.id);
|
|
137
|
+
|
|
138
|
+
if (!prepareResult.success) {
|
|
139
|
+
// Prepare failed - abort
|
|
140
|
+
return AtomicAdmissionResultSchema.parse({
|
|
141
|
+
success: false,
|
|
142
|
+
admitted: [],
|
|
143
|
+
denied: mergeResult.admitted,
|
|
144
|
+
conflict_receipts: [],
|
|
145
|
+
errors: prepareResult.errors,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Phase 4: Commit transaction
|
|
150
|
+
const commitResult = await this.txManager.commit(tx.id);
|
|
151
|
+
|
|
152
|
+
if (!commitResult.success) {
|
|
153
|
+
// Commit failed - rollback
|
|
154
|
+
await this.txManager.rollback(tx.id);
|
|
155
|
+
|
|
156
|
+
// Log rollback
|
|
157
|
+
await this.rollbackLog.append(
|
|
158
|
+
tx.id,
|
|
159
|
+
prepareResult.undoOps,
|
|
160
|
+
tx.hash
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
return AtomicAdmissionResultSchema.parse({
|
|
164
|
+
success: false,
|
|
165
|
+
admitted: [],
|
|
166
|
+
denied: mergeResult.admitted,
|
|
167
|
+
conflict_receipts: [],
|
|
168
|
+
errors: commitResult.errors,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Success - store admitted capsules
|
|
173
|
+
for (const capsuleId of mergeResult.admitted) {
|
|
174
|
+
const capsule = request.capsules.find(c => c.id === capsuleId);
|
|
175
|
+
this.admittedCapsules.set(capsuleId, capsule);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return AtomicAdmissionResultSchema.parse({
|
|
179
|
+
success: true,
|
|
180
|
+
transaction_id: tx.id,
|
|
181
|
+
admitted: mergeResult.admitted,
|
|
182
|
+
denied: [],
|
|
183
|
+
conflict_receipts: mergeResult.conflict_receipts,
|
|
184
|
+
receipts: commitResult.receipts,
|
|
185
|
+
});
|
|
186
|
+
} catch (error) {
|
|
187
|
+
return AtomicAdmissionResultSchema.parse({
|
|
188
|
+
success: false,
|
|
189
|
+
admitted: [],
|
|
190
|
+
denied: capsules.map(c => c.id),
|
|
191
|
+
conflict_receipts: [],
|
|
192
|
+
errors: [error.message],
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Admit single capsule (convenience method)
|
|
199
|
+
*
|
|
200
|
+
* @param {any} capsule - Capsule to admit
|
|
201
|
+
* @param {any} totalOrder - Conflict resolution rules
|
|
202
|
+
* @returns {Promise<AtomicAdmissionResult>} Admission result
|
|
203
|
+
*/
|
|
204
|
+
async admitCapsule(capsule, totalOrder) {
|
|
205
|
+
return this.admitCapsules([capsule], totalOrder);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Rollback a transaction by ID
|
|
210
|
+
*
|
|
211
|
+
* @param {string} transactionId - Transaction ID to rollback
|
|
212
|
+
* @returns {Promise<{success: boolean, operations_undone: number}>} Rollback result
|
|
213
|
+
*/
|
|
214
|
+
async rollbackTransaction(transactionId) {
|
|
215
|
+
// Rollback using transaction manager
|
|
216
|
+
const result = await this.txManager.rollback(transactionId);
|
|
217
|
+
|
|
218
|
+
// Remove rolled back capsules from admitted set
|
|
219
|
+
const tx = this.txManager.getTransaction(transactionId);
|
|
220
|
+
if (tx) {
|
|
221
|
+
for (const op of tx.operations) {
|
|
222
|
+
if (op.type === 'add_capsule' && op.data?.id) {
|
|
223
|
+
this.admittedCapsules.delete(op.data.id);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get all admitted capsules
|
|
233
|
+
* @returns {any[]} Admitted capsules
|
|
234
|
+
*/
|
|
235
|
+
getAdmittedCapsules() {
|
|
236
|
+
return Array.from(this.admittedCapsules.values());
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Check if capsule is admitted
|
|
241
|
+
*
|
|
242
|
+
* @param {string} capsuleId - Capsule ID
|
|
243
|
+
* @returns {boolean} True if admitted
|
|
244
|
+
*/
|
|
245
|
+
isAdmitted(capsuleId) {
|
|
246
|
+
return this.admittedCapsules.has(capsuleId);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Handle rollback event
|
|
251
|
+
* @private
|
|
252
|
+
*/
|
|
253
|
+
_handleRollback(event) {
|
|
254
|
+
console.log(`Transaction ${event.transaction_id} rolled back: ${event.operations_undone} operations undone`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Reset state (for testing)
|
|
259
|
+
*/
|
|
260
|
+
reset() {
|
|
261
|
+
this.txManager.reset();
|
|
262
|
+
this.admittedCapsules.clear();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ============================================================================
|
|
267
|
+
// Cascading Rollback Support
|
|
268
|
+
// ============================================================================
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Rollback transaction and all dependent transactions
|
|
272
|
+
*
|
|
273
|
+
* @param {AtomicAdmissionGate} gate - Admission gate
|
|
274
|
+
* @param {string} transactionId - Root transaction to rollback
|
|
275
|
+
* @returns {Promise<{success: boolean, rolled_back: string[]}>} Rollback result
|
|
276
|
+
*/
|
|
277
|
+
export async function cascadingRollback(gate, transactionId) {
|
|
278
|
+
const rolledBack = [];
|
|
279
|
+
const toRollback = [transactionId];
|
|
280
|
+
|
|
281
|
+
while (toRollback.length > 0) {
|
|
282
|
+
const txId = toRollback.pop();
|
|
283
|
+
|
|
284
|
+
// Rollback this transaction
|
|
285
|
+
const result = await gate.rollbackTransaction(txId);
|
|
286
|
+
|
|
287
|
+
if (result.success) {
|
|
288
|
+
rolledBack.push(txId);
|
|
289
|
+
|
|
290
|
+
// Find dependent transactions (those with this tx as parent)
|
|
291
|
+
const allTx = gate.txManager.getAllTransactions();
|
|
292
|
+
const dependents = allTx.filter(tx => tx.parentHash === txId);
|
|
293
|
+
|
|
294
|
+
for (const depTx of dependents) {
|
|
295
|
+
toRollback.push(depTx.id);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
success: true,
|
|
302
|
+
rolled_back: rolledBack,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ============================================================================
|
|
307
|
+
// Exports
|
|
308
|
+
// ============================================================================
|
|
309
|
+
|
|
310
|
+
export default AtomicAdmissionGate;
|