open-multi-agent-kit 0.90.7 → 0.90.9
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/CHANGELOG.md +52 -0
- package/README.md +17 -10
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +5 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/commands/doctor-provider-cli.d.ts +33 -0
- package/dist/commands/doctor-provider-cli.d.ts.map +1 -0
- package/dist/commands/doctor-provider-cli.js +215 -0
- package/dist/commands/doctor-provider-cli.js.map +1 -0
- package/dist/commands/doctor-provider-transport.d.ts +35 -0
- package/dist/commands/doctor-provider-transport.d.ts.map +1 -0
- package/dist/commands/doctor-provider-transport.js +185 -0
- package/dist/commands/doctor-provider-transport.js.map +1 -0
- package/dist/commands/doctor-provider.d.ts +121 -7
- package/dist/commands/doctor-provider.d.ts.map +1 -1
- package/dist/commands/doctor-provider.js +1295 -102
- package/dist/commands/doctor-provider.js.map +1 -1
- package/dist/commands/session-doctor-cli.d.ts +43 -0
- package/dist/commands/session-doctor-cli.d.ts.map +1 -0
- package/dist/commands/session-doctor-cli.js +494 -0
- package/dist/commands/session-doctor-cli.js.map +1 -0
- package/dist/commands/verify-ci.d.ts +21 -0
- package/dist/commands/verify-ci.d.ts.map +1 -0
- package/dist/commands/verify-ci.js +66 -0
- package/dist/commands/verify-ci.js.map +1 -0
- package/dist/core/agent-session-services.d.ts +3 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +3 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +116 -2
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +1015 -71
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/agent-tool-settings.d.ts +35 -0
- package/dist/core/agent-tool-settings.d.ts.map +1 -0
- package/dist/core/agent-tool-settings.js +155 -0
- package/dist/core/agent-tool-settings.js.map +1 -0
- package/dist/core/atomic-session-file.d.ts +14 -0
- package/dist/core/atomic-session-file.d.ts.map +1 -0
- package/dist/core/atomic-session-file.js +104 -0
- package/dist/core/atomic-session-file.js.map +1 -0
- package/dist/core/compaction/compaction.d.ts +6 -0
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +12 -1
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/compaction/hysteresis.d.ts +27 -0
- package/dist/core/compaction/hysteresis.d.ts.map +1 -0
- package/dist/core/compaction/hysteresis.js +84 -0
- package/dist/core/compaction/hysteresis.js.map +1 -0
- package/dist/core/compaction/index.d.ts +2 -0
- package/dist/core/compaction/index.d.ts.map +1 -1
- package/dist/core/compaction/index.js +2 -0
- package/dist/core/compaction/index.js.map +1 -1
- package/dist/core/compaction/transaction.d.ts +172 -0
- package/dist/core/compaction/transaction.d.ts.map +1 -0
- package/dist/core/compaction/transaction.js +824 -0
- package/dist/core/compaction/transaction.js.map +1 -0
- package/dist/core/context-budget-governor-v2.d.ts +2 -1
- package/dist/core/context-budget-governor-v2.d.ts.map +1 -1
- package/dist/core/context-budget-governor-v2.js +2 -1
- package/dist/core/context-budget-governor-v2.js.map +1 -1
- package/dist/core/context-budget-headroom.d.ts.map +1 -1
- package/dist/core/context-budget-headroom.js +5 -1
- package/dist/core/context-budget-headroom.js.map +1 -1
- package/dist/core/context-budget-reserved-tokens.d.ts +110 -0
- package/dist/core/context-budget-reserved-tokens.d.ts.map +1 -0
- package/dist/core/context-budget-reserved-tokens.js +188 -0
- package/dist/core/context-budget-reserved-tokens.js.map +1 -0
- package/dist/core/context-budget-v2-cache-invalidation.d.ts +133 -0
- package/dist/core/context-budget-v2-cache-invalidation.d.ts.map +1 -0
- package/dist/core/context-budget-v2-cache-invalidation.js +452 -0
- package/dist/core/context-budget-v2-cache-invalidation.js.map +1 -0
- package/dist/core/context-budget-v2-cache-keys.d.ts +3 -0
- package/dist/core/context-budget-v2-cache-keys.d.ts.map +1 -1
- package/dist/core/context-budget-v2-cache-keys.js +6 -0
- package/dist/core/context-budget-v2-cache-keys.js.map +1 -1
- package/dist/core/context-budget-v2-cache-provider.d.ts +8 -2
- package/dist/core/context-budget-v2-cache-provider.d.ts.map +1 -1
- package/dist/core/context-budget-v2-cache-provider.js +28 -5
- package/dist/core/context-budget-v2-cache-provider.js.map +1 -1
- package/dist/core/context-budget-v2-plan-cache-keys.d.ts.map +1 -1
- package/dist/core/context-budget-v2-plan-cache-keys.js +1 -0
- package/dist/core/context-budget-v2-plan-cache-keys.js.map +1 -1
- package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
- package/dist/core/context-budget-v2-planner.js +1 -0
- package/dist/core/context-budget-v2-planner.js.map +1 -1
- package/dist/core/context-budget-v2-selection.d.ts.map +1 -1
- package/dist/core/context-budget-v2-selection.js +9 -0
- package/dist/core/context-budget-v2-selection.js.map +1 -1
- package/dist/core/context-budget-v2-types.d.ts +4 -0
- package/dist/core/context-budget-v2-types.d.ts.map +1 -1
- package/dist/core/context-budget-v2-types.js.map +1 -1
- package/dist/core/durable-file-directory.d.ts +4 -0
- package/dist/core/durable-file-directory.d.ts.map +1 -0
- package/dist/core/durable-file-directory.js +76 -0
- package/dist/core/durable-file-directory.js.map +1 -0
- package/dist/core/durable-file-identity.d.ts +26 -0
- package/dist/core/durable-file-identity.d.ts.map +1 -0
- package/dist/core/durable-file-identity.js +118 -0
- package/dist/core/durable-file-identity.js.map +1 -0
- package/dist/core/durable-file-io.d.ts +29 -0
- package/dist/core/durable-file-io.d.ts.map +1 -0
- package/dist/core/durable-file-io.js +179 -0
- package/dist/core/durable-file-io.js.map +1 -0
- package/dist/core/durable-file-lock-cleanup.d.ts +12 -0
- package/dist/core/durable-file-lock-cleanup.d.ts.map +1 -0
- package/dist/core/durable-file-lock-cleanup.js +39 -0
- package/dist/core/durable-file-lock-cleanup.js.map +1 -0
- package/dist/core/durable-file-lock-owner.d.ts +18 -0
- package/dist/core/durable-file-lock-owner.d.ts.map +1 -0
- package/dist/core/durable-file-lock-owner.js +78 -0
- package/dist/core/durable-file-lock-owner.js.map +1 -0
- package/dist/core/durable-file-lock-storage.d.ts +35 -0
- package/dist/core/durable-file-lock-storage.d.ts.map +1 -0
- package/dist/core/durable-file-lock-storage.js +263 -0
- package/dist/core/durable-file-lock-storage.js.map +1 -0
- package/dist/core/durable-file-mode.d.ts +8 -0
- package/dist/core/durable-file-mode.d.ts.map +1 -0
- package/dist/core/durable-file-mode.js +41 -0
- package/dist/core/durable-file-mode.js.map +1 -0
- package/dist/core/extensions/loader.d.ts +12 -0
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +50 -37
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/types.d.ts +2 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/index.d.ts +6 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +5 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/loop-gates.d.ts.map +1 -1
- package/dist/core/loop-gates.js +25 -1
- package/dist/core/loop-gates.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +1 -1
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/pi-package-intake.d.ts +1 -0
- package/dist/core/pi-package-intake.d.ts.map +1 -1
- package/dist/core/pi-package-intake.js +7 -0
- package/dist/core/pi-package-intake.js.map +1 -1
- package/dist/core/redaction.d.ts +8 -0
- package/dist/core/redaction.d.ts.map +1 -0
- package/dist/core/redaction.js +31 -0
- package/dist/core/redaction.js.map +1 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +36 -0
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/run-journal-store.d.ts +53 -0
- package/dist/core/run-journal-store.d.ts.map +1 -0
- package/dist/core/run-journal-store.js +278 -0
- package/dist/core/run-journal-store.js.map +1 -0
- package/dist/core/run-journal.d.ts +172 -0
- package/dist/core/run-journal.d.ts.map +1 -0
- package/dist/core/run-journal.js +1053 -0
- package/dist/core/run-journal.js.map +1 -0
- package/dist/core/sdk.d.ts +7 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +94 -3
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-doctor-plan.d.ts +128 -0
- package/dist/core/session-doctor-plan.d.ts.map +1 -0
- package/dist/core/session-doctor-plan.js +833 -0
- package/dist/core/session-doctor-plan.js.map +1 -0
- package/dist/core/session-file-compaction.d.ts +6 -0
- package/dist/core/session-file-compaction.d.ts.map +1 -0
- package/dist/core/session-file-compaction.js +169 -0
- package/dist/core/session-file-compaction.js.map +1 -0
- package/dist/core/session-file-head.d.ts +10 -0
- package/dist/core/session-file-head.d.ts.map +1 -0
- package/dist/core/session-file-head.js +78 -0
- package/dist/core/session-file-head.js.map +1 -0
- package/dist/core/session-file-persistence.d.ts +30 -0
- package/dist/core/session-file-persistence.d.ts.map +1 -0
- package/dist/core/session-file-persistence.js +106 -0
- package/dist/core/session-file-persistence.js.map +1 -0
- package/dist/core/session-integrity.d.ts +43 -0
- package/dist/core/session-integrity.d.ts.map +1 -0
- package/dist/core/session-integrity.js +420 -0
- package/dist/core/session-integrity.js.map +1 -0
- package/dist/core/session-manager.d.ts +24 -4
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +242 -125
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-owner-lease.d.ts +22 -0
- package/dist/core/session-owner-lease.d.ts.map +1 -0
- package/dist/core/session-owner-lease.js +176 -0
- package/dist/core/session-owner-lease.js.map +1 -0
- package/dist/core/session-path-policy.d.ts +132 -0
- package/dist/core/session-path-policy.d.ts.map +1 -0
- package/dist/core/session-path-policy.js +780 -0
- package/dist/core/session-path-policy.js.map +1 -0
- package/dist/core/session-repair-plan.d.ts +49 -0
- package/dist/core/session-repair-plan.d.ts.map +1 -0
- package/dist/core/session-repair-plan.js +202 -0
- package/dist/core/session-repair-plan.js.map +1 -0
- package/dist/core/session-termination.d.ts +100 -0
- package/dist/core/session-termination.d.ts.map +1 -0
- package/dist/core/session-termination.js +403 -0
- package/dist/core/session-termination.js.map +1 -0
- package/dist/core/settings-manager.d.ts +32 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +52 -5
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +1 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/verified-bash-adapter.d.ts +36 -0
- package/dist/core/verified-bash-adapter.d.ts.map +1 -0
- package/dist/core/verified-bash-adapter.js +90 -0
- package/dist/core/verified-bash-adapter.js.map +1 -0
- package/dist/guardrails/command-redaction.d.ts +32 -0
- package/dist/guardrails/command-redaction.d.ts.map +1 -0
- package/dist/guardrails/command-redaction.js +776 -0
- package/dist/guardrails/command-redaction.js.map +1 -0
- package/dist/guardrails/evidence-attestation.d.ts +31 -0
- package/dist/guardrails/evidence-attestation.d.ts.map +1 -0
- package/dist/guardrails/evidence-attestation.js +178 -0
- package/dist/guardrails/evidence-attestation.js.map +1 -0
- package/dist/guardrails/evidence-receipt-store.d.ts +33 -0
- package/dist/guardrails/evidence-receipt-store.d.ts.map +1 -0
- package/dist/guardrails/evidence-receipt-store.js +446 -0
- package/dist/guardrails/evidence-receipt-store.js.map +1 -0
- package/dist/guardrails/evidence-receipt.d.ts +58 -0
- package/dist/guardrails/evidence-receipt.d.ts.map +1 -0
- package/dist/guardrails/evidence-receipt.js +389 -0
- package/dist/guardrails/evidence-receipt.js.map +1 -0
- package/dist/guardrails/evidence-system.d.ts +40 -9
- package/dist/guardrails/evidence-system.d.ts.map +1 -1
- package/dist/guardrails/evidence-system.js +379 -83
- package/dist/guardrails/evidence-system.js.map +1 -1
- package/dist/guardrails/replay-ledger-io.d.ts +6 -0
- package/dist/guardrails/replay-ledger-io.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-io.js +42 -0
- package/dist/guardrails/replay-ledger-io.js.map +1 -0
- package/dist/guardrails/replay-ledger-lock-owner.d.ts +32 -0
- package/dist/guardrails/replay-ledger-lock-owner.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-lock-owner.js +138 -0
- package/dist/guardrails/replay-ledger-lock-owner.js.map +1 -0
- package/dist/guardrails/replay-ledger-lock.d.ts +24 -0
- package/dist/guardrails/replay-ledger-lock.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-lock.js +135 -0
- package/dist/guardrails/replay-ledger-lock.js.map +1 -0
- package/dist/guardrails/replay-ledger-mutation-gate.d.ts +24 -0
- package/dist/guardrails/replay-ledger-mutation-gate.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-mutation-gate.js +205 -0
- package/dist/guardrails/replay-ledger-mutation-gate.js.map +1 -0
- package/dist/guardrails/replay-ledger-quarantine.d.ts +2 -0
- package/dist/guardrails/replay-ledger-quarantine.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-quarantine.js +19 -0
- package/dist/guardrails/replay-ledger-quarantine.js.map +1 -0
- package/dist/guardrails/replay-ledger-store.d.ts +32 -0
- package/dist/guardrails/replay-ledger-store.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-store.js +211 -0
- package/dist/guardrails/replay-ledger-store.js.map +1 -0
- package/dist/guardrails/shell-command-name-glob.d.ts +3 -0
- package/dist/guardrails/shell-command-name-glob.d.ts.map +1 -0
- package/dist/guardrails/shell-command-name-glob.js +233 -0
- package/dist/guardrails/shell-command-name-glob.js.map +1 -0
- package/dist/guardrails/shell-command-option.d.ts +7 -0
- package/dist/guardrails/shell-command-option.d.ts.map +1 -0
- package/dist/guardrails/shell-command-option.js +24 -0
- package/dist/guardrails/shell-command-option.js.map +1 -0
- package/dist/guardrails/shell-command-tokenizer.d.ts +29 -0
- package/dist/guardrails/shell-command-tokenizer.d.ts.map +1 -0
- package/dist/guardrails/shell-command-tokenizer.js +145 -0
- package/dist/guardrails/shell-command-tokenizer.js.map +1 -0
- package/dist/guardrails/shell-command-wrapper.d.ts +18 -0
- package/dist/guardrails/shell-command-wrapper.d.ts.map +1 -0
- package/dist/guardrails/shell-command-wrapper.js +42 -0
- package/dist/guardrails/shell-command-wrapper.js.map +1 -0
- package/dist/guardrails/verified-executor.d.ts +65 -0
- package/dist/guardrails/verified-executor.d.ts.map +1 -0
- package/dist/guardrails/verified-executor.js +171 -0
- package/dist/guardrails/verified-executor.js.map +1 -0
- package/dist/guardrails/workspace-fingerprint.d.ts +23 -0
- package/dist/guardrails/workspace-fingerprint.d.ts.map +1 -0
- package/dist/guardrails/workspace-fingerprint.js +634 -0
- package/dist/guardrails/workspace-fingerprint.js.map +1 -0
- package/dist/index.d.ts +15 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +12 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/control-panel-layout.d.ts.map +1 -1
- package/dist/modes/interactive/components/control-panel-layout.js +1 -1
- package/dist/modes/interactive/components/control-panel-layout.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +1 -1
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +3 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +59 -9
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +43 -2
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +23 -5
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +3 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/types/evidence.d.ts +215 -1
- package/dist/types/evidence.d.ts.map +1 -1
- package/dist/types/evidence.js.map +1 -1
- package/dist/verify-ci.d.ts +3 -0
- package/dist/verify-ci.d.ts.map +1 -0
- package/dist/verify-ci.js +30 -0
- package/dist/verify-ci.js.map +1 -0
- package/docs/compaction.md +44 -4
- package/docs/extensions.md +2 -0
- package/docs/providers.md +6 -0
- package/docs/sdk.md +238 -1
- package/docs/settings.md +67 -4
- package/docs/usage.md +38 -0
- package/examples/extensions/correctness-wall/README.md +6 -6
- package/examples/extensions/correctness-wall/index.ts +37 -11
- package/examples/extensions/custom-provider-anthropic/package-lock.json +5 -5
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +4 -4
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- package/package.json +4 -4
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { linkSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { createLockCandidate, fsyncDirectory, removeLockCandidate } from "./replay-ledger-io.js";
|
|
4
|
+
import { createLockOwner, errorCode, inspectProcessIdentity, lockOwnerIsDead, lockOwnersEqual, ReplayLedgerLockError, readLockOwner, } from "./replay-ledger-lock-owner.js";
|
|
5
|
+
import { ReplayLedgerMutationGate } from "./replay-ledger-mutation-gate.js";
|
|
6
|
+
export { fsyncDirectory, writeAll } from "./replay-ledger-io.js";
|
|
7
|
+
export { ReplayLedgerLockError } from "./replay-ledger-lock-owner.js";
|
|
8
|
+
const LOCK_ATTEMPTS = 200;
|
|
9
|
+
const LOCK_WAIT_MS = 5;
|
|
10
|
+
const sleeper = new Int32Array(new SharedArrayBuffer(4));
|
|
11
|
+
export class ReplayLedgerLock {
|
|
12
|
+
path;
|
|
13
|
+
gate;
|
|
14
|
+
processIdentity;
|
|
15
|
+
beforeReclaimRemove;
|
|
16
|
+
constructor(path, options = {}) {
|
|
17
|
+
this.path = path;
|
|
18
|
+
this.processIdentity = options.processIdentity ?? inspectProcessIdentity;
|
|
19
|
+
this.beforeReclaimRemove = options.beforeReclaimRemove;
|
|
20
|
+
this.gate = new ReplayLedgerMutationGate(`${path}.reclaim`, {
|
|
21
|
+
processIdentity: this.processIdentity,
|
|
22
|
+
beforeReclaimRemove: options.beforeMutationGateReclaimRemove,
|
|
23
|
+
beforeRelease: options.beforeMutationGateRelease,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
withMutationGate(operation) {
|
|
27
|
+
return this.gate.run(operation);
|
|
28
|
+
}
|
|
29
|
+
tryAcquire(candidate, expected) {
|
|
30
|
+
return this.withMutationGate(() => {
|
|
31
|
+
try {
|
|
32
|
+
linkSync(candidate, this.path);
|
|
33
|
+
fsyncDirectory(dirname(this.path));
|
|
34
|
+
const acquired = readLockOwner(this.path);
|
|
35
|
+
if (!lockOwnersEqual(acquired, expected)) {
|
|
36
|
+
throw new ReplayLedgerLockError("replay lock ownership changed during acquisition");
|
|
37
|
+
}
|
|
38
|
+
return acquired;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (errorCode(error) === "EEXIST")
|
|
42
|
+
return undefined;
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
reclaim(observed) {
|
|
48
|
+
this.withMutationGate(() => {
|
|
49
|
+
let current;
|
|
50
|
+
try {
|
|
51
|
+
current = readLockOwner(this.path);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (errorCode(error) === "ENOENT")
|
|
55
|
+
return;
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
if (!lockOwnersEqual(current, observed))
|
|
59
|
+
return;
|
|
60
|
+
this.beforeReclaimRemove?.();
|
|
61
|
+
let finalOwner;
|
|
62
|
+
try {
|
|
63
|
+
finalOwner = readLockOwner(this.path);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (errorCode(error) === "ENOENT")
|
|
67
|
+
return;
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
if (!lockOwnersEqual(finalOwner, observed))
|
|
71
|
+
return;
|
|
72
|
+
unlinkSync(this.path);
|
|
73
|
+
fsyncDirectory(dirname(this.path));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
acquire(owner) {
|
|
77
|
+
const candidate = createLockCandidate(this.path, owner);
|
|
78
|
+
try {
|
|
79
|
+
const expected = readLockOwner(candidate);
|
|
80
|
+
for (let attempt = 0; attempt < LOCK_ATTEMPTS; attempt++) {
|
|
81
|
+
let current;
|
|
82
|
+
try {
|
|
83
|
+
current = readLockOwner(this.path);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (errorCode(error) !== "ENOENT")
|
|
87
|
+
throw error;
|
|
88
|
+
const acquired = this.tryAcquire(candidate, expected);
|
|
89
|
+
if (acquired !== undefined)
|
|
90
|
+
return acquired;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (lockOwnerIsDead(current.owner, this.processIdentity)) {
|
|
94
|
+
this.reclaim(current);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
Atomics.wait(sleeper, 0, 0, LOCK_WAIT_MS);
|
|
98
|
+
}
|
|
99
|
+
throw new ReplayLedgerLockError("replay ledger interprocess lock is unavailable");
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
removeLockCandidate(candidate);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
release(acquired) {
|
|
106
|
+
this.withMutationGate(() => {
|
|
107
|
+
const current = readLockOwner(this.path);
|
|
108
|
+
if (!lockOwnersEqual(current, acquired)) {
|
|
109
|
+
throw new ReplayLedgerLockError("replay lock ownership changed before release");
|
|
110
|
+
}
|
|
111
|
+
unlinkSync(this.path);
|
|
112
|
+
fsyncDirectory(dirname(this.path));
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
run(operation) {
|
|
116
|
+
const acquired = this.acquire(createLockOwner(this.processIdentity));
|
|
117
|
+
let result;
|
|
118
|
+
try {
|
|
119
|
+
result = operation();
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
this.release(acquired);
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
if (result !== null &&
|
|
126
|
+
(typeof result === "object" || typeof result === "function") &&
|
|
127
|
+
"then" in result &&
|
|
128
|
+
typeof result.then === "function") {
|
|
129
|
+
return Promise.resolve(result).finally(() => this.release(acquired));
|
|
130
|
+
}
|
|
131
|
+
this.release(acquired);
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=replay-ledger-lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay-ledger-lock.js","sourceRoot":"","sources":["../../src/guardrails/replay-ledger-lock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACjG,OAAO,EACN,eAAe,EACf,SAAS,EACT,sBAAsB,EAGtB,eAAe,EACf,eAAe,EACf,qBAAqB,EAErB,aAAa,GACb,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAE5E,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEjE,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAEtE,MAAM,aAAa,GAAG,GAAG,CAAC;AAC1B,MAAM,YAAY,GAAG,CAAC,CAAC;AACvB,MAAM,OAAO,GAAG,IAAI,UAAU,CAAC,IAAI,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC;AASzD,MAAM,OAAO,gBAAgB;IACX,IAAI,CAAS;IACb,IAAI,CAA2B;IAC/B,eAAe,CAAoC;IACnD,mBAAmB,CAA2B;IAE/D,YAAY,IAAY,EAAE,OAAO,GAA4B,EAAE,EAAE;QAChE,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,eAAe,GAAG,OAAO,CAAC,eAAe,IAAI,sBAAsB,CAAC;QACzE,IAAI,CAAC,mBAAmB,GAAG,OAAO,CAAC,mBAAmB,CAAC;QACvD,IAAI,CAAC,IAAI,GAAG,IAAI,wBAAwB,CAAC,GAAG,IAAI,UAAU,EAAE;YAC3D,eAAe,EAAE,IAAI,CAAC,eAAe;YACrC,mBAAmB,EAAE,OAAO,CAAC,+BAA+B;YAC5D,aAAa,EAAE,OAAO,CAAC,yBAAyB;SAChD,CAAC,CAAC;IAAA,CACH;IAEO,gBAAgB,CAAI,SAAkB,EAAK;QAClD,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAAA,CAChC;IAEO,UAAU,CAAC,SAAiB,EAAE,QAA2B,EAAiC;QACjG,OAAO,IAAI,CAAC,gBAAgB,CAAC,GAAG,EAAE,CAAC;YAClC,IAAI,CAAC;gBACJ,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;gBAC/B,cAAc,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;gBACnC,MAAM,QAAQ,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAC1C,IAAI,CAAC,eAAe,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAAE,CAAC;oBAC1C,MAAM,IAAI,qBAAqB,CAAC,kDAAkD,CAAC,CAAC;gBACrF,CAAC;gBACD,OAAO,QAAQ,CAAC;YACjB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,IAAI,SAAS,CAAC,KAAK,CAAC,KAAK,QAAQ;oBAAE,OAAO,SAAS,CAAC;gBACpD,MAAM,KAAK,CAAC;YACb,CAAC;QAAA,CACD,CAAC,CAAC;IAAA,CACH;IAEO,OAAO,CAAC,QAA2B,EAAQ;QAClD,IAAI,CAAC,gBAAgB,CAAC,GAAG,EAAE,CAAC;YAC3B,IAAI,OAA0B,CAAC;YAC/B,IAAI,CAAC;gBACJ,OAAO,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACpC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,IAAI,SAAS,CAAC,KAAK,CAAC,KAAK,QAAQ;oBAAE,OAAO;gBAC1C,MAAM,KAAK,CAAC;YACb,CAAC;YACD,IAAI,CAAC,eAAe,CAAC,OAAO,EAAE,QAAQ,CAAC;gBAAE,OAAO;YAChD,IAAI,CAAC,mBAAmB,EAAE,EAAE,CAAC;YAC7B,IAAI,UAA6B,CAAC;YAClC,IAAI,CAAC;gBACJ,UAAU,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACvC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,IAAI,SAAS,CAAC,KAAK,CAAC,KAAK,QAAQ;oBAAE,OAAO;gBAC1C,MAAM,KAAK,CAAC;YACb,CAAC;YACD,IAAI,CAAC,eAAe,CAAC,UAAU,EAAE,QAAQ,CAAC;gBAAE,OAAO;YACnD,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACtB,cAAc,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAAA,CACnC,CAAC,CAAC;IAAA,CACH;IAEO,OAAO,CAAC,KAAgB,EAAqB;QACpD,MAAM,SAAS,GAAG,mBAAmB,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QACxD,IAAI,CAAC;YACJ,MAAM,QAAQ,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC;YAC1C,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,aAAa,EAAE,OAAO,EAAE,EAAE,CAAC;gBAC1D,IAAI,OAA0B,CAAC;gBAC/B,IAAI,CAAC;oBACJ,OAAO,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACpC,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBAChB,IAAI,SAAS,CAAC,KAAK,CAAC,KAAK,QAAQ;wBAAE,MAAM,KAAK,CAAC;oBAC/C,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;oBACtD,IAAI,QAAQ,KAAK,SAAS;wBAAE,OAAO,QAAQ,CAAC;oBAC5C,SAAS;gBACV,CAAC;gBACD,IAAI,eAAe,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,eAAe,CAAC,EAAE,CAAC;oBAC1D,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;oBACtB,SAAS;gBACV,CAAC;gBACD,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,YAAY,CAAC,CAAC;YAC3C,CAAC;YACD,MAAM,IAAI,qBAAqB,CAAC,gDAAgD,CAAC,CAAC;QACnF,CAAC;gBAAS,CAAC;YACV,mBAAmB,CAAC,SAAS,CAAC,CAAC;QAChC,CAAC;IAAA,CACD;IAEO,OAAO,CAAC,QAA2B,EAAQ;QAClD,IAAI,CAAC,gBAAgB,CAAC,GAAG,EAAE,CAAC;YAC3B,MAAM,OAAO,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACzC,IAAI,CAAC,eAAe,CAAC,OAAO,EAAE,QAAQ,CAAC,EAAE,CAAC;gBACzC,MAAM,IAAI,qBAAqB,CAAC,8CAA8C,CAAC,CAAC;YACjF,CAAC;YACD,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACtB,cAAc,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAAA,CACnC,CAAC,CAAC;IAAA,CACH;IAED,GAAG,CAAI,SAAkB,EAAK;QAC7B,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC;QACrE,IAAI,MAAS,CAAC;QACd,IAAI,CAAC;YACJ,MAAM,GAAG,SAAS,EAAE,CAAC;QACtB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;YACvB,MAAM,KAAK,CAAC;QACb,CAAC;QACD,IACC,MAAM,KAAK,IAAI;YACf,CAAC,OAAO,MAAM,KAAK,QAAQ,IAAI,OAAO,MAAM,KAAK,UAAU,CAAC;YAC5D,MAAM,IAAI,MAAM;YAChB,OAAO,MAAM,CAAC,IAAI,KAAK,UAAU,EAChC,CAAC;YACF,OAAO,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAM,CAAC;QAC3E,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACvB,OAAO,MAAM,CAAC;IAAA,CACd;CACD","sourcesContent":["import { linkSync, unlinkSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { createLockCandidate, fsyncDirectory, removeLockCandidate } from \"./replay-ledger-io.ts\";\nimport {\n\tcreateLockOwner,\n\terrorCode,\n\tinspectProcessIdentity,\n\ttype LockOwner,\n\ttype LockOwnerSnapshot,\n\tlockOwnerIsDead,\n\tlockOwnersEqual,\n\tReplayLedgerLockError,\n\ttype ReplayLedgerProcessIdentityReader,\n\treadLockOwner,\n} from \"./replay-ledger-lock-owner.ts\";\nimport { ReplayLedgerMutationGate } from \"./replay-ledger-mutation-gate.ts\";\n\nexport { fsyncDirectory, writeAll } from \"./replay-ledger-io.ts\";\nexport type { ReplayLedgerProcessIdentity } from \"./replay-ledger-lock-owner.ts\";\nexport { ReplayLedgerLockError } from \"./replay-ledger-lock-owner.ts\";\n\nconst LOCK_ATTEMPTS = 200;\nconst LOCK_WAIT_MS = 5;\nconst sleeper = new Int32Array(new SharedArrayBuffer(4));\n\nexport interface ReplayLedgerLockOptions {\n\treadonly processIdentity?: ReplayLedgerProcessIdentityReader;\n\treadonly beforeReclaimRemove?: () => void;\n\treadonly beforeMutationGateReclaimRemove?: () => void;\n\treadonly beforeMutationGateRelease?: () => void;\n}\n\nexport class ReplayLedgerLock {\n\tprivate readonly path: string;\n\tprivate readonly gate: ReplayLedgerMutationGate;\n\tprivate readonly processIdentity: ReplayLedgerProcessIdentityReader;\n\tprivate readonly beforeReclaimRemove: (() => void) | undefined;\n\n\tconstructor(path: string, options: ReplayLedgerLockOptions = {}) {\n\t\tthis.path = path;\n\t\tthis.processIdentity = options.processIdentity ?? inspectProcessIdentity;\n\t\tthis.beforeReclaimRemove = options.beforeReclaimRemove;\n\t\tthis.gate = new ReplayLedgerMutationGate(`${path}.reclaim`, {\n\t\t\tprocessIdentity: this.processIdentity,\n\t\t\tbeforeReclaimRemove: options.beforeMutationGateReclaimRemove,\n\t\t\tbeforeRelease: options.beforeMutationGateRelease,\n\t\t});\n\t}\n\n\tprivate withMutationGate<T>(operation: () => T): T {\n\t\treturn this.gate.run(operation);\n\t}\n\n\tprivate tryAcquire(candidate: string, expected: LockOwnerSnapshot): LockOwnerSnapshot | undefined {\n\t\treturn this.withMutationGate(() => {\n\t\t\ttry {\n\t\t\t\tlinkSync(candidate, this.path);\n\t\t\t\tfsyncDirectory(dirname(this.path));\n\t\t\t\tconst acquired = readLockOwner(this.path);\n\t\t\t\tif (!lockOwnersEqual(acquired, expected)) {\n\t\t\t\t\tthrow new ReplayLedgerLockError(\"replay lock ownership changed during acquisition\");\n\t\t\t\t}\n\t\t\t\treturn acquired;\n\t\t\t} catch (error) {\n\t\t\t\tif (errorCode(error) === \"EEXIST\") return undefined;\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate reclaim(observed: LockOwnerSnapshot): void {\n\t\tthis.withMutationGate(() => {\n\t\t\tlet current: LockOwnerSnapshot;\n\t\t\ttry {\n\t\t\t\tcurrent = readLockOwner(this.path);\n\t\t\t} catch (error) {\n\t\t\t\tif (errorCode(error) === \"ENOENT\") return;\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tif (!lockOwnersEqual(current, observed)) return;\n\t\t\tthis.beforeReclaimRemove?.();\n\t\t\tlet finalOwner: LockOwnerSnapshot;\n\t\t\ttry {\n\t\t\t\tfinalOwner = readLockOwner(this.path);\n\t\t\t} catch (error) {\n\t\t\t\tif (errorCode(error) === \"ENOENT\") return;\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tif (!lockOwnersEqual(finalOwner, observed)) return;\n\t\t\tunlinkSync(this.path);\n\t\t\tfsyncDirectory(dirname(this.path));\n\t\t});\n\t}\n\n\tprivate acquire(owner: LockOwner): LockOwnerSnapshot {\n\t\tconst candidate = createLockCandidate(this.path, owner);\n\t\ttry {\n\t\t\tconst expected = readLockOwner(candidate);\n\t\t\tfor (let attempt = 0; attempt < LOCK_ATTEMPTS; attempt++) {\n\t\t\t\tlet current: LockOwnerSnapshot;\n\t\t\t\ttry {\n\t\t\t\t\tcurrent = readLockOwner(this.path);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (errorCode(error) !== \"ENOENT\") throw error;\n\t\t\t\t\tconst acquired = this.tryAcquire(candidate, expected);\n\t\t\t\t\tif (acquired !== undefined) return acquired;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (lockOwnerIsDead(current.owner, this.processIdentity)) {\n\t\t\t\t\tthis.reclaim(current);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tAtomics.wait(sleeper, 0, 0, LOCK_WAIT_MS);\n\t\t\t}\n\t\t\tthrow new ReplayLedgerLockError(\"replay ledger interprocess lock is unavailable\");\n\t\t} finally {\n\t\t\tremoveLockCandidate(candidate);\n\t\t}\n\t}\n\n\tprivate release(acquired: LockOwnerSnapshot): void {\n\t\tthis.withMutationGate(() => {\n\t\t\tconst current = readLockOwner(this.path);\n\t\t\tif (!lockOwnersEqual(current, acquired)) {\n\t\t\t\tthrow new ReplayLedgerLockError(\"replay lock ownership changed before release\");\n\t\t\t}\n\t\t\tunlinkSync(this.path);\n\t\t\tfsyncDirectory(dirname(this.path));\n\t\t});\n\t}\n\n\trun<T>(operation: () => T): T {\n\t\tconst acquired = this.acquire(createLockOwner(this.processIdentity));\n\t\tlet result: T;\n\t\ttry {\n\t\t\tresult = operation();\n\t\t} catch (error) {\n\t\t\tthis.release(acquired);\n\t\t\tthrow error;\n\t\t}\n\t\tif (\n\t\t\tresult !== null &&\n\t\t\t(typeof result === \"object\" || typeof result === \"function\") &&\n\t\t\t\"then\" in result &&\n\t\t\ttypeof result.then === \"function\"\n\t\t) {\n\t\t\treturn Promise.resolve(result).finally(() => this.release(acquired)) as T;\n\t\t}\n\t\tthis.release(acquired);\n\t\treturn result;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ReplayLedgerProcessIdentityReader } from "./replay-ledger-lock-owner.ts";
|
|
2
|
+
export interface ReplayLedgerMutationGateOptions {
|
|
3
|
+
readonly processIdentity?: ReplayLedgerProcessIdentityReader;
|
|
4
|
+
readonly beforeReclaimRemove?: () => void;
|
|
5
|
+
readonly beforeRelease?: () => void;
|
|
6
|
+
}
|
|
7
|
+
export declare class ReplayLedgerMutationGate {
|
|
8
|
+
private readonly path;
|
|
9
|
+
private readonly processIdentity;
|
|
10
|
+
private readonly beforeReclaimRemove;
|
|
11
|
+
private readonly beforeRelease;
|
|
12
|
+
constructor(path: string, options?: ReplayLedgerMutationGateOptions);
|
|
13
|
+
private intentPaths;
|
|
14
|
+
private hasLiveIntent;
|
|
15
|
+
private withIntent;
|
|
16
|
+
private abandon;
|
|
17
|
+
private tryAcquire;
|
|
18
|
+
private reclaim;
|
|
19
|
+
private acquire;
|
|
20
|
+
private releaseOwned;
|
|
21
|
+
private release;
|
|
22
|
+
run<T>(operation: () => T): T;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=replay-ledger-mutation-gate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay-ledger-mutation-gate.d.ts","sourceRoot":"","sources":["../../src/guardrails/replay-ledger-mutation-gate.ts"],"names":[],"mappings":"AAGA,OAAO,EASN,KAAK,iCAAiC,EAEtC,MAAM,+BAA+B,CAAC;AAMvC,MAAM,WAAW,+BAA+B;IAC/C,QAAQ,CAAC,eAAe,CAAC,EAAE,iCAAiC,CAAC;IAC7D,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1C,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;CACpC;AAED,qBAAa,wBAAwB;IACpC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAoC;IACpE,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAA2B;IAC/D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA2B;IAEzD,YAAY,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,+BAAoC,EAKtE;IAED,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,aAAa;IA4BrB,OAAO,CAAC,UAAU;IAYlB,OAAO,CAAC,OAAO;IASf,OAAO,CAAC,UAAU;IAqBlB,OAAO,CAAC,OAAO;IAwBf,OAAO,CAAC,OAAO;IA0Bf,OAAO,CAAC,YAAY;IAiBpB,OAAO,CAAC,OAAO;IAKf,GAAG,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,GAAG,CAAC,CAmB5B;CACD","sourcesContent":["import { linkSync, readdirSync, unlinkSync } from \"node:fs\";\nimport { basename, dirname, join } from \"node:path\";\nimport { createLockCandidate, fsyncDirectory, removeLockCandidate } from \"./replay-ledger-io.ts\";\nimport {\n\tcreateLockOwner,\n\terrorCode,\n\tinspectProcessIdentity,\n\ttype LockOwner,\n\ttype LockOwnerSnapshot,\n\tlockOwnerIsDead,\n\tlockOwnersEqual,\n\tReplayLedgerLockError,\n\ttype ReplayLedgerProcessIdentityReader,\n\treadLockOwner,\n} from \"./replay-ledger-lock-owner.ts\";\n\nconst GATE_ATTEMPTS = 200;\nconst GATE_WAIT_MS = 5;\nconst sleeper = new Int32Array(new SharedArrayBuffer(4));\n\nexport interface ReplayLedgerMutationGateOptions {\n\treadonly processIdentity?: ReplayLedgerProcessIdentityReader;\n\treadonly beforeReclaimRemove?: () => void;\n\treadonly beforeRelease?: () => void;\n}\n\nexport class ReplayLedgerMutationGate {\n\tprivate readonly path: string;\n\tprivate readonly processIdentity: ReplayLedgerProcessIdentityReader;\n\tprivate readonly beforeReclaimRemove: (() => void) | undefined;\n\tprivate readonly beforeRelease: (() => void) | undefined;\n\n\tconstructor(path: string, options: ReplayLedgerMutationGateOptions = {}) {\n\t\tthis.path = path;\n\t\tthis.processIdentity = options.processIdentity ?? inspectProcessIdentity;\n\t\tthis.beforeReclaimRemove = options.beforeReclaimRemove;\n\t\tthis.beforeRelease = options.beforeRelease;\n\t}\n\n\tprivate intentPaths(): string[] {\n\t\tconst directory = dirname(this.path);\n\t\tconst prefix = `${basename(this.path)}.intent.`;\n\t\treturn readdirSync(directory)\n\t\t\t.filter((name) => name.startsWith(prefix))\n\t\t\t.map((name) => join(directory, name));\n\t}\n\n\tprivate hasLiveIntent(): boolean {\n\t\tconst prefix = `${this.path}.intent.`;\n\t\tfor (const path of this.intentPaths()) {\n\t\t\tlet observed: LockOwnerSnapshot;\n\t\t\ttry {\n\t\t\t\tobserved = readLockOwner(path);\n\t\t\t} catch (error) {\n\t\t\t\tif (errorCode(error) === \"ENOENT\") continue;\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tif (path.slice(prefix.length) !== observed.owner.ownerId) {\n\t\t\t\tthrow new ReplayLedgerLockError(\"replay lock reclaim intent owner is invalid\");\n\t\t\t}\n\t\t\tif (!lockOwnerIsDead(observed.owner, this.processIdentity)) return true;\n\t\t\tlet current: LockOwnerSnapshot;\n\t\t\ttry {\n\t\t\t\tcurrent = readLockOwner(path);\n\t\t\t} catch (error) {\n\t\t\t\tif (errorCode(error) === \"ENOENT\") continue;\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tif (!lockOwnersEqual(current, observed) || !lockOwnerIsDead(current.owner, this.processIdentity)) return true;\n\t\t\tremoveLockCandidate(path);\n\t\t\tfsyncDirectory(dirname(this.path));\n\t\t}\n\t\treturn false;\n\t}\n\n\tprivate withIntent<T>(candidate: string, owner: LockOwner, operation: () => T): T {\n\t\tconst intent = `${this.path}.intent.${owner.ownerId}`;\n\t\tlinkSync(candidate, intent);\n\t\tfsyncDirectory(dirname(this.path));\n\t\ttry {\n\t\t\treturn operation();\n\t\t} finally {\n\t\t\tremoveLockCandidate(intent);\n\t\t\tfsyncDirectory(dirname(this.path));\n\t\t}\n\t}\n\n\tprivate abandon(acquired: LockOwnerSnapshot): void {\n\t\ttry {\n\t\t\tconst current = readLockOwner(this.path);\n\t\t\tif (lockOwnersEqual(current, acquired)) unlinkSync(this.path);\n\t\t} catch {\n\t\t\t// Preserve the original acquisition error; a mismatched owner must remain.\n\t\t}\n\t}\n\n\tprivate tryAcquire(candidate: string, expected: LockOwnerSnapshot): LockOwnerSnapshot | undefined {\n\t\tif (this.hasLiveIntent()) return undefined;\n\t\tlet linked = false;\n\t\ttry {\n\t\t\tlinkSync(candidate, this.path);\n\t\t\tlinked = true;\n\t\t\tfsyncDirectory(dirname(this.path));\n\t\t\tconst acquired = readLockOwner(this.path);\n\t\t\tif (!lockOwnersEqual(acquired, expected)) {\n\t\t\t\tthrow new ReplayLedgerLockError(\"replay lock reclaim ownership changed during acquisition\");\n\t\t\t}\n\t\t\tif (!this.hasLiveIntent()) return acquired;\n\t\t\tthis.releaseOwned(acquired);\n\t\t\treturn undefined;\n\t\t} catch (error) {\n\t\t\tif (errorCode(error) === \"EEXIST\") return undefined;\n\t\t\tif (linked) this.abandon(expected);\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tprivate reclaim(candidate: string, owner: LockOwner, observed: LockOwnerSnapshot): void {\n\t\tthis.withIntent(candidate, owner, () => {\n\t\t\tlet current: LockOwnerSnapshot;\n\t\t\ttry {\n\t\t\t\tcurrent = readLockOwner(this.path);\n\t\t\t} catch (error) {\n\t\t\t\tif (errorCode(error) === \"ENOENT\") return;\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tif (!lockOwnersEqual(current, observed) || !lockOwnerIsDead(current.owner, this.processIdentity)) return;\n\t\t\tthis.beforeReclaimRemove?.();\n\t\t\tlet finalOwner: LockOwnerSnapshot;\n\t\t\ttry {\n\t\t\t\tfinalOwner = readLockOwner(this.path);\n\t\t\t} catch (error) {\n\t\t\t\tif (errorCode(error) === \"ENOENT\") return;\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tif (!lockOwnersEqual(finalOwner, observed) || !lockOwnerIsDead(finalOwner.owner, this.processIdentity)) return;\n\t\t\tremoveLockCandidate(this.path);\n\t\t\tfsyncDirectory(dirname(this.path));\n\t\t});\n\t}\n\n\tprivate acquire(owner: LockOwner): LockOwnerSnapshot {\n\t\tconst candidate = createLockCandidate(this.path, owner);\n\t\ttry {\n\t\t\tconst expected = readLockOwner(candidate);\n\t\t\tfor (let attempt = 0; attempt < GATE_ATTEMPTS; attempt++) {\n\t\t\t\tconst acquired = this.tryAcquire(candidate, expected);\n\t\t\t\tif (acquired !== undefined) return acquired;\n\t\t\t\tlet current: LockOwnerSnapshot;\n\t\t\t\ttry {\n\t\t\t\t\tcurrent = readLockOwner(this.path);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (errorCode(error) === \"ENOENT\") continue;\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tif (lockOwnerIsDead(current.owner, this.processIdentity)) {\n\t\t\t\t\tthis.reclaim(candidate, owner, current);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tAtomics.wait(sleeper, 0, 0, GATE_WAIT_MS);\n\t\t\t}\n\t\t\tthrow new ReplayLedgerLockError(\"replay lock reclaim serialization is unavailable\");\n\t\t} finally {\n\t\t\tremoveLockCandidate(candidate);\n\t\t}\n\t}\n\n\tprivate releaseOwned(acquired: LockOwnerSnapshot): void {\n\t\tlet current: LockOwnerSnapshot;\n\t\ttry {\n\t\t\tcurrent = readLockOwner(this.path);\n\t\t} catch (error) {\n\t\t\tif (errorCode(error) === \"ENOENT\") {\n\t\t\t\tthrow new ReplayLedgerLockError(\"replay lock reclaim ownership changed before release\");\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t\tif (!lockOwnersEqual(current, acquired)) {\n\t\t\tthrow new ReplayLedgerLockError(\"replay lock reclaim ownership changed before release\");\n\t\t}\n\t\tunlinkSync(this.path);\n\t\tfsyncDirectory(dirname(this.path));\n\t}\n\n\tprivate release(acquired: LockOwnerSnapshot): void {\n\t\tthis.beforeRelease?.();\n\t\tthis.releaseOwned(acquired);\n\t}\n\n\trun<T>(operation: () => T): T {\n\t\tconst acquired = this.acquire(createLockOwner(this.processIdentity));\n\t\tlet result: T;\n\t\ttry {\n\t\t\tresult = operation();\n\t\t} catch (error) {\n\t\t\tthis.release(acquired);\n\t\t\tthrow error;\n\t\t}\n\t\tif (\n\t\t\tresult !== null &&\n\t\t\t(typeof result === \"object\" || typeof result === \"function\") &&\n\t\t\t\"then\" in result &&\n\t\t\ttypeof result.then === \"function\"\n\t\t) {\n\t\t\treturn Promise.resolve(result).finally(() => this.release(acquired)) as T;\n\t\t}\n\t\tthis.release(acquired);\n\t\treturn result;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { linkSync, readdirSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
|
+
import { createLockCandidate, fsyncDirectory, removeLockCandidate } from "./replay-ledger-io.js";
|
|
4
|
+
import { createLockOwner, errorCode, inspectProcessIdentity, lockOwnerIsDead, lockOwnersEqual, ReplayLedgerLockError, readLockOwner, } from "./replay-ledger-lock-owner.js";
|
|
5
|
+
const GATE_ATTEMPTS = 200;
|
|
6
|
+
const GATE_WAIT_MS = 5;
|
|
7
|
+
const sleeper = new Int32Array(new SharedArrayBuffer(4));
|
|
8
|
+
export class ReplayLedgerMutationGate {
|
|
9
|
+
path;
|
|
10
|
+
processIdentity;
|
|
11
|
+
beforeReclaimRemove;
|
|
12
|
+
beforeRelease;
|
|
13
|
+
constructor(path, options = {}) {
|
|
14
|
+
this.path = path;
|
|
15
|
+
this.processIdentity = options.processIdentity ?? inspectProcessIdentity;
|
|
16
|
+
this.beforeReclaimRemove = options.beforeReclaimRemove;
|
|
17
|
+
this.beforeRelease = options.beforeRelease;
|
|
18
|
+
}
|
|
19
|
+
intentPaths() {
|
|
20
|
+
const directory = dirname(this.path);
|
|
21
|
+
const prefix = `${basename(this.path)}.intent.`;
|
|
22
|
+
return readdirSync(directory)
|
|
23
|
+
.filter((name) => name.startsWith(prefix))
|
|
24
|
+
.map((name) => join(directory, name));
|
|
25
|
+
}
|
|
26
|
+
hasLiveIntent() {
|
|
27
|
+
const prefix = `${this.path}.intent.`;
|
|
28
|
+
for (const path of this.intentPaths()) {
|
|
29
|
+
let observed;
|
|
30
|
+
try {
|
|
31
|
+
observed = readLockOwner(path);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (errorCode(error) === "ENOENT")
|
|
35
|
+
continue;
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
if (path.slice(prefix.length) !== observed.owner.ownerId) {
|
|
39
|
+
throw new ReplayLedgerLockError("replay lock reclaim intent owner is invalid");
|
|
40
|
+
}
|
|
41
|
+
if (!lockOwnerIsDead(observed.owner, this.processIdentity))
|
|
42
|
+
return true;
|
|
43
|
+
let current;
|
|
44
|
+
try {
|
|
45
|
+
current = readLockOwner(path);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (errorCode(error) === "ENOENT")
|
|
49
|
+
continue;
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
if (!lockOwnersEqual(current, observed) || !lockOwnerIsDead(current.owner, this.processIdentity))
|
|
53
|
+
return true;
|
|
54
|
+
removeLockCandidate(path);
|
|
55
|
+
fsyncDirectory(dirname(this.path));
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
withIntent(candidate, owner, operation) {
|
|
60
|
+
const intent = `${this.path}.intent.${owner.ownerId}`;
|
|
61
|
+
linkSync(candidate, intent);
|
|
62
|
+
fsyncDirectory(dirname(this.path));
|
|
63
|
+
try {
|
|
64
|
+
return operation();
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
removeLockCandidate(intent);
|
|
68
|
+
fsyncDirectory(dirname(this.path));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
abandon(acquired) {
|
|
72
|
+
try {
|
|
73
|
+
const current = readLockOwner(this.path);
|
|
74
|
+
if (lockOwnersEqual(current, acquired))
|
|
75
|
+
unlinkSync(this.path);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Preserve the original acquisition error; a mismatched owner must remain.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
tryAcquire(candidate, expected) {
|
|
82
|
+
if (this.hasLiveIntent())
|
|
83
|
+
return undefined;
|
|
84
|
+
let linked = false;
|
|
85
|
+
try {
|
|
86
|
+
linkSync(candidate, this.path);
|
|
87
|
+
linked = true;
|
|
88
|
+
fsyncDirectory(dirname(this.path));
|
|
89
|
+
const acquired = readLockOwner(this.path);
|
|
90
|
+
if (!lockOwnersEqual(acquired, expected)) {
|
|
91
|
+
throw new ReplayLedgerLockError("replay lock reclaim ownership changed during acquisition");
|
|
92
|
+
}
|
|
93
|
+
if (!this.hasLiveIntent())
|
|
94
|
+
return acquired;
|
|
95
|
+
this.releaseOwned(acquired);
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (errorCode(error) === "EEXIST")
|
|
100
|
+
return undefined;
|
|
101
|
+
if (linked)
|
|
102
|
+
this.abandon(expected);
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
reclaim(candidate, owner, observed) {
|
|
107
|
+
this.withIntent(candidate, owner, () => {
|
|
108
|
+
let current;
|
|
109
|
+
try {
|
|
110
|
+
current = readLockOwner(this.path);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (errorCode(error) === "ENOENT")
|
|
114
|
+
return;
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
if (!lockOwnersEqual(current, observed) || !lockOwnerIsDead(current.owner, this.processIdentity))
|
|
118
|
+
return;
|
|
119
|
+
this.beforeReclaimRemove?.();
|
|
120
|
+
let finalOwner;
|
|
121
|
+
try {
|
|
122
|
+
finalOwner = readLockOwner(this.path);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (errorCode(error) === "ENOENT")
|
|
126
|
+
return;
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
if (!lockOwnersEqual(finalOwner, observed) || !lockOwnerIsDead(finalOwner.owner, this.processIdentity))
|
|
130
|
+
return;
|
|
131
|
+
removeLockCandidate(this.path);
|
|
132
|
+
fsyncDirectory(dirname(this.path));
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
acquire(owner) {
|
|
136
|
+
const candidate = createLockCandidate(this.path, owner);
|
|
137
|
+
try {
|
|
138
|
+
const expected = readLockOwner(candidate);
|
|
139
|
+
for (let attempt = 0; attempt < GATE_ATTEMPTS; attempt++) {
|
|
140
|
+
const acquired = this.tryAcquire(candidate, expected);
|
|
141
|
+
if (acquired !== undefined)
|
|
142
|
+
return acquired;
|
|
143
|
+
let current;
|
|
144
|
+
try {
|
|
145
|
+
current = readLockOwner(this.path);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
if (errorCode(error) === "ENOENT")
|
|
149
|
+
continue;
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
if (lockOwnerIsDead(current.owner, this.processIdentity)) {
|
|
153
|
+
this.reclaim(candidate, owner, current);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
Atomics.wait(sleeper, 0, 0, GATE_WAIT_MS);
|
|
157
|
+
}
|
|
158
|
+
throw new ReplayLedgerLockError("replay lock reclaim serialization is unavailable");
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
removeLockCandidate(candidate);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
releaseOwned(acquired) {
|
|
165
|
+
let current;
|
|
166
|
+
try {
|
|
167
|
+
current = readLockOwner(this.path);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (errorCode(error) === "ENOENT") {
|
|
171
|
+
throw new ReplayLedgerLockError("replay lock reclaim ownership changed before release");
|
|
172
|
+
}
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
if (!lockOwnersEqual(current, acquired)) {
|
|
176
|
+
throw new ReplayLedgerLockError("replay lock reclaim ownership changed before release");
|
|
177
|
+
}
|
|
178
|
+
unlinkSync(this.path);
|
|
179
|
+
fsyncDirectory(dirname(this.path));
|
|
180
|
+
}
|
|
181
|
+
release(acquired) {
|
|
182
|
+
this.beforeRelease?.();
|
|
183
|
+
this.releaseOwned(acquired);
|
|
184
|
+
}
|
|
185
|
+
run(operation) {
|
|
186
|
+
const acquired = this.acquire(createLockOwner(this.processIdentity));
|
|
187
|
+
let result;
|
|
188
|
+
try {
|
|
189
|
+
result = operation();
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
this.release(acquired);
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
if (result !== null &&
|
|
196
|
+
(typeof result === "object" || typeof result === "function") &&
|
|
197
|
+
"then" in result &&
|
|
198
|
+
typeof result.then === "function") {
|
|
199
|
+
return Promise.resolve(result).finally(() => this.release(acquired));
|
|
200
|
+
}
|
|
201
|
+
this.release(acquired);
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=replay-ledger-mutation-gate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay-ledger-mutation-gate.js","sourceRoot":"","sources":["../../src/guardrails/replay-ledger-mutation-gate.ts"],"names":[],"mappings":"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","sourcesContent":["import { linkSync, readdirSync, unlinkSync } from \"node:fs\";\nimport { basename, dirname, join } from \"node:path\";\nimport { createLockCandidate, fsyncDirectory, removeLockCandidate } from \"./replay-ledger-io.ts\";\nimport {\n\tcreateLockOwner,\n\terrorCode,\n\tinspectProcessIdentity,\n\ttype LockOwner,\n\ttype LockOwnerSnapshot,\n\tlockOwnerIsDead,\n\tlockOwnersEqual,\n\tReplayLedgerLockError,\n\ttype ReplayLedgerProcessIdentityReader,\n\treadLockOwner,\n} from \"./replay-ledger-lock-owner.ts\";\n\nconst GATE_ATTEMPTS = 200;\nconst GATE_WAIT_MS = 5;\nconst sleeper = new Int32Array(new SharedArrayBuffer(4));\n\nexport interface ReplayLedgerMutationGateOptions {\n\treadonly processIdentity?: ReplayLedgerProcessIdentityReader;\n\treadonly beforeReclaimRemove?: () => void;\n\treadonly beforeRelease?: () => void;\n}\n\nexport class ReplayLedgerMutationGate {\n\tprivate readonly path: string;\n\tprivate readonly processIdentity: ReplayLedgerProcessIdentityReader;\n\tprivate readonly beforeReclaimRemove: (() => void) | undefined;\n\tprivate readonly beforeRelease: (() => void) | undefined;\n\n\tconstructor(path: string, options: ReplayLedgerMutationGateOptions = {}) {\n\t\tthis.path = path;\n\t\tthis.processIdentity = options.processIdentity ?? inspectProcessIdentity;\n\t\tthis.beforeReclaimRemove = options.beforeReclaimRemove;\n\t\tthis.beforeRelease = options.beforeRelease;\n\t}\n\n\tprivate intentPaths(): string[] {\n\t\tconst directory = dirname(this.path);\n\t\tconst prefix = `${basename(this.path)}.intent.`;\n\t\treturn readdirSync(directory)\n\t\t\t.filter((name) => name.startsWith(prefix))\n\t\t\t.map((name) => join(directory, name));\n\t}\n\n\tprivate hasLiveIntent(): boolean {\n\t\tconst prefix = `${this.path}.intent.`;\n\t\tfor (const path of this.intentPaths()) {\n\t\t\tlet observed: LockOwnerSnapshot;\n\t\t\ttry {\n\t\t\t\tobserved = readLockOwner(path);\n\t\t\t} catch (error) {\n\t\t\t\tif (errorCode(error) === \"ENOENT\") continue;\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tif (path.slice(prefix.length) !== observed.owner.ownerId) {\n\t\t\t\tthrow new ReplayLedgerLockError(\"replay lock reclaim intent owner is invalid\");\n\t\t\t}\n\t\t\tif (!lockOwnerIsDead(observed.owner, this.processIdentity)) return true;\n\t\t\tlet current: LockOwnerSnapshot;\n\t\t\ttry {\n\t\t\t\tcurrent = readLockOwner(path);\n\t\t\t} catch (error) {\n\t\t\t\tif (errorCode(error) === \"ENOENT\") continue;\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tif (!lockOwnersEqual(current, observed) || !lockOwnerIsDead(current.owner, this.processIdentity)) return true;\n\t\t\tremoveLockCandidate(path);\n\t\t\tfsyncDirectory(dirname(this.path));\n\t\t}\n\t\treturn false;\n\t}\n\n\tprivate withIntent<T>(candidate: string, owner: LockOwner, operation: () => T): T {\n\t\tconst intent = `${this.path}.intent.${owner.ownerId}`;\n\t\tlinkSync(candidate, intent);\n\t\tfsyncDirectory(dirname(this.path));\n\t\ttry {\n\t\t\treturn operation();\n\t\t} finally {\n\t\t\tremoveLockCandidate(intent);\n\t\t\tfsyncDirectory(dirname(this.path));\n\t\t}\n\t}\n\n\tprivate abandon(acquired: LockOwnerSnapshot): void {\n\t\ttry {\n\t\t\tconst current = readLockOwner(this.path);\n\t\t\tif (lockOwnersEqual(current, acquired)) unlinkSync(this.path);\n\t\t} catch {\n\t\t\t// Preserve the original acquisition error; a mismatched owner must remain.\n\t\t}\n\t}\n\n\tprivate tryAcquire(candidate: string, expected: LockOwnerSnapshot): LockOwnerSnapshot | undefined {\n\t\tif (this.hasLiveIntent()) return undefined;\n\t\tlet linked = false;\n\t\ttry {\n\t\t\tlinkSync(candidate, this.path);\n\t\t\tlinked = true;\n\t\t\tfsyncDirectory(dirname(this.path));\n\t\t\tconst acquired = readLockOwner(this.path);\n\t\t\tif (!lockOwnersEqual(acquired, expected)) {\n\t\t\t\tthrow new ReplayLedgerLockError(\"replay lock reclaim ownership changed during acquisition\");\n\t\t\t}\n\t\t\tif (!this.hasLiveIntent()) return acquired;\n\t\t\tthis.releaseOwned(acquired);\n\t\t\treturn undefined;\n\t\t} catch (error) {\n\t\t\tif (errorCode(error) === \"EEXIST\") return undefined;\n\t\t\tif (linked) this.abandon(expected);\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tprivate reclaim(candidate: string, owner: LockOwner, observed: LockOwnerSnapshot): void {\n\t\tthis.withIntent(candidate, owner, () => {\n\t\t\tlet current: LockOwnerSnapshot;\n\t\t\ttry {\n\t\t\t\tcurrent = readLockOwner(this.path);\n\t\t\t} catch (error) {\n\t\t\t\tif (errorCode(error) === \"ENOENT\") return;\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tif (!lockOwnersEqual(current, observed) || !lockOwnerIsDead(current.owner, this.processIdentity)) return;\n\t\t\tthis.beforeReclaimRemove?.();\n\t\t\tlet finalOwner: LockOwnerSnapshot;\n\t\t\ttry {\n\t\t\t\tfinalOwner = readLockOwner(this.path);\n\t\t\t} catch (error) {\n\t\t\t\tif (errorCode(error) === \"ENOENT\") return;\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tif (!lockOwnersEqual(finalOwner, observed) || !lockOwnerIsDead(finalOwner.owner, this.processIdentity)) return;\n\t\t\tremoveLockCandidate(this.path);\n\t\t\tfsyncDirectory(dirname(this.path));\n\t\t});\n\t}\n\n\tprivate acquire(owner: LockOwner): LockOwnerSnapshot {\n\t\tconst candidate = createLockCandidate(this.path, owner);\n\t\ttry {\n\t\t\tconst expected = readLockOwner(candidate);\n\t\t\tfor (let attempt = 0; attempt < GATE_ATTEMPTS; attempt++) {\n\t\t\t\tconst acquired = this.tryAcquire(candidate, expected);\n\t\t\t\tif (acquired !== undefined) return acquired;\n\t\t\t\tlet current: LockOwnerSnapshot;\n\t\t\t\ttry {\n\t\t\t\t\tcurrent = readLockOwner(this.path);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (errorCode(error) === \"ENOENT\") continue;\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tif (lockOwnerIsDead(current.owner, this.processIdentity)) {\n\t\t\t\t\tthis.reclaim(candidate, owner, current);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tAtomics.wait(sleeper, 0, 0, GATE_WAIT_MS);\n\t\t\t}\n\t\t\tthrow new ReplayLedgerLockError(\"replay lock reclaim serialization is unavailable\");\n\t\t} finally {\n\t\t\tremoveLockCandidate(candidate);\n\t\t}\n\t}\n\n\tprivate releaseOwned(acquired: LockOwnerSnapshot): void {\n\t\tlet current: LockOwnerSnapshot;\n\t\ttry {\n\t\t\tcurrent = readLockOwner(this.path);\n\t\t} catch (error) {\n\t\t\tif (errorCode(error) === \"ENOENT\") {\n\t\t\t\tthrow new ReplayLedgerLockError(\"replay lock reclaim ownership changed before release\");\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t\tif (!lockOwnersEqual(current, acquired)) {\n\t\t\tthrow new ReplayLedgerLockError(\"replay lock reclaim ownership changed before release\");\n\t\t}\n\t\tunlinkSync(this.path);\n\t\tfsyncDirectory(dirname(this.path));\n\t}\n\n\tprivate release(acquired: LockOwnerSnapshot): void {\n\t\tthis.beforeRelease?.();\n\t\tthis.releaseOwned(acquired);\n\t}\n\n\trun<T>(operation: () => T): T {\n\t\tconst acquired = this.acquire(createLockOwner(this.processIdentity));\n\t\tlet result: T;\n\t\ttry {\n\t\t\tresult = operation();\n\t\t} catch (error) {\n\t\t\tthis.release(acquired);\n\t\t\tthrow error;\n\t\t}\n\t\tif (\n\t\t\tresult !== null &&\n\t\t\t(typeof result === \"object\" || typeof result === \"function\") &&\n\t\t\t\"then\" in result &&\n\t\t\ttypeof result.then === \"function\"\n\t\t) {\n\t\t\treturn Promise.resolve(result).finally(() => this.release(acquired)) as T;\n\t\t}\n\t\tthis.release(acquired);\n\t\treturn result;\n\t}\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay-ledger-quarantine.d.ts","sourceRoot":"","sources":["../../src/guardrails/replay-ledger-quarantine.ts"],"names":[],"mappings":"AAKA,wBAAgB,4BAA4B,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,IAAI,GAAG,MAAM,CAYjH","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { closeSync, constants as fsConstants, fsyncSync, openSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { fsyncDirectory, writeAll } from \"./replay-ledger-lock.ts\";\n\nexport function quarantineReplayLedgerSuffix(ledgerPath: string, suffix: Buffer, beforeFsync?: () => void): string {\n\tconst artifactPath = `${ledgerPath}.quarantine.${randomUUID()}`;\n\tconst fd = openSync(artifactPath, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, 0o600);\n\ttry {\n\t\twriteAll(fd, suffix);\n\t\tbeforeFsync?.();\n\t\tfsyncSync(fd);\n\t} finally {\n\t\tcloseSync(fd);\n\t}\n\tfsyncDirectory(dirname(ledgerPath));\n\treturn artifactPath;\n}\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { closeSync, constants as fsConstants, fsyncSync, openSync } from "node:fs";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { fsyncDirectory, writeAll } from "./replay-ledger-lock.js";
|
|
5
|
+
export function quarantineReplayLedgerSuffix(ledgerPath, suffix, beforeFsync) {
|
|
6
|
+
const artifactPath = `${ledgerPath}.quarantine.${randomUUID()}`;
|
|
7
|
+
const fd = openSync(artifactPath, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, 0o600);
|
|
8
|
+
try {
|
|
9
|
+
writeAll(fd, suffix);
|
|
10
|
+
beforeFsync?.();
|
|
11
|
+
fsyncSync(fd);
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
closeSync(fd);
|
|
15
|
+
}
|
|
16
|
+
fsyncDirectory(dirname(ledgerPath));
|
|
17
|
+
return artifactPath;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=replay-ledger-quarantine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay-ledger-quarantine.js","sourceRoot":"","sources":["../../src/guardrails/replay-ledger-quarantine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,SAAS,IAAI,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnF,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEnE,MAAM,UAAU,4BAA4B,CAAC,UAAkB,EAAE,MAAc,EAAE,WAAwB,EAAU;IAClH,MAAM,YAAY,GAAG,GAAG,UAAU,eAAe,UAAU,EAAE,EAAE,CAAC;IAChE,MAAM,EAAE,GAAG,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC,OAAO,GAAG,WAAW,CAAC,MAAM,GAAG,WAAW,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IAC1G,IAAI,CAAC;QACJ,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;QACrB,WAAW,EAAE,EAAE,CAAC;QAChB,SAAS,CAAC,EAAE,CAAC,CAAC;IACf,CAAC;YAAS,CAAC;QACV,SAAS,CAAC,EAAE,CAAC,CAAC;IACf,CAAC;IACD,cAAc,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC;IACpC,OAAO,YAAY,CAAC;AAAA,CACpB","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { closeSync, constants as fsConstants, fsyncSync, openSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { fsyncDirectory, writeAll } from \"./replay-ledger-lock.ts\";\n\nexport function quarantineReplayLedgerSuffix(ledgerPath: string, suffix: Buffer, beforeFsync?: () => void): string {\n\tconst artifactPath = `${ledgerPath}.quarantine.${randomUUID()}`;\n\tconst fd = openSync(artifactPath, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, 0o600);\n\ttry {\n\t\twriteAll(fd, suffix);\n\t\tbeforeFsync?.();\n\t\tfsyncSync(fd);\n\t} finally {\n\t\tcloseSync(fd);\n\t}\n\tfsyncDirectory(dirname(ledgerPath));\n\treturn artifactPath;\n}\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ReplayLedgerHead } from "../types/evidence.ts";
|
|
2
|
+
export declare const EMPTY_REPLAY_LEDGER_HEAD: ReplayLedgerHead;
|
|
3
|
+
interface InspectedLedger {
|
|
4
|
+
readonly lastSeq: number;
|
|
5
|
+
readonly lastHash: string;
|
|
6
|
+
}
|
|
7
|
+
interface StoreSnapshot {
|
|
8
|
+
readonly bytes: Buffer;
|
|
9
|
+
readonly head: ReplayLedgerHead;
|
|
10
|
+
}
|
|
11
|
+
export declare function replayLedgerHeadsEqual(left: ReplayLedgerHead, right: ReplayLedgerHead): boolean;
|
|
12
|
+
export interface ReplayLedgerStoreOptions {
|
|
13
|
+
readonly afterLedgerFsync?: () => void;
|
|
14
|
+
readonly beforeQuarantineFsync?: () => void;
|
|
15
|
+
}
|
|
16
|
+
export declare class ReplayLedgerStore {
|
|
17
|
+
private readonly path;
|
|
18
|
+
private readonly inspect;
|
|
19
|
+
private readonly headPath;
|
|
20
|
+
private readonly lock;
|
|
21
|
+
private readonly afterLedgerFsync;
|
|
22
|
+
private readonly beforeQuarantineFsync;
|
|
23
|
+
constructor(path: string, inspect: (bytes: Buffer) => InspectedLedger, options?: ReplayLedgerStoreOptions);
|
|
24
|
+
private withLock;
|
|
25
|
+
private readVerified;
|
|
26
|
+
private initializeGenesis;
|
|
27
|
+
private publishHead;
|
|
28
|
+
load(expectedHead?: ReplayLedgerHead): StoreSnapshot;
|
|
29
|
+
append(line: Buffer, nextSeq: number, nextHash: string, expectedHead: ReplayLedgerHead): ReplayLedgerHead;
|
|
30
|
+
}
|
|
31
|
+
export {};
|
|
32
|
+
//# sourceMappingURL=replay-ledger-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay-ledger-store.d.ts","sourceRoot":"","sources":["../../src/guardrails/replay-ledger-store.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAK7D,eAAO,MAAM,wBAAwB,EAAE,gBAKrC,CAAC;AACH,UAAU,eAAe;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC1B;AACD,UAAU,aAAa;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;CAChC;AASD,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,gBAAgB,EAAE,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAO/F;AA4CD,MAAM,WAAW,wBAAwB;IACxC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IACvC,QAAQ,CAAC,qBAAqB,CAAC,EAAE,MAAM,IAAI,CAAC;CAC5C;AAED,qBAAa,iBAAiB;IAC7B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqC;IAC7D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAmB;IACxC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA2B;IAC5D,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAA2B;IAEjE,YAAY,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,eAAe,EAAE,OAAO,GAAE,wBAA6B,EAQ5G;IAED,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,YAAY;IAmDpB,OAAO,CAAC,iBAAiB;IAazB,OAAO,CAAC,WAAW;IAkBnB,IAAI,CAAC,YAAY,CAAC,EAAE,gBAAgB,GAAG,aAAa,CAQnD;IAED,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,gBAAgB,GAAG,gBAAgB,CAoCxG;CACD","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport {\n\tcloseSync,\n\texistsSync,\n\tconstants as fsConstants,\n\tfstatSync,\n\tfsyncSync,\n\tftruncateSync,\n\tmkdirSync,\n\topenSync,\n\treadFileSync,\n\trenameSync,\n\tunlinkSync,\n} from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport type { ReplayLedgerHead } from \"../types/evidence.ts\";\nimport { fsyncDirectory, ReplayLedgerLock, writeAll } from \"./replay-ledger-lock.ts\";\nimport { quarantineReplayLedgerSuffix } from \"./replay-ledger-quarantine.ts\";\n\nconst GENESIS_HASH = \"genesis\";\nexport const EMPTY_REPLAY_LEDGER_HEAD: ReplayLedgerHead = Object.freeze({\n\tfileIdentity: null,\n\tsize: 0,\n\tlastSeq: 0,\n\tlastHash: GENESIS_HASH,\n});\ninterface InspectedLedger {\n\treadonly lastSeq: number;\n\treadonly lastHash: string;\n}\ninterface StoreSnapshot {\n\treadonly bytes: Buffer;\n\treadonly head: ReplayLedgerHead;\n}\nfunction identity(stat: ReturnType<typeof fstatSync>): ReplayLedgerHead[\"fileIdentity\"] {\n\treturn Object.freeze({ dev: String(stat.dev), ino: String(stat.ino) });\n}\n\nfunction sameIdentity(left: ReplayLedgerHead[\"fileIdentity\"], right: ReplayLedgerHead[\"fileIdentity\"]): boolean {\n\treturn left === null ? right === null : right !== null && left.dev === right.dev && left.ino === right.ino;\n}\n\nexport function replayLedgerHeadsEqual(left: ReplayLedgerHead, right: ReplayLedgerHead): boolean {\n\treturn (\n\t\tsameIdentity(left.fileIdentity, right.fileIdentity) &&\n\t\tleft.size === right.size &&\n\t\tleft.lastSeq === right.lastSeq &&\n\t\tleft.lastHash === right.lastHash\n\t);\n}\n\nfunction parseHead(value: unknown): ReplayLedgerHead {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value))\n\t\tthrow new Error(\"replay committed head is invalid\");\n\tconst record = value as Record<string, unknown>;\n\tif (Object.keys(record).sort().join(\",\") !== \"fileIdentity,lastHash,lastSeq,size\") {\n\t\tthrow new Error(\"replay committed head has an invalid key set\");\n\t}\n\tconst rawIdentity = record.fileIdentity;\n\tlet fileIdentity: ReplayLedgerHead[\"fileIdentity\"] = null;\n\tif (rawIdentity !== null) {\n\t\tif (typeof rawIdentity !== \"object\" || Array.isArray(rawIdentity))\n\t\t\tthrow new Error(\"replay file identity is invalid\");\n\t\tconst fields = rawIdentity as Record<string, unknown>;\n\t\tif (\n\t\t\tObject.keys(fields).sort().join(\",\") !== \"dev,ino\" ||\n\t\t\ttypeof fields.dev !== \"string\" ||\n\t\t\ttypeof fields.ino !== \"string\" ||\n\t\t\t!/^\\d+$/.test(fields.dev) ||\n\t\t\t!/^\\d+$/.test(fields.ino)\n\t\t) {\n\t\t\tthrow new Error(\"replay file identity is invalid\");\n\t\t}\n\t\tfileIdentity = Object.freeze({ dev: fields.dev, ino: fields.ino });\n\t}\n\tif (\n\t\t!Number.isSafeInteger(record.size) ||\n\t\t(record.size as number) < 0 ||\n\t\t!Number.isSafeInteger(record.lastSeq) ||\n\t\t(record.lastSeq as number) < 0 ||\n\t\ttypeof record.lastHash !== \"string\" ||\n\t\t(record.lastSeq === 0 ? record.lastHash !== GENESIS_HASH : !/^[0-9a-f]{64}$/.test(record.lastHash))\n\t) {\n\t\tthrow new Error(\"replay committed head fields are invalid\");\n\t}\n\treturn Object.freeze({\n\t\tfileIdentity,\n\t\tsize: record.size as number,\n\t\tlastSeq: record.lastSeq as number,\n\t\tlastHash: record.lastHash,\n\t});\n}\n\nexport interface ReplayLedgerStoreOptions {\n\treadonly afterLedgerFsync?: () => void;\n\treadonly beforeQuarantineFsync?: () => void;\n}\n\nexport class ReplayLedgerStore {\n\tprivate readonly path: string;\n\tprivate readonly inspect: (bytes: Buffer) => InspectedLedger;\n\tprivate readonly headPath: string;\n\tprivate readonly lock: ReplayLedgerLock;\n\tprivate readonly afterLedgerFsync: (() => void) | undefined;\n\tprivate readonly beforeQuarantineFsync: (() => void) | undefined;\n\n\tconstructor(path: string, inspect: (bytes: Buffer) => InspectedLedger, options: ReplayLedgerStoreOptions = {}) {\n\t\tthis.path = path;\n\t\tthis.inspect = inspect;\n\t\tmkdirSync(dirname(path), { recursive: true });\n\t\tthis.headPath = `${path}.head`;\n\t\tthis.lock = new ReplayLedgerLock(`${path}.lock`);\n\t\tthis.afterLedgerFsync = options.afterLedgerFsync;\n\t\tthis.beforeQuarantineFsync = options.beforeQuarantineFsync;\n\t}\n\n\tprivate withLock<T>(operation: () => T): T {\n\t\treturn this.lock.run(operation);\n\t}\n\n\tprivate readVerified(): StoreSnapshot {\n\t\tconst ledgerExists = existsSync(this.path);\n\t\tconst headExists = existsSync(this.headPath);\n\t\tif (!ledgerExists && !headExists) return { bytes: Buffer.alloc(0), head: EMPTY_REPLAY_LEDGER_HEAD };\n\t\tif (ledgerExists && !headExists) {\n\t\t\tconst fd = openSync(this.path, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));\n\t\t\ttry {\n\t\t\t\tconst stat = fstatSync(fd);\n\t\t\t\tif (!stat.isFile() || stat.size !== 0) throw new Error(\"replay ledger or committed head suffix is missing\");\n\t\t\t} finally {\n\t\t\t\tcloseSync(fd);\n\t\t\t}\n\t\t\tunlinkSync(this.path);\n\t\t\tfsyncDirectory(dirname(this.path));\n\t\t\treturn { bytes: Buffer.alloc(0), head: EMPTY_REPLAY_LEDGER_HEAD };\n\t\t}\n\t\tif (!ledgerExists) throw new Error(\"replay ledger or committed head suffix is missing\");\n\t\tconst head = parseHead(JSON.parse(readFileSync(this.headPath, \"utf8\")));\n\t\tconst fd = openSync(this.path, fsConstants.O_RDWR | (fsConstants.O_NOFOLLOW ?? 0));\n\t\ttry {\n\t\t\tconst before = fstatSync(fd);\n\t\t\tconst bytes = readFileSync(fd);\n\t\t\tconst after = fstatSync(fd);\n\t\t\tif (\n\t\t\t\t!before.isFile() ||\n\t\t\t\t!after.isFile() ||\n\t\t\t\t!sameIdentity(identity(before), identity(after)) ||\n\t\t\t\tbefore.size !== after.size ||\n\t\t\t\tafter.size !== bytes.byteLength ||\n\t\t\t\t!sameIdentity(head.fileIdentity, identity(after)) ||\n\t\t\t\thead.size > after.size\n\t\t\t) {\n\t\t\t\tthrow new Error(\"replay ledger tampered: file identity or size does not match its committed head\");\n\t\t\t}\n\t\t\tconst committedBytes = head.size === after.size ? bytes : bytes.subarray(0, head.size);\n\t\t\tconst inspected = this.inspect(committedBytes);\n\t\t\tif (inspected.lastSeq !== head.lastSeq || inspected.lastHash !== head.lastHash) {\n\t\t\t\tthrow new Error(\"replay ledger suffix does not match its committed head\");\n\t\t\t}\n\t\t\tif (head.size < after.size) {\n\t\t\t\tquarantineReplayLedgerSuffix(this.path, bytes.subarray(head.size), this.beforeQuarantineFsync);\n\t\t\t\tftruncateSync(fd, head.size);\n\t\t\t\tfsyncSync(fd);\n\t\t\t\tfsyncDirectory(dirname(this.path));\n\t\t\t}\n\t\t\treturn { bytes: committedBytes, head };\n\t\t} finally {\n\t\t\tcloseSync(fd);\n\t\t}\n\t}\n\n\tprivate initializeGenesis(): ReplayLedgerHead {\n\t\tconst fd = openSync(this.path, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, 0o600);\n\t\ttry {\n\t\t\tfsyncSync(fd);\n\t\t\tconst stat = fstatSync(fd);\n\t\t\tconst head = Object.freeze({ ...EMPTY_REPLAY_LEDGER_HEAD, fileIdentity: identity(stat) });\n\t\t\tthis.publishHead(head);\n\t\t\treturn head;\n\t\t} finally {\n\t\t\tcloseSync(fd);\n\t\t}\n\t}\n\n\tprivate publishHead(head: ReplayLedgerHead): void {\n\t\tconst temp = `${this.headPath}.${randomUUID()}.tmp`;\n\t\tconst fd = openSync(temp, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, 0o600);\n\t\ttry {\n\t\t\twriteAll(fd, Buffer.from(`${JSON.stringify(head)}\\n`, \"utf8\"));\n\t\t\tfsyncSync(fd);\n\t\t} finally {\n\t\t\tcloseSync(fd);\n\t\t}\n\t\ttry {\n\t\t\trenameSync(temp, this.headPath);\n\t\t\tfsyncDirectory(dirname(this.path));\n\t\t} catch (error) {\n\t\t\tif (existsSync(temp)) unlinkSync(temp);\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tload(expectedHead?: ReplayLedgerHead): StoreSnapshot {\n\t\treturn this.withLock(() => {\n\t\t\tconst snapshot = this.readVerified();\n\t\t\tif (expectedHead !== undefined && !replayLedgerHeadsEqual(snapshot.head, expectedHead)) {\n\t\t\t\tthrow new Error(\"replay ledger expected-head CAS verification failed\");\n\t\t\t}\n\t\t\treturn snapshot;\n\t\t});\n\t}\n\n\tappend(line: Buffer, nextSeq: number, nextHash: string, expectedHead: ReplayLedgerHead): ReplayLedgerHead {\n\t\treturn this.withLock(() => {\n\t\t\tconst snapshot = this.readVerified();\n\t\t\tif (!replayLedgerHeadsEqual(snapshot.head, expectedHead)) {\n\t\t\t\tthrow new Error(\"replay ledger concurrent append failed expected-head CAS\");\n\t\t\t}\n\t\t\tconst appendHead = snapshot.head.fileIdentity === null ? this.initializeGenesis() : snapshot.head;\n\t\t\tconst fd = openSync(\n\t\t\t\tthis.path,\n\t\t\t\tfsConstants.O_CREAT | fsConstants.O_APPEND | fsConstants.O_WRONLY | (fsConstants.O_NOFOLLOW ?? 0),\n\t\t\t\t0o600,\n\t\t\t);\n\t\t\ttry {\n\t\t\t\tconst before = fstatSync(fd);\n\t\t\t\tif (before.size !== appendHead.size || !sameIdentity(appendHead.fileIdentity, identity(before))) {\n\t\t\t\t\tthrow new Error(\"replay ledger file identity or size changed before CAS append\");\n\t\t\t\t}\n\t\t\t\twriteAll(fd, line);\n\t\t\t\tfsyncSync(fd);\n\t\t\t\tconst after = fstatSync(fd);\n\t\t\t\tif (!sameIdentity(identity(before), identity(after)) || after.size !== appendHead.size + line.byteLength) {\n\t\t\t\t\tthrow new Error(\"replay ledger file changed during CAS append\");\n\t\t\t\t}\n\t\t\t\tconst head = Object.freeze({\n\t\t\t\t\tfileIdentity: identity(after),\n\t\t\t\t\tsize: after.size,\n\t\t\t\t\tlastSeq: nextSeq,\n\t\t\t\t\tlastHash: nextHash,\n\t\t\t\t});\n\t\t\t\tthis.afterLedgerFsync?.();\n\t\t\t\tthis.publishHead(head);\n\t\t\t\treturn head;\n\t\t\t} finally {\n\t\t\t\tcloseSync(fd);\n\t\t\t}\n\t\t});\n\t}\n}\n"]}
|