@unrdf/knowledge-engine 5.0.1
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/LICENSE +21 -0
- package/README.md +84 -0
- package/package.json +64 -0
- package/src/browser-shims.mjs +343 -0
- package/src/browser.mjs +910 -0
- package/src/canonicalize.mjs +414 -0
- package/src/condition-cache.mjs +109 -0
- package/src/condition-evaluator.mjs +722 -0
- package/src/dark-matter-core.mjs +742 -0
- package/src/define-hook.mjs +213 -0
- package/src/effect-sandbox-browser.mjs +283 -0
- package/src/effect-sandbox-worker.mjs +170 -0
- package/src/effect-sandbox.mjs +517 -0
- package/src/engines/index.mjs +11 -0
- package/src/engines/rdf-engine.mjs +299 -0
- package/src/file-resolver.mjs +387 -0
- package/src/hook-executor-batching.mjs +277 -0
- package/src/hook-executor.mjs +870 -0
- package/src/hook-management.mjs +150 -0
- package/src/index.mjs +93 -0
- package/src/ken-parliment.mjs +119 -0
- package/src/ken.mjs +149 -0
- package/src/knowledge-engine/builtin-rules.mjs +190 -0
- package/src/knowledge-engine/inference-engine.mjs +418 -0
- package/src/knowledge-engine/knowledge-engine.mjs +317 -0
- package/src/knowledge-engine/pattern-dsl.mjs +142 -0
- package/src/knowledge-engine/pattern-matcher.mjs +215 -0
- package/src/knowledge-engine/rules.mjs +184 -0
- package/src/knowledge-engine.mjs +319 -0
- package/src/knowledge-hook-engine.mjs +360 -0
- package/src/knowledge-hook-manager.mjs +469 -0
- package/src/knowledge-substrate-core.mjs +927 -0
- package/src/lite.mjs +222 -0
- package/src/lockchain-writer-browser.mjs +414 -0
- package/src/lockchain-writer.mjs +602 -0
- package/src/monitoring/andon-signals.mjs +775 -0
- package/src/observability.mjs +531 -0
- package/src/parse.mjs +290 -0
- package/src/performance-optimizer.mjs +678 -0
- package/src/policy-pack.mjs +572 -0
- package/src/query-cache.mjs +116 -0
- package/src/query-optimizer.mjs +1051 -0
- package/src/query.mjs +306 -0
- package/src/reason.mjs +350 -0
- package/src/resolution-layer.mjs +506 -0
- package/src/schemas.mjs +1063 -0
- package/src/security/error-sanitizer.mjs +257 -0
- package/src/security/path-validator.mjs +194 -0
- package/src/security/sandbox-restrictions.mjs +331 -0
- package/src/security-validator.mjs +389 -0
- package/src/store-cache.mjs +137 -0
- package/src/telemetry.mjs +167 -0
- package/src/transaction.mjs +810 -0
- package/src/utils/adaptive-monitor.mjs +746 -0
- package/src/utils/circuit-breaker.mjs +513 -0
- package/src/utils/edge-case-handler.mjs +503 -0
- package/src/utils/memory-manager.mjs +498 -0
- package/src/utils/ring-buffer.mjs +282 -0
- package/src/validate.mjs +319 -0
- package/src/validators/index.mjs +338 -0
|
@@ -0,0 +1,810 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Transaction manager with hooks and receipts.
|
|
3
|
+
* @module transaction
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createStore } from '@unrdf/oxigraph';
|
|
7
|
+
import { sha3_256 } from '@noble/hashes/sha3.js';
|
|
8
|
+
import { blake3 } from '@noble/hashes/blake3.js';
|
|
9
|
+
import { utf8ToBytes, bytesToHex } from '@noble/hashes/utils.js';
|
|
10
|
+
import { canonicalize } from './canonicalize.mjs';
|
|
11
|
+
import { createLockchainWriter } from './lockchain-writer.mjs';
|
|
12
|
+
import { createResolutionLayer } from './resolution-layer.mjs';
|
|
13
|
+
import { createObservabilityManager } from './observability.mjs';
|
|
14
|
+
import { randomUUID } from 'crypto';
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
import { trace, SpanStatusCode } from '@opentelemetry/api';
|
|
17
|
+
|
|
18
|
+
const tracer = trace.getTracer('unrdf');
|
|
19
|
+
|
|
20
|
+
// Import consolidated schemas
|
|
21
|
+
import {
|
|
22
|
+
_QuadSchema,
|
|
23
|
+
DeltaSchema,
|
|
24
|
+
TransactionHookSchema,
|
|
25
|
+
_TransactionHookResultSchema,
|
|
26
|
+
_HashSchema,
|
|
27
|
+
_TransactionReceiptSchemaNew,
|
|
28
|
+
TransactionOptionsSchema,
|
|
29
|
+
ManagerOptionsSchema,
|
|
30
|
+
} from './schemas.mjs';
|
|
31
|
+
|
|
32
|
+
// Zod schemas for validation
|
|
33
|
+
// QuadSchema now imported from schemas.mjs
|
|
34
|
+
|
|
35
|
+
// All schemas now imported from schemas.mjs
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {z.infer<typeof DeltaSchema>} Delta
|
|
39
|
+
* @typedef {z.infer<typeof TransactionHookSchema>} Hook
|
|
40
|
+
* @typedef {z.infer<typeof TransactionHookResultSchema>} HookResult
|
|
41
|
+
* @typedef {z.infer<typeof TransactionReceiptSchemaNew>} Receipt
|
|
42
|
+
* @typedef {z.infer<typeof TransactionOptionsSchema>} TransactionOptions
|
|
43
|
+
* @typedef {z.infer<typeof ManagerOptionsSchema>} ManagerOptions
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Hash a store canonically with SHA-3 and BLAKE3.
|
|
48
|
+
* @param {Store} store - The store to hash
|
|
49
|
+
* @param {Object} [options] - Hashing options
|
|
50
|
+
* @param {boolean} [options.afterHashOnly=false] - Skip canonicalization for performance
|
|
51
|
+
* @returns {Promise<{ sha3: string, blake3: string }>} Promise resolving to hash object
|
|
52
|
+
*
|
|
53
|
+
* @throws {Error} If hashing fails
|
|
54
|
+
*/
|
|
55
|
+
async function hashStore(store, options = {}) {
|
|
56
|
+
try {
|
|
57
|
+
if (options.afterHashOnly) {
|
|
58
|
+
// Fast hash without canonicalization for performance
|
|
59
|
+
const quads = store.getQuads();
|
|
60
|
+
const content = quads
|
|
61
|
+
.map(q => `${q.subject.value} ${q.predicate.value} ${q.object.value} ${q.graph.value}`)
|
|
62
|
+
.join('\n');
|
|
63
|
+
const bytes = utf8ToBytes(content);
|
|
64
|
+
return {
|
|
65
|
+
sha3: bytesToHex(sha3_256(bytes)),
|
|
66
|
+
blake3: bytesToHex(blake3(bytes)),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const c14n = await canonicalize(store);
|
|
71
|
+
const bytes = utf8ToBytes(c14n);
|
|
72
|
+
return {
|
|
73
|
+
sha3: bytesToHex(sha3_256(bytes)),
|
|
74
|
+
blake3: bytesToHex(blake3(bytes)),
|
|
75
|
+
};
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw new Error(`Store hashing failed: ${error.message}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Transaction manager with hooks and receipts.
|
|
83
|
+
* Provides atomic transactions with pre/post hooks and comprehensive receipts.
|
|
84
|
+
*/
|
|
85
|
+
export class TransactionManager {
|
|
86
|
+
/**
|
|
87
|
+
* Create a new transaction manager.
|
|
88
|
+
* @param {ManagerOptions} [options] - Manager options
|
|
89
|
+
*/
|
|
90
|
+
constructor(options = {}) {
|
|
91
|
+
/** @type {Hook[]} */
|
|
92
|
+
this.hooks = [];
|
|
93
|
+
this.options = ManagerOptionsSchema.parse(options);
|
|
94
|
+
|
|
95
|
+
// Simple mutex for concurrency control - no circular ref accumulation
|
|
96
|
+
this._applyMutex = null;
|
|
97
|
+
this._resetMutex();
|
|
98
|
+
|
|
99
|
+
// Initialize observability manager
|
|
100
|
+
this.observability = createObservabilityManager(this.options.observability || {});
|
|
101
|
+
|
|
102
|
+
// Performance tracking with bounded arrays
|
|
103
|
+
this.performanceMetrics = {
|
|
104
|
+
transactionLatency: [],
|
|
105
|
+
hookExecutionRate: 0,
|
|
106
|
+
errorCount: 0,
|
|
107
|
+
totalTransactions: 0,
|
|
108
|
+
_maxLatencyEntries: 1000,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Initialize lockchain writer if enabled
|
|
112
|
+
if (this.options.enableLockchain) {
|
|
113
|
+
const lockchainConfig = {
|
|
114
|
+
gitRepo: this.options.lockchainConfig?.gitRepo || process.cwd(),
|
|
115
|
+
refName: this.options.lockchainConfig?.refName || 'refs/notes/lockchain',
|
|
116
|
+
signingKey: this.options.lockchainConfig?.signingKey,
|
|
117
|
+
batchSize: this.options.lockchainConfig?.batchSize || 10,
|
|
118
|
+
};
|
|
119
|
+
this.lockchainWriter = createLockchainWriter(lockchainConfig);
|
|
120
|
+
} else {
|
|
121
|
+
this.lockchainWriter = null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Initialize resolution layer if enabled
|
|
125
|
+
if (this.options.enableResolution) {
|
|
126
|
+
const resolutionConfig = {
|
|
127
|
+
defaultStrategy: this.options.resolutionConfig?.defaultStrategy || 'voting',
|
|
128
|
+
maxProposals: this.options.resolutionConfig?.maxProposals || 100,
|
|
129
|
+
enableConflictDetection: this.options.resolutionConfig?.enableConflictDetection !== false,
|
|
130
|
+
enableConsensus: this.options.resolutionConfig?.enableConsensus !== false,
|
|
131
|
+
timeout: this.options.resolutionConfig?.timeout || 30000,
|
|
132
|
+
};
|
|
133
|
+
this.resolutionLayer = createResolutionLayer(resolutionConfig);
|
|
134
|
+
} else {
|
|
135
|
+
this.resolutionLayer = null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Register a hook.
|
|
141
|
+
* @param {Hook} hook - Hook to register
|
|
142
|
+
* @throws {Error} If hook is invalid or limit exceeded
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* tx.addHook({
|
|
146
|
+
* id: "no-eve",
|
|
147
|
+
* mode: "pre",
|
|
148
|
+
* condition: async (store, delta) => !delta.additions.some(q => q.object.value.endsWith("eve")),
|
|
149
|
+
* effect: "veto"
|
|
150
|
+
* });
|
|
151
|
+
*/
|
|
152
|
+
addHook(hook) {
|
|
153
|
+
// Validate hook with Zod
|
|
154
|
+
const validatedHook = TransactionHookSchema.parse(hook);
|
|
155
|
+
|
|
156
|
+
// Check for duplicate IDs
|
|
157
|
+
if (this.hooks.some(h => h.id === validatedHook.id)) {
|
|
158
|
+
throw new Error(`Hook with id "${validatedHook.id}" already exists`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Check hook limit
|
|
162
|
+
if (this.hooks.length >= this.options.maxHooks) {
|
|
163
|
+
throw new Error(`Maximum number of hooks (${this.options.maxHooks}) exceeded`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
this.hooks.push(validatedHook);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Remove a hook by ID.
|
|
171
|
+
* @param {string} hookId - Hook identifier to remove
|
|
172
|
+
* @returns {boolean} True if hook was removed, false if not found
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* const removed = tx.removeHook("no-eve");
|
|
176
|
+
* console.log('Hook removed:', removed);
|
|
177
|
+
*/
|
|
178
|
+
removeHook(hookId) {
|
|
179
|
+
// Validate hookId with Zod
|
|
180
|
+
const validatedHookId = z.string().parse(hookId);
|
|
181
|
+
|
|
182
|
+
const index = this.hooks.findIndex(h => h.id === validatedHookId);
|
|
183
|
+
if (index === -1) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.hooks.splice(index, 1);
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get all registered hooks.
|
|
193
|
+
* @returns {Hook[]} Array of registered hooks
|
|
194
|
+
*/
|
|
195
|
+
getHooks() {
|
|
196
|
+
return [...this.hooks];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Clear all hooks.
|
|
201
|
+
*/
|
|
202
|
+
clearHooks() {
|
|
203
|
+
this.hooks = [];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Apply a transaction.
|
|
208
|
+
* @param {Store} store - The store to apply the transaction to
|
|
209
|
+
* @param {Delta} delta - The delta to apply
|
|
210
|
+
* @param {TransactionOptions} [options] - Transaction options
|
|
211
|
+
* @returns {Promise<{store: Store, receipt: Receipt}>} Promise resolving to transaction result
|
|
212
|
+
*
|
|
213
|
+
* @throws {Error} If transaction fails
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* const delta = {
|
|
217
|
+
* additions: [quad(namedNode("ex:alice"), namedNode("ex:knows"), namedNode("ex:bob"))],
|
|
218
|
+
* removals: []
|
|
219
|
+
* };
|
|
220
|
+
*
|
|
221
|
+
* const result = await tx.apply(store, delta);
|
|
222
|
+
* console.log('Committed:', result.receipt.committed);
|
|
223
|
+
* console.log('New store size:', result.store.size);
|
|
224
|
+
*/
|
|
225
|
+
async apply(store, delta, options = {}) {
|
|
226
|
+
// Validate inputs with Zod
|
|
227
|
+
if (!store || typeof store.getQuads !== 'function') {
|
|
228
|
+
throw new TypeError('apply: store must be a valid Store instance');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const validatedDelta = DeltaSchema.parse(delta);
|
|
232
|
+
const validatedOptions = TransactionOptionsSchema.parse(options);
|
|
233
|
+
const startTime = Date.now();
|
|
234
|
+
const transactionId = randomUUID();
|
|
235
|
+
|
|
236
|
+
/** @type {HookResult[]} */
|
|
237
|
+
const hookResults = [];
|
|
238
|
+
/** @type {string[]} */
|
|
239
|
+
const hookErrors = [];
|
|
240
|
+
|
|
241
|
+
// Start observability span
|
|
242
|
+
const _spanContext = this.observability.startTransactionSpan(transactionId, {
|
|
243
|
+
'kgc.delta.additions': validatedDelta.additions.length,
|
|
244
|
+
'kgc.delta.removals': validatedDelta.removals.length,
|
|
245
|
+
'kgc.actor': validatedOptions.actor || 'system',
|
|
246
|
+
'kgc.skipHooks': validatedOptions.skipHooks || false,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Use mutex for concurrency control - reset to prevent chain buildup
|
|
250
|
+
const currentMutex = this._applyMutex;
|
|
251
|
+
|
|
252
|
+
return new Promise((resolve, reject) => {
|
|
253
|
+
this._applyMutex = currentMutex
|
|
254
|
+
.then(async () => {
|
|
255
|
+
try {
|
|
256
|
+
// Set up timeout with proper cleanup
|
|
257
|
+
let timeoutHandle;
|
|
258
|
+
const timeoutPromise = new Promise((_, timeoutReject) => {
|
|
259
|
+
timeoutHandle = setTimeout(
|
|
260
|
+
() => timeoutReject(new Error('Transaction timeout')),
|
|
261
|
+
validatedOptions.timeoutMs
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const transactionPromise = this._executeTransaction(
|
|
266
|
+
store,
|
|
267
|
+
validatedDelta,
|
|
268
|
+
validatedOptions.skipHooks,
|
|
269
|
+
hookResults,
|
|
270
|
+
hookErrors,
|
|
271
|
+
transactionId,
|
|
272
|
+
validatedOptions.actor
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const result = await Promise.race([transactionPromise, timeoutPromise]);
|
|
276
|
+
clearTimeout(timeoutHandle);
|
|
277
|
+
|
|
278
|
+
// Reset mutex chain to prevent circular reference buildup
|
|
279
|
+
this._resetMutex();
|
|
280
|
+
|
|
281
|
+
const finalReceipt = {
|
|
282
|
+
...result.receipt,
|
|
283
|
+
id: transactionId,
|
|
284
|
+
timestamp: startTime,
|
|
285
|
+
durationMs: Date.now() - startTime,
|
|
286
|
+
actor: validatedOptions.actor,
|
|
287
|
+
hookErrors,
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// Write to lockchain if enabled
|
|
291
|
+
if (this.lockchainWriter && result.receipt.committed) {
|
|
292
|
+
try {
|
|
293
|
+
await this.lockchainWriter.writeReceipt(finalReceipt);
|
|
294
|
+
} catch (lockchainError) {
|
|
295
|
+
console.warn('Failed to write receipt to lockchain:', lockchainError.message);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Update performance metrics
|
|
300
|
+
const duration = Date.now() - startTime;
|
|
301
|
+
this._updatePerformanceMetrics(duration, true);
|
|
302
|
+
|
|
303
|
+
// End observability span
|
|
304
|
+
this.observability.endTransactionSpan(transactionId, {
|
|
305
|
+
'kgc.transaction.committed': finalReceipt.committed,
|
|
306
|
+
'kgc.hook.results': hookResults.length,
|
|
307
|
+
'kgc.hook.errors': hookErrors.length,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
resolve({
|
|
311
|
+
store: result.store,
|
|
312
|
+
receipt: finalReceipt,
|
|
313
|
+
});
|
|
314
|
+
} catch (error) {
|
|
315
|
+
const beforeHash = await hashStore(store, this.options).catch(() => ({
|
|
316
|
+
sha3: '',
|
|
317
|
+
blake3: '',
|
|
318
|
+
}));
|
|
319
|
+
|
|
320
|
+
// Update performance metrics
|
|
321
|
+
const duration = Date.now() - startTime;
|
|
322
|
+
this._updatePerformanceMetrics(duration, false);
|
|
323
|
+
|
|
324
|
+
// Record error
|
|
325
|
+
this.observability.recordError(error, {
|
|
326
|
+
'kgc.transaction.id': transactionId,
|
|
327
|
+
'kgc.actor': validatedOptions.actor || 'system',
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// End observability span with error
|
|
331
|
+
this.observability.endTransactionSpan(transactionId, {}, error);
|
|
332
|
+
|
|
333
|
+
resolve({
|
|
334
|
+
store,
|
|
335
|
+
receipt: {
|
|
336
|
+
id: transactionId,
|
|
337
|
+
delta: validatedDelta,
|
|
338
|
+
committed: false,
|
|
339
|
+
hookResults,
|
|
340
|
+
beforeHash,
|
|
341
|
+
afterHash: beforeHash,
|
|
342
|
+
timestamp: startTime,
|
|
343
|
+
durationMs: Date.now() - startTime,
|
|
344
|
+
actor: validatedOptions.actor,
|
|
345
|
+
hookErrors,
|
|
346
|
+
error: error.message,
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
})
|
|
351
|
+
.catch(reject);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Execute the transaction with hooks.
|
|
357
|
+
* @private
|
|
358
|
+
*/
|
|
359
|
+
async _executeTransaction(
|
|
360
|
+
store,
|
|
361
|
+
delta,
|
|
362
|
+
skipHooks,
|
|
363
|
+
hookResults,
|
|
364
|
+
hookErrors,
|
|
365
|
+
transactionId,
|
|
366
|
+
actor
|
|
367
|
+
) {
|
|
368
|
+
return tracer.startActiveSpan('transaction.commit', async span => {
|
|
369
|
+
try {
|
|
370
|
+
span.setAttributes({
|
|
371
|
+
'transaction.id': transactionId,
|
|
372
|
+
'transaction.actor': actor || 'system',
|
|
373
|
+
'transaction.skip_hooks': skipHooks,
|
|
374
|
+
'transaction.additions_count': delta.additions.length,
|
|
375
|
+
'transaction.removals_count': delta.removals.length,
|
|
376
|
+
'transaction.store_size_before': store.size,
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
const beforeHash = await hashStore(store, this.options);
|
|
380
|
+
|
|
381
|
+
const result = await this._executeTransactionWithHooks(
|
|
382
|
+
store,
|
|
383
|
+
delta,
|
|
384
|
+
skipHooks,
|
|
385
|
+
hookResults,
|
|
386
|
+
hookErrors,
|
|
387
|
+
beforeHash
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
span.setAttributes({
|
|
391
|
+
'transaction.committed': result.receipt.committed,
|
|
392
|
+
'transaction.hook_results': hookResults.length,
|
|
393
|
+
'transaction.hook_errors': hookErrors.length,
|
|
394
|
+
'transaction.store_size_after': store.size,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
398
|
+
return result;
|
|
399
|
+
} catch (error) {
|
|
400
|
+
span.recordException(error);
|
|
401
|
+
span.setStatus({
|
|
402
|
+
code: SpanStatusCode.ERROR,
|
|
403
|
+
message: error.message,
|
|
404
|
+
});
|
|
405
|
+
throw error;
|
|
406
|
+
} finally {
|
|
407
|
+
span.end();
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Execute transaction with hooks
|
|
414
|
+
* @private
|
|
415
|
+
*/
|
|
416
|
+
async _executeTransactionWithHooks(store, delta, skipHooks, hookResults, hookErrors, beforeHash) {
|
|
417
|
+
// Pre-hooks
|
|
418
|
+
if (!skipHooks) {
|
|
419
|
+
for (const hook of this.hooks.filter(h => h.mode === 'pre')) {
|
|
420
|
+
try {
|
|
421
|
+
const ok = await hook.condition(store, delta);
|
|
422
|
+
hookResults.push({ hookId: hook.id, mode: hook.mode, result: ok });
|
|
423
|
+
|
|
424
|
+
if (!ok && hook.effect === 'veto') {
|
|
425
|
+
return {
|
|
426
|
+
store,
|
|
427
|
+
receipt: {
|
|
428
|
+
delta,
|
|
429
|
+
committed: false,
|
|
430
|
+
hookResults,
|
|
431
|
+
beforeHash,
|
|
432
|
+
afterHash: beforeHash,
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
} catch (error) {
|
|
437
|
+
const errorMsg = `Pre-hook "${hook.id}" failed: ${error.message}`;
|
|
438
|
+
hookErrors.push(errorMsg);
|
|
439
|
+
hookResults.push({
|
|
440
|
+
hookId: hook.id,
|
|
441
|
+
mode: hook.mode,
|
|
442
|
+
result: false,
|
|
443
|
+
error: error.message,
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
if (this.options.strictMode) {
|
|
447
|
+
throw new Error(errorMsg);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Commit transaction - MUTATE IN PLACE for state accumulation
|
|
454
|
+
// Remove quads first
|
|
455
|
+
for (const quad of delta.removals) {
|
|
456
|
+
store.removeQuad(quad);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Add new quads
|
|
460
|
+
for (const quad of delta.additions) {
|
|
461
|
+
store.addQuad(quad);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Post-hooks
|
|
465
|
+
if (!skipHooks) {
|
|
466
|
+
for (const hook of this.hooks.filter(h => h.mode === 'post')) {
|
|
467
|
+
try {
|
|
468
|
+
const ok = await hook.condition(store, delta);
|
|
469
|
+
hookResults.push({ hookId: hook.id, mode: hook.mode, result: ok });
|
|
470
|
+
|
|
471
|
+
// Post-hooks ignore veto effects - only execute function effects
|
|
472
|
+
if (ok && typeof hook.effect === 'function') {
|
|
473
|
+
await hook.effect(store, delta);
|
|
474
|
+
}
|
|
475
|
+
} catch (error) {
|
|
476
|
+
const errorMsg = `Post-hook "${hook.id}" failed: ${error.message}`;
|
|
477
|
+
hookErrors.push(errorMsg);
|
|
478
|
+
hookResults.push({
|
|
479
|
+
hookId: hook.id,
|
|
480
|
+
mode: hook.mode,
|
|
481
|
+
result: false,
|
|
482
|
+
error: error.message,
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
if (this.options.strictMode) {
|
|
486
|
+
throw new Error(errorMsg);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const afterHash = await hashStore(store, this.options);
|
|
493
|
+
|
|
494
|
+
return {
|
|
495
|
+
store,
|
|
496
|
+
receipt: {
|
|
497
|
+
delta,
|
|
498
|
+
committed: true,
|
|
499
|
+
hookResults,
|
|
500
|
+
beforeHash,
|
|
501
|
+
afterHash,
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Create a transaction session for batch operations.
|
|
508
|
+
* @param {Store} initialStore - Initial store state
|
|
509
|
+
* @param {Object} [sessionOptions] - Session options
|
|
510
|
+
* @returns {Object} Transaction session
|
|
511
|
+
*
|
|
512
|
+
* @example
|
|
513
|
+
* const session = tx.createSession(store);
|
|
514
|
+
*
|
|
515
|
+
* // Add multiple deltas
|
|
516
|
+
* session.addDelta(delta1);
|
|
517
|
+
* session.addDelta(delta2);
|
|
518
|
+
*
|
|
519
|
+
* // Apply all deltas
|
|
520
|
+
* const results = await session.applyAll();
|
|
521
|
+
*
|
|
522
|
+
* // Get final state
|
|
523
|
+
* const finalStore = session.getCurrentStore();
|
|
524
|
+
*/
|
|
525
|
+
createSession(initialStore, _sessionOptions = {}) {
|
|
526
|
+
if (!initialStore || typeof initialStore.getQuads !== 'function') {
|
|
527
|
+
throw new TypeError('createSession: initialStore must be a valid Store instance');
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
let currentStore = createStore(initialStore.getQuads());
|
|
531
|
+
const deltas = [];
|
|
532
|
+
const receipts = [];
|
|
533
|
+
|
|
534
|
+
return {
|
|
535
|
+
/**
|
|
536
|
+
* Add a delta to the session.
|
|
537
|
+
* @param {Delta} delta - Delta to add
|
|
538
|
+
*/
|
|
539
|
+
addDelta(delta) {
|
|
540
|
+
const validatedDelta = DeltaSchema.parse(delta);
|
|
541
|
+
deltas.push(validatedDelta);
|
|
542
|
+
},
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Apply all deltas in the session.
|
|
546
|
+
* @param {TransactionOptions} [options] - Apply options
|
|
547
|
+
* @returns {Promise<Array<Receipt>>} Promise resolving to array of receipts
|
|
548
|
+
*/
|
|
549
|
+
applyAll: async (options = {}) => {
|
|
550
|
+
const validatedOptions = TransactionOptionsSchema.parse(options);
|
|
551
|
+
const results = [];
|
|
552
|
+
|
|
553
|
+
for (const delta of deltas) {
|
|
554
|
+
// Use arrow function to preserve this context
|
|
555
|
+
const result = await this.apply(currentStore, delta, validatedOptions);
|
|
556
|
+
currentStore = result.store;
|
|
557
|
+
receipts.push(result.receipt);
|
|
558
|
+
results.push(result.receipt);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return results;
|
|
562
|
+
},
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Get current store state.
|
|
566
|
+
* @returns {Store} Current store
|
|
567
|
+
*/
|
|
568
|
+
getCurrentStore() {
|
|
569
|
+
return createStore(currentStore.getQuads());
|
|
570
|
+
},
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Get all receipts.
|
|
574
|
+
* @returns {Receipt[]} Array of receipts
|
|
575
|
+
*/
|
|
576
|
+
getReceipts() {
|
|
577
|
+
return [...receipts];
|
|
578
|
+
},
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Reset session to initial state.
|
|
582
|
+
*/
|
|
583
|
+
reset() {
|
|
584
|
+
currentStore = createStore(initialStore.getQuads());
|
|
585
|
+
deltas.length = 0;
|
|
586
|
+
receipts.length = 0;
|
|
587
|
+
},
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Get session statistics.
|
|
591
|
+
* @returns {Object} Session statistics
|
|
592
|
+
*/
|
|
593
|
+
getStats() {
|
|
594
|
+
const committedCount = receipts.filter(r => r.committed).length;
|
|
595
|
+
const failedCount = receipts.length - committedCount;
|
|
596
|
+
|
|
597
|
+
return {
|
|
598
|
+
deltaCount: deltas.length,
|
|
599
|
+
receiptCount: receipts.length,
|
|
600
|
+
committedCount,
|
|
601
|
+
failedCount,
|
|
602
|
+
successRate: receipts.length > 0 ? committedCount / receipts.length : 0,
|
|
603
|
+
};
|
|
604
|
+
},
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Get transaction manager statistics.
|
|
610
|
+
* @returns {Object} Manager statistics
|
|
611
|
+
*/
|
|
612
|
+
getStats() {
|
|
613
|
+
const preHooks = this.hooks.filter(h => h.mode === 'pre').length;
|
|
614
|
+
const postHooks = this.hooks.filter(h => h.mode === 'post').length;
|
|
615
|
+
|
|
616
|
+
const stats = {
|
|
617
|
+
totalHooks: this.hooks.length,
|
|
618
|
+
preHooks,
|
|
619
|
+
postHooks,
|
|
620
|
+
maxHooks: this.options.maxHooks,
|
|
621
|
+
strictMode: this.options.strictMode,
|
|
622
|
+
afterHashOnly: this.options.afterHashOnly,
|
|
623
|
+
lockchainEnabled: this.options.enableLockchain,
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
// Add lockchain stats if enabled
|
|
627
|
+
if (this.lockchainWriter) {
|
|
628
|
+
stats.lockchain = this.lockchainWriter.getStats();
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return stats;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Commit pending lockchain entries
|
|
636
|
+
* @returns {Promise<Object>} Commit result
|
|
637
|
+
*/
|
|
638
|
+
async commitLockchain() {
|
|
639
|
+
if (!this.lockchainWriter) {
|
|
640
|
+
throw new Error('Lockchain is not enabled');
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return this.lockchainWriter.commitBatch();
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Submit a proposal to the resolution layer
|
|
648
|
+
* @param {string} agentId - Agent identifier
|
|
649
|
+
* @param {Object} delta - Proposed delta
|
|
650
|
+
* @param {Object} [options] - Proposal options
|
|
651
|
+
* @returns {Promise<string>} Proposal ID
|
|
652
|
+
*/
|
|
653
|
+
async submitProposal(agentId, delta, options = {}) {
|
|
654
|
+
if (!this.resolutionLayer) {
|
|
655
|
+
throw new Error('Resolution layer is not enabled');
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return this.resolutionLayer.submitProposal(agentId, delta, options);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Resolve proposals using the resolution layer
|
|
663
|
+
* @param {Array<string>} proposalIds - Proposal IDs to resolve
|
|
664
|
+
* @param {Object} [strategy] - Resolution strategy
|
|
665
|
+
* @returns {Promise<Object>} Resolution result
|
|
666
|
+
*/
|
|
667
|
+
async resolveProposals(proposalIds, strategy = {}) {
|
|
668
|
+
if (!this.resolutionLayer) {
|
|
669
|
+
throw new Error('Resolution layer is not enabled');
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return this.resolutionLayer.resolveProposals(proposalIds, strategy);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Get resolution layer statistics
|
|
677
|
+
* @returns {Object} Resolution statistics
|
|
678
|
+
*/
|
|
679
|
+
getResolutionStats() {
|
|
680
|
+
if (!this.resolutionLayer) {
|
|
681
|
+
return { enabled: false };
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
return {
|
|
685
|
+
enabled: true,
|
|
686
|
+
...this.resolutionLayer.getStats(),
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Get manager statistics.
|
|
692
|
+
* @returns {Object} Statistics
|
|
693
|
+
*/
|
|
694
|
+
getStats() {
|
|
695
|
+
return {
|
|
696
|
+
hooks: this.hooks.length,
|
|
697
|
+
lockchain: this.lockchainWriter ? this.lockchainWriter.getStats() : null,
|
|
698
|
+
resolution: this.resolutionLayer ? this.resolutionLayer.getStats() : null,
|
|
699
|
+
performance: this.performanceMetrics,
|
|
700
|
+
observability: this.observability.getPerformanceMetrics(),
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Update performance metrics
|
|
706
|
+
* @param {number} duration - Transaction duration
|
|
707
|
+
* @param {boolean} success - Whether transaction succeeded
|
|
708
|
+
* @private
|
|
709
|
+
*/
|
|
710
|
+
_updatePerformanceMetrics(duration, success) {
|
|
711
|
+
const maxEntries = this.performanceMetrics._maxLatencyEntries;
|
|
712
|
+
|
|
713
|
+
// Prevent unbounded array growth - remove oldest before adding
|
|
714
|
+
if (this.performanceMetrics.transactionLatency.length >= maxEntries) {
|
|
715
|
+
this.performanceMetrics.transactionLatency.shift();
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
this.performanceMetrics.transactionLatency.push({
|
|
719
|
+
timestamp: Date.now(),
|
|
720
|
+
duration,
|
|
721
|
+
success,
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
this.performanceMetrics.totalTransactions++;
|
|
725
|
+
|
|
726
|
+
if (!success) {
|
|
727
|
+
this.performanceMetrics.errorCount++;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Reset mutex chain to prevent circular references
|
|
733
|
+
* @private
|
|
734
|
+
*/
|
|
735
|
+
_resetMutex() {
|
|
736
|
+
this._applyMutex = Promise.resolve();
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Cleanup transaction manager resources
|
|
741
|
+
*/
|
|
742
|
+
async cleanup() {
|
|
743
|
+
// Clear hooks
|
|
744
|
+
this.hooks.length = 0;
|
|
745
|
+
|
|
746
|
+
// Clear performance metrics
|
|
747
|
+
this.performanceMetrics.transactionLatency.length = 0;
|
|
748
|
+
this.performanceMetrics.errorCount = 0;
|
|
749
|
+
this.performanceMetrics.totalTransactions = 0;
|
|
750
|
+
|
|
751
|
+
// Cleanup lockchain writer
|
|
752
|
+
if (this.lockchainWriter && typeof this.lockchainWriter.cleanup === 'function') {
|
|
753
|
+
await this.lockchainWriter.cleanup();
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Cleanup resolution layer
|
|
757
|
+
if (this.resolutionLayer && typeof this.resolutionLayer.cleanup === 'function') {
|
|
758
|
+
await this.resolutionLayer.cleanup();
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Reset mutex
|
|
762
|
+
this._resetMutex();
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Print a receipt in a consistent format.
|
|
768
|
+
* @param {Receipt} receipt - The receipt to print
|
|
769
|
+
* @param {Object} [options] - Print options
|
|
770
|
+
* @param {boolean} [options.verbose=false] - Include detailed information
|
|
771
|
+
*/
|
|
772
|
+
export function printReceipt(receipt, options = {}) {
|
|
773
|
+
const { verbose = false } = options;
|
|
774
|
+
|
|
775
|
+
console.log(`📋 Transaction Receipt ${receipt.id}`);
|
|
776
|
+
console.log(` Status: ${receipt.committed ? '✅ Committed' : '❌ Failed'}`);
|
|
777
|
+
console.log(` Duration: ${receipt.durationMs}ms`);
|
|
778
|
+
|
|
779
|
+
if (receipt.actor) {
|
|
780
|
+
console.log(` Actor: ${receipt.actor}`);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (receipt.error) {
|
|
784
|
+
console.log(` Error: ${receipt.error}`);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
console.log(` Hooks: ${receipt.hookResults.length} executed`);
|
|
788
|
+
receipt.hookResults.forEach(result => {
|
|
789
|
+
const status = result.result ? '✅' : '❌';
|
|
790
|
+
console.log(` ${status} ${result.hookId} (${result.mode})`);
|
|
791
|
+
if (result.error) {
|
|
792
|
+
console.log(` Error: ${result.error}`);
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
if (receipt.hookErrors.length > 0) {
|
|
797
|
+
console.log(` Hook Errors: ${receipt.hookErrors.length}`);
|
|
798
|
+
receipt.hookErrors.forEach(error => {
|
|
799
|
+
console.log(` • ${error}`);
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (verbose) {
|
|
804
|
+
console.log(
|
|
805
|
+
` Delta: ${receipt.delta.additions.length} additions, ${receipt.delta.removals.length} removals`
|
|
806
|
+
);
|
|
807
|
+
console.log(` Before Hash: ${receipt.beforeHash.sha3.substring(0, 16)}...`);
|
|
808
|
+
console.log(` After Hash: ${receipt.afterHash.sha3.substring(0, 16)}...`);
|
|
809
|
+
}
|
|
810
|
+
}
|