//#region src/idempotency.ts
var IdempotencyValidationError = class extends Error {
	code;
	field;
	constructor(code, field, message) {
		super(message);
		this.name = "IdempotencyValidationError";
		this.code = code;
		this.field = field;
	}
};
var IdempotencyContentionError = class extends Error {
	key;
	attempts;
	constructor(key, attempts) {
		super(`Idempotency claim contention exceeded ${attempts} attempts for key ${key}`);
		this.name = "IdempotencyContentionError";
		this.key = key;
		this.attempts = attempts;
	}
};
const DEFAULT_MAX_ATTEMPTS = 8;
function failValidation(code, field, message) {
	throw new IdempotencyValidationError(code, field, message);
}
function assertPositiveFinite(name, value) {
	if (!Number.isFinite(value) || value <= 0) failValidation("invalid-duration", name, `${name} must be a finite positive number`);
}
function resolveMaxAttempts(value) {
	const attempts = value ?? DEFAULT_MAX_ATTEMPTS;
	if (!Number.isInteger(attempts) || attempts <= 0) failValidation("invalid-attempts", "maxAttempts", "maxAttempts must be a positive integer");
	return attempts;
}
function assertKey(key) {
	if (typeof key !== "string" || key.trim().length === 0) failValidation("invalid-key", "key", "key must be a non-empty string");
}
function assertToken(token, field = "token") {
	if (typeof token !== "string" || token.trim().length === 0) failValidation("invalid-token", field, `${field} must be a non-empty string`);
}
function createClaimToken(generateToken, currentToken) {
	let token;
	try {
		token = generateToken();
	} catch {
		failValidation("token-generation-failed", "generateToken", "generateToken failed to produce a claim token");
	}
	assertToken(token, "generateToken");
	if (currentToken !== void 0 && token === currentToken) failValidation("reused-token", "generateToken", "generateToken must return a token distinct from the current claim token");
	return token;
}
function duplicateResult(record) {
	return {
		status: "duplicate",
		existing: {
			claimedAtMs: record.claimedAtMs,
			staleAtMs: record.staleAtMs,
			expiresAtMs: record.expiresAtMs
		}
	};
}
function readNow(now) {
	let nowMs;
	try {
		nowMs = now();
	} catch {
		failValidation("invalid-clock", "now", "now failed to produce a timestamp");
	}
	if (typeof nowMs !== "number" || !Number.isFinite(nowMs)) failValidation("invalid-clock", "now", "now must return a finite timestamp");
	return nowMs;
}
async function claimIdempotency(store, key, options) {
	assertKey(key);
	assertPositiveFinite("ttlMs", options.ttlMs);
	assertPositiveFinite("staleAfterMs", options.staleAfterMs);
	if (options.staleAfterMs > options.ttlMs) failValidation("invalid-duration", "staleAfterMs", "staleAfterMs must not exceed ttlMs");
	const maxAttempts = resolveMaxAttempts(options.maxAttempts);
	const now = options.now ?? Date.now;
	const generateToken = options.generateToken ?? (() => crypto.randomUUID());
	for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
		const current = await store.read(key);
		const nowMs = readNow(now);
		if (current && nowMs < current.record.staleAtMs && nowMs < current.record.expiresAtMs) return duplicateResult(current.record);
		const staleAtMs = nowMs + options.staleAfterMs;
		const expiresAtMs = nowMs + options.ttlMs;
		if (!Number.isFinite(staleAtMs) || !Number.isFinite(expiresAtMs)) failValidation("invalid-clock", "now", "now plus configured durations must produce finite timestamps");
		const record = {
			token: createClaimToken(generateToken, current?.record.token),
			claimedAtMs: nowMs,
			staleAtMs,
			expiresAtMs
		};
		if (await store.compareAndSwap({
			key,
			expectedVersion: current?.version ?? null,
			next: record,
			ttlMs: options.ttlMs
		})) return {
			status: "claimed",
			claim: {
				key,
				...record
			},
			reclaimed: current !== null
		};
	}
	throw new IdempotencyContentionError(key, maxAttempts);
}
async function releaseIdempotencyClaim(store, claim, options = {}) {
	assertKey(claim.key);
	assertToken(claim.token, "claim.token");
	const maxAttempts = resolveMaxAttempts(options.maxAttempts);
	for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
		const current = await store.read(claim.key);
		if (!current || current.record.token !== claim.token) return false;
		if (await store.compareAndSwap({
			key: claim.key,
			expectedVersion: current.version,
			next: null
		})) return true;
	}
	throw new IdempotencyContentionError(claim.key, maxAttempts);
}
//#endregion
export { IdempotencyContentionError, IdempotencyValidationError, claimIdempotency, releaseIdempotencyClaim };
