@nifrajs/core 2.0.0 → 2.2.0
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/README.md +27 -0
- package/dist/capabilities.d.ts +54 -2
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +240 -5
- package/dist/capabilities.js.map +1 -1
- package/dist/durable-adapters.d.ts +241 -0
- package/dist/durable-adapters.d.ts.map +1 -0
- package/dist/durable-adapters.js +697 -0
- package/dist/durable-adapters.js.map +1 -0
- package/dist/durable-execution.d.ts +349 -0
- package/dist/durable-execution.d.ts.map +1 -0
- package/dist/durable-execution.js +1173 -0
- package/dist/durable-execution.js.map +1 -0
- package/dist/effect-lifecycle.d.ts +42 -0
- package/dist/effect-lifecycle.d.ts.map +1 -0
- package/dist/effect-lifecycle.js +66 -0
- package/dist/effect-lifecycle.js.map +1 -0
- package/dist/effect-scope.d.ts +75 -0
- package/dist/effect-scope.d.ts.map +1 -0
- package/dist/effect-scope.js +177 -0
- package/dist/effect-scope.js.map +1 -0
- package/dist/idempotency-plugin.d.ts +5 -0
- package/dist/idempotency-plugin.d.ts.map +1 -1
- package/dist/idempotency-plugin.js +8 -0
- package/dist/idempotency-plugin.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/capability-runtime.d.ts +62 -1
- package/dist/internal/capability-runtime.d.ts.map +1 -1
- package/dist/internal/capability-runtime.js +11 -2
- package/dist/internal/capability-runtime.js.map +1 -1
- package/dist/internal/effect-execution.d.ts +16 -0
- package/dist/internal/effect-execution.d.ts.map +1 -0
- package/dist/internal/effect-execution.js +50 -0
- package/dist/internal/effect-execution.js.map +1 -0
- package/dist/internal/safe-equal.d.ts +20 -0
- package/dist/internal/safe-equal.d.ts.map +1 -0
- package/dist/internal/safe-equal.js +31 -0
- package/dist/internal/safe-equal.js.map +1 -0
- package/dist/ledger.d.ts +14 -6
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +31 -5
- package/dist/ledger.js.map +1 -1
- package/dist/reconciliation-worker.d.ts +108 -0
- package/dist/reconciliation-worker.d.ts.map +1 -0
- package/dist/reconciliation-worker.js +239 -0
- package/dist/reconciliation-worker.js.map +1 -0
- package/dist/router/pattern.d.ts +26 -1
- package/dist/router/pattern.d.ts.map +1 -1
- package/dist/router/pattern.js +164 -8
- package/dist/router/pattern.js.map +1 -1
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/router.js +49 -1
- package/dist/router/router.js.map +1 -1
- package/dist/server/idempotency-lane.d.ts +1 -0
- package/dist/server/idempotency-lane.d.ts.map +1 -1
- package/dist/server/idempotency-lane.js +61 -37
- package/dist/server/idempotency-lane.js.map +1 -1
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js.map +1 -1
- package/dist/server/server.d.ts +12 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +33 -2
- package/dist/server/server.js.map +1 -1
- package/dist/server/transport-lane.d.ts +28 -0
- package/dist/server/transport-lane.d.ts.map +1 -0
- package/dist/server/transport-lane.js +106 -0
- package/dist/server/transport-lane.js.map +1 -0
- package/dist/server/websocket.d.ts +6 -0
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +10 -1
- package/dist/server/websocket.js.map +1 -1
- package/dist/transport-codec-rich.d.ts +8 -0
- package/dist/transport-codec-rich.d.ts.map +1 -0
- package/dist/transport-codec-rich.js +14 -0
- package/dist/transport-codec-rich.js.map +1 -0
- package/dist/transport-codec.d.ts +31 -0
- package/dist/transport-codec.d.ts.map +1 -0
- package/dist/transport-codec.js +214 -0
- package/dist/transport-codec.js.map +1 -0
- package/dist/transport-plugin.d.ts +3 -0
- package/dist/transport-plugin.d.ts.map +1 -0
- package/dist/transport-plugin.js +3 -0
- package/dist/transport-plugin.js.map +1 -0
- package/dist/wire.d.ts +58 -0
- package/dist/wire.d.ts.map +1 -0
- package/dist/wire.js +445 -0
- package/dist/wire.js.map +1 -0
- package/package.json +46 -1
- package/src/capabilities.ts +306 -5
- package/src/durable-adapters.ts +1039 -0
- package/src/durable-execution.ts +1676 -0
- package/src/effect-lifecycle.ts +120 -0
- package/src/effect-scope.ts +267 -0
- package/src/idempotency-plugin.ts +10 -0
- package/src/index.ts +1 -1
- package/src/internal/capability-runtime.ts +87 -1
- package/src/internal/effect-execution.ts +70 -0
- package/src/internal/safe-equal.ts +27 -0
- package/src/ledger.ts +45 -11
- package/src/reconciliation-worker.ts +372 -0
- package/src/router/pattern.ts +179 -8
- package/src/router/router.ts +75 -1
- package/src/server/idempotency-lane.ts +68 -34
- package/src/server/respond.ts +1 -0
- package/src/server/server.ts +46 -0
- package/src/server/transport-lane.ts +141 -0
- package/src/server/websocket.ts +15 -1
- package/src/transport-codec-rich.ts +20 -0
- package/src/transport-codec.ts +284 -0
- package/src/transport-plugin.ts +2 -0
- package/src/wire.ts +474 -0
package/src/capabilities.ts
CHANGED
|
@@ -4,17 +4,45 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { DataClassification } from "./classification.ts"
|
|
7
|
+
import { type EffectLifecycleObserver, effectTraceParentOf } from "./effect-lifecycle.ts"
|
|
8
|
+
import { createEffectScope } from "./effect-scope.ts"
|
|
7
9
|
import {
|
|
10
|
+
type AroundCapabilityOptions,
|
|
8
11
|
CAPABILITY_GUARD,
|
|
12
|
+
type CapabilityApprovalGate,
|
|
13
|
+
type CapabilityApprovalInput,
|
|
14
|
+
type CapabilityExecutionIdentity,
|
|
15
|
+
type CapabilityExecutionJournal,
|
|
9
16
|
type CapabilityGuard,
|
|
17
|
+
type CapabilityInterceptor,
|
|
18
|
+
type CapabilityInterceptorEvent,
|
|
19
|
+
type CapabilityInterceptorNext,
|
|
10
20
|
type CapabilityUseEvent,
|
|
21
|
+
type RegisteredCapabilityInterceptor,
|
|
11
22
|
validCapabilityId,
|
|
12
23
|
} from "./internal/capability-runtime.ts"
|
|
24
|
+
import { effectScopeForContext } from "./internal/effect-execution.ts"
|
|
13
25
|
import { NIFRA_ASSURANCE_IDS } from "./internal/route-assurance.ts"
|
|
14
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
type EffectCost,
|
|
28
|
+
type EffectPhase,
|
|
29
|
+
effectLedgerOf,
|
|
30
|
+
normalizeEffectMetadata,
|
|
31
|
+
} from "./ledger.ts"
|
|
15
32
|
import { reflectRoutes } from "./reflection.ts"
|
|
16
33
|
|
|
17
|
-
export type {
|
|
34
|
+
export type {
|
|
35
|
+
AroundCapabilityOptions,
|
|
36
|
+
CapabilityApprovalGate,
|
|
37
|
+
CapabilityApprovalInput,
|
|
38
|
+
CapabilityExecutionIdentity,
|
|
39
|
+
CapabilityExecutionJournal,
|
|
40
|
+
CapabilityInterceptor,
|
|
41
|
+
CapabilityInterceptorEvent,
|
|
42
|
+
CapabilityInterceptorNext,
|
|
43
|
+
CapabilityUseEvent,
|
|
44
|
+
EffectLifecycleObserver,
|
|
45
|
+
}
|
|
18
46
|
|
|
19
47
|
export type CapabilityZone = "domain" | "operational"
|
|
20
48
|
export type CapabilityAccess = "read" | "write"
|
|
@@ -409,6 +437,162 @@ export interface CapabilityOutcomeOptions extends UseCapabilityOptions {
|
|
|
409
437
|
readonly error?: { readonly code: string }
|
|
410
438
|
}
|
|
411
439
|
|
|
440
|
+
/** Durable controls consumed by `executeCapability`; none of these fields enter the effect ledger. */
|
|
441
|
+
export interface CapabilityExecutionOptions extends UseCapabilityOptions {
|
|
442
|
+
readonly approval?: CapabilityApprovalInput & { readonly gate: CapabilityApprovalGate }
|
|
443
|
+
readonly journal?: CapabilityExecutionJournal
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** Context passed to the owned effect callback. Use the signal for cancellation-aware I/O. */
|
|
447
|
+
export interface CapabilityExecutionContext {
|
|
448
|
+
readonly effectId: string
|
|
449
|
+
readonly signal: AbortSignal
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export type CapabilityExecutor<T> = (execution: CapabilityExecutionContext) => T | PromiseLike<T>
|
|
453
|
+
|
|
454
|
+
/** A capability admission policy returned without calling `next()`. */
|
|
455
|
+
export class CapabilityDeniedError extends Error {
|
|
456
|
+
constructor(
|
|
457
|
+
public readonly capability: string,
|
|
458
|
+
public readonly effectId: string,
|
|
459
|
+
) {
|
|
460
|
+
super(`capability assurance: ${capability} was denied by an async interceptor`)
|
|
461
|
+
this.name = "CapabilityDeniedError"
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** A capability admission policy exceeded its configured bound. */
|
|
466
|
+
export class CapabilityInterceptorTimeoutError extends Error {
|
|
467
|
+
constructor(
|
|
468
|
+
public readonly capability: string,
|
|
469
|
+
public readonly effectId: string,
|
|
470
|
+
public readonly timeoutMs: number,
|
|
471
|
+
) {
|
|
472
|
+
super(`capability assurance: ${capability} admission timed out after ${timeoutMs}ms`)
|
|
473
|
+
this.name = "CapabilityInterceptorTimeoutError"
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** The request was cancelled while capability admission was pending. */
|
|
478
|
+
export class CapabilityAdmissionAbortedError extends Error {
|
|
479
|
+
constructor(
|
|
480
|
+
public readonly capability: string,
|
|
481
|
+
public readonly effectId: string,
|
|
482
|
+
) {
|
|
483
|
+
super(`capability assurance: ${capability} admission was aborted`)
|
|
484
|
+
this.name = "CapabilityAdmissionAbortedError"
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** An interceptor called its one-shot `next()` continuation more than once. */
|
|
489
|
+
export class CapabilityInterceptorProtocolError extends Error {
|
|
490
|
+
constructor(
|
|
491
|
+
public readonly capability: string,
|
|
492
|
+
public readonly effectId: string,
|
|
493
|
+
) {
|
|
494
|
+
super(`capability assurance: ${capability} interceptor called next() more than once`)
|
|
495
|
+
this.name = "CapabilityInterceptorProtocolError"
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** The effect may have committed, but its durable terminal transition could not be recorded. */
|
|
500
|
+
export class CapabilityJournalTransitionError extends Error {
|
|
501
|
+
constructor(
|
|
502
|
+
public readonly capability: string,
|
|
503
|
+
public readonly effectId: string,
|
|
504
|
+
public readonly transition: "committed",
|
|
505
|
+
) {
|
|
506
|
+
super(`capability assurance: ${capability} durable ${transition} transition failed`)
|
|
507
|
+
this.name = "CapabilityJournalTransitionError"
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
let unboundedCapabilitySignal: AbortSignal | undefined
|
|
512
|
+
|
|
513
|
+
function signalFor(context: object): AbortSignal {
|
|
514
|
+
const signal = (context as { readonly signal?: AbortSignal }).signal
|
|
515
|
+
if (signal !== undefined) return signal
|
|
516
|
+
unboundedCapabilitySignal ??= new AbortController().signal
|
|
517
|
+
return unboundedCapabilitySignal
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function admissionErrorCode(error: unknown): string {
|
|
521
|
+
if (error instanceof CapabilityDeniedError) return "admission_denied"
|
|
522
|
+
if (error instanceof CapabilityInterceptorTimeoutError) return "admission_timeout"
|
|
523
|
+
if (error instanceof CapabilityAdmissionAbortedError) return "admission_aborted"
|
|
524
|
+
if (error instanceof CapabilityInterceptorProtocolError) return "admission_protocol"
|
|
525
|
+
return "admission_failed"
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async function withInterceptorBound(
|
|
529
|
+
registration: RegisteredCapabilityInterceptor,
|
|
530
|
+
parentSignal: AbortSignal,
|
|
531
|
+
capability: string,
|
|
532
|
+
effectId: string,
|
|
533
|
+
run: (signal: AbortSignal) => Promise<void>,
|
|
534
|
+
): Promise<void> {
|
|
535
|
+
if (parentSignal.aborted) throw new CapabilityAdmissionAbortedError(capability, effectId)
|
|
536
|
+
const controller = new AbortController()
|
|
537
|
+
let timeout: ReturnType<typeof setTimeout> | undefined
|
|
538
|
+
let rejectGate: ((error: Error) => void) | undefined
|
|
539
|
+
const gate = new Promise<never>((_resolve, reject) => {
|
|
540
|
+
rejectGate = reject
|
|
541
|
+
})
|
|
542
|
+
const abort = (): void => {
|
|
543
|
+
const error = new CapabilityAdmissionAbortedError(capability, effectId)
|
|
544
|
+
controller.abort(error)
|
|
545
|
+
rejectGate?.(error)
|
|
546
|
+
}
|
|
547
|
+
parentSignal.addEventListener("abort", abort, { once: true })
|
|
548
|
+
timeout = setTimeout(() => {
|
|
549
|
+
const error = new CapabilityInterceptorTimeoutError(
|
|
550
|
+
capability,
|
|
551
|
+
effectId,
|
|
552
|
+
registration.timeoutMs,
|
|
553
|
+
)
|
|
554
|
+
controller.abort(error)
|
|
555
|
+
rejectGate?.(error)
|
|
556
|
+
}, registration.timeoutMs)
|
|
557
|
+
try {
|
|
558
|
+
await Promise.race([run(controller.signal), gate])
|
|
559
|
+
} finally {
|
|
560
|
+
clearTimeout(timeout)
|
|
561
|
+
parentSignal.removeEventListener("abort", abort)
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async function runCapabilityInterceptors(
|
|
566
|
+
registrations: readonly RegisteredCapabilityInterceptor[],
|
|
567
|
+
baseEvent: Omit<CapabilityInterceptorEvent, "signal">,
|
|
568
|
+
parentSignal: AbortSignal,
|
|
569
|
+
index = 0,
|
|
570
|
+
): Promise<void> {
|
|
571
|
+
const registration = registrations[index]
|
|
572
|
+
if (registration === undefined) return
|
|
573
|
+
let nextPromise: Promise<void> | undefined
|
|
574
|
+
await withInterceptorBound(
|
|
575
|
+
registration,
|
|
576
|
+
parentSignal,
|
|
577
|
+
baseEvent.capability,
|
|
578
|
+
baseEvent.effectId,
|
|
579
|
+
async (signal) => {
|
|
580
|
+
const event = Object.freeze({ ...baseEvent, signal })
|
|
581
|
+
await registration.interceptor(event, () => {
|
|
582
|
+
if (nextPromise !== undefined) {
|
|
583
|
+
throw new CapabilityInterceptorProtocolError(baseEvent.capability, baseEvent.effectId)
|
|
584
|
+
}
|
|
585
|
+
nextPromise = runCapabilityInterceptors(registrations, baseEvent, signal, index + 1)
|
|
586
|
+
return nextPromise
|
|
587
|
+
})
|
|
588
|
+
},
|
|
589
|
+
)
|
|
590
|
+
if (nextPromise === undefined) {
|
|
591
|
+
throw new CapabilityDeniedError(baseEvent.capability, baseEvent.effectId)
|
|
592
|
+
}
|
|
593
|
+
await nextPromise
|
|
594
|
+
}
|
|
595
|
+
|
|
412
596
|
function guardFor(context: object, capability: string): CapabilityGuard {
|
|
413
597
|
if (!validCapabilityId(capability)) {
|
|
414
598
|
throw new Error(
|
|
@@ -444,6 +628,7 @@ export function useCapability(
|
|
|
444
628
|
// Validate/append the intent before private admission hooks debit budgets. Overflow or malformed
|
|
445
629
|
// evidence therefore cannot consume quota and then fail the request.
|
|
446
630
|
ledger?.append({ capability, ...options, phase: "intent" })
|
|
631
|
+
guard.trackEffect?.(context, false)
|
|
447
632
|
guard.onUse?.({ capability, method: guard.method, path: guard.path })
|
|
448
633
|
}
|
|
449
634
|
|
|
@@ -453,10 +638,126 @@ export function recordCapabilityOutcome(
|
|
|
453
638
|
capability: string,
|
|
454
639
|
options: CapabilityOutcomeOptions,
|
|
455
640
|
): void {
|
|
456
|
-
guardFor(context, capability)
|
|
641
|
+
const guard = guardFor(context, capability)
|
|
457
642
|
const ledger = effectLedgerOf(context)
|
|
458
|
-
|
|
459
|
-
|
|
643
|
+
ledger?.append({ capability, ...options })
|
|
644
|
+
if (options.phase === "committed" || options.phase === "compensated") {
|
|
645
|
+
guard.trackEffect?.(context, true)
|
|
646
|
+
} else {
|
|
647
|
+
guard.trackEffect?.(context, false)
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Execute one owned effect behind a fail-closed capability boundary. The boundary assigns a stable
|
|
653
|
+
* effect id, records intent before execution, and records exactly one terminal outcome automatically.
|
|
654
|
+
* The callback result and errors never enter the token-only ledger.
|
|
655
|
+
*/
|
|
656
|
+
export async function executeCapability<T>(
|
|
657
|
+
context: object,
|
|
658
|
+
capability: string,
|
|
659
|
+
options: CapabilityExecutionOptions,
|
|
660
|
+
executor: CapabilityExecutor<T>,
|
|
661
|
+
): Promise<T> {
|
|
662
|
+
if (typeof executor !== "function") {
|
|
663
|
+
throw new TypeError("capability assurance: executeCapability executor must be a function")
|
|
664
|
+
}
|
|
665
|
+
const guard = guardFor(context, capability)
|
|
666
|
+
const ledger = effectLedgerOf(context)
|
|
667
|
+
const effectId = crypto.randomUUID()
|
|
668
|
+
const metadata = normalizeEffectMetadata(options)
|
|
669
|
+
const trace = effectTraceParentOf(context)
|
|
670
|
+
const identity = options.approval
|
|
671
|
+
const requestEvidence = effectScopeForContext(context)
|
|
672
|
+
const scope = createEffectScope({}, requestEvidence)
|
|
673
|
+
const result = await scope.run(
|
|
674
|
+
{
|
|
675
|
+
effectId,
|
|
676
|
+
capability,
|
|
677
|
+
...metadata,
|
|
678
|
+
signal: signalFor(context),
|
|
679
|
+
observers: guard.observers,
|
|
680
|
+
...(trace === undefined ? {} : { trace }),
|
|
681
|
+
errorCode: (error, began) => (began ? "execution_failed" : admissionErrorCode(error)),
|
|
682
|
+
async admit(execution) {
|
|
683
|
+
if (identity !== undefined) {
|
|
684
|
+
await identity.gate.authorize({
|
|
685
|
+
effectId: execution.effectId,
|
|
686
|
+
capability,
|
|
687
|
+
...(metadata.target === undefined ? {} : { target: metadata.target }),
|
|
688
|
+
...(metadata.digest === undefined ? {} : { digest: metadata.digest }),
|
|
689
|
+
identity: { tenantId: identity.tenantId, principalId: identity.principalId },
|
|
690
|
+
...(identity.resumeToken === undefined ? {} : { resumeToken: identity.resumeToken }),
|
|
691
|
+
signal: execution.signal,
|
|
692
|
+
})
|
|
693
|
+
}
|
|
694
|
+
guard.onUse?.({ capability, method: guard.method, path: guard.path })
|
|
695
|
+
if (guard.interceptors.length > 0) {
|
|
696
|
+
await runCapabilityInterceptors(
|
|
697
|
+
guard.interceptors,
|
|
698
|
+
Object.freeze({
|
|
699
|
+
capability,
|
|
700
|
+
method: guard.method,
|
|
701
|
+
path: guard.path,
|
|
702
|
+
effectId: execution.effectId,
|
|
703
|
+
...metadata,
|
|
704
|
+
}),
|
|
705
|
+
execution.signal,
|
|
706
|
+
)
|
|
707
|
+
}
|
|
708
|
+
},
|
|
709
|
+
transitions: {
|
|
710
|
+
async intent(execution) {
|
|
711
|
+
ledger?.append({ capability, effectId: execution.effectId, ...metadata, phase: "intent" })
|
|
712
|
+
await options.journal?.intent({
|
|
713
|
+
effectId: execution.effectId,
|
|
714
|
+
capability,
|
|
715
|
+
...(metadata.target === undefined ? {} : { target: metadata.target }),
|
|
716
|
+
...(metadata.digest === undefined ? {} : { digest: metadata.digest }),
|
|
717
|
+
...(identity === undefined
|
|
718
|
+
? {}
|
|
719
|
+
: { identity: { tenantId: identity.tenantId, principalId: identity.principalId } }),
|
|
720
|
+
})
|
|
721
|
+
},
|
|
722
|
+
async executing(execution) {
|
|
723
|
+
await options.journal?.executing(execution.effectId)
|
|
724
|
+
},
|
|
725
|
+
async committed(execution) {
|
|
726
|
+
try {
|
|
727
|
+
await options.journal?.committed(execution.effectId)
|
|
728
|
+
} catch {
|
|
729
|
+
throw new CapabilityJournalTransitionError(capability, execution.effectId, "committed")
|
|
730
|
+
}
|
|
731
|
+
},
|
|
732
|
+
async failed(execution, failure) {
|
|
733
|
+
try {
|
|
734
|
+
await options.journal?.failed(execution.effectId, {
|
|
735
|
+
began: failure.began,
|
|
736
|
+
errorCode: failure.errorCode,
|
|
737
|
+
})
|
|
738
|
+
} finally {
|
|
739
|
+
try {
|
|
740
|
+
ledger?.append({
|
|
741
|
+
capability,
|
|
742
|
+
effectId: execution.effectId,
|
|
743
|
+
...metadata,
|
|
744
|
+
phase: "failed",
|
|
745
|
+
error: { code: failure.errorCode },
|
|
746
|
+
})
|
|
747
|
+
} catch {
|
|
748
|
+
// Preserve the original effect failure when terminal evidence overflows.
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
(execution) =>
|
|
755
|
+
executor(Object.freeze({ effectId: execution.effectId, signal: execution.signal })),
|
|
756
|
+
)
|
|
757
|
+
// Durable commit and success telemetry are already final. A ledger overflow here must not rewrite
|
|
758
|
+
// the effect to failed/unknown or erase the request scope's proven committed evidence.
|
|
759
|
+
ledger?.append({ capability, effectId, ...metadata, phase: "committed" })
|
|
760
|
+
return result
|
|
460
761
|
}
|
|
461
762
|
|
|
462
763
|
/**
|