mirror of
https://github.com/Routstr/routstrd.git
synced 2026-10-05 20:38:22 +00:00
- Share one wallet-lifetime tracker (family:id keys) across startup settlement, targeted recovery, explicit mint-quote recovery, and the pending-mint sweep, so a timed-out observation is never retried underneath. - Bound targeted recovery per operation (15s) and per pass (60s); a timeout reports timedOut and keeps the underlying work tracked. - Executing-send recovery holds coco's per-operation lock until the underlying drive actually settles, not until the caller times out. - Dispose now rejects new/queued recovery, drains queued and in-flight work, and only then closes coco/database/ownership; a bounded caller wait reports incomplete shutdown instead of closing early. - Fail-closed checks for coco private recovery internals before any local cleanup writes. - Remove the periodic recheck; recovery stays startup- or operator-triggered.
46 lines
1.7 KiB
TypeScript
46 lines
1.7 KiB
TypeScript
/** A timed-out wait is not cancellation: retain work until it actually settles. */
|
|
export type RecoveryWork = Map<string, Promise<unknown>>;
|
|
export const recoveryKey = (kind: string, id: string): string => `${kind}:${id}`;
|
|
|
|
export function trackRecovery<T>(work: RecoveryWork, key: string, promise: Promise<T>): Promise<T> {
|
|
work.set(key, promise);
|
|
const clear = () => { if (work.get(key) === promise) work.delete(key); };
|
|
void promise.then(clear, clear);
|
|
return promise;
|
|
}
|
|
|
|
export class RecoveryWaitTimeout extends Error {
|
|
constructor() { super("Timed out waiting for recovery; underlying work is still tracked"); }
|
|
}
|
|
|
|
export async function waitForRecoveryWork<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
|
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
try {
|
|
return await Promise.race([promise, new Promise<never>((_, reject) => {
|
|
timer = setTimeout(() => reject(new RecoveryWaitTimeout()), timeoutMs);
|
|
})]);
|
|
} finally {
|
|
if (timer !== undefined) clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
/** Drain actual work, including work registered while an earlier task settles. */
|
|
export async function drainRecoveryWork(work: RecoveryWork): Promise<void> {
|
|
while (work.size) await Promise.allSettled([...work.values()]);
|
|
}
|
|
|
|
/** Timeout reports incomplete disposal; actual cleanup continues safely. */
|
|
export function createRecoveryDisposer(
|
|
quiesce: () => void,
|
|
settle: () => Promise<void>,
|
|
close: () => Promise<void>,
|
|
timeoutMs = 30_000,
|
|
): () => Promise<void> {
|
|
let disposal: Promise<void> | undefined;
|
|
return async () => {
|
|
quiesce();
|
|
disposal ??= (async () => { await settle(); await close(); })();
|
|
await waitForRecoveryWork(disposal, timeoutMs);
|
|
};
|
|
}
|