Run workers and recover
Nobody needs to wait on an Operation: a process can call transfer, return, and even crash. Background workers finish the job. They claim due Operations from the store, observe them, rebroadcast dropped ones, report nonce gaps (nonce.gap), and apply proven verdicts. Any number of processes can run workers on shared stores. Each claim carries a token, and a worker whose claim expired and was taken over has its writes refused (FENCING).
ts
const aio = new CryptoAio({ namespace: 'payments', stores, signers, wallets, chains, providers });
const report = await aio.operations.recover(); // at startup, before serving
// report: { rebroadcast, checked, skipped, unresent, failed, reconciled }
const stop = new AbortController();
const workers = aio.monitor.start({ workerId: `api-${process.pid}`, signal: stop.signal });
process.once('SIGTERM', () => stop.abort());
await workers;
await aio.close(); // closes native clients and pooled driversrecover()resendssignedand ambiguouslysubmittedOperations, checks the other signed ones on chain, and returns leaked nonces for reuse.created,prepared,awaiting-signatureandstalledOperations need you, so it skips them with arecovery.skippedevent.- Workers and
recover()never sign and need no signer. They read through the Operation's chain, network and library with the current configuration. A resend or a nonce reconciliation needs only the Operation's wallet, and a watch-only wallet is enough. You can rotate or remove a signer while Operations are in flight. If the wallet itself is gone from the configuration,recover()still checks the Operation and counts itunresent: restore the wallet and recover again to resend it. - A nonce that leaked in a crash (allocated, never stored) is also reclaimed by the next
transferorprepareTransferfrom that wallet, so a quiet wallet never stays blocked. aio.monitor.runOnce({ workerId, batch })runs one pass and returns how many Operations it claimed. Use it from a scheduler.- Call
close()on the root container; a scope'sclose()does nothing. After it, handle methods andnative()throwStateError(INVALID_TRANSITION). It also stops everymonitor.start()loop, and a runningrunOnce()orrecover()at its next check, so the closed container claims no more Operations; starting one afterwards throwsINVALID_TRANSITIONtoo. - Tune timing with
lifecycle:pollIntervalMs,droppedGracePeriodMs,rebroadcastIntervalMs,leaseMs,claimLeaseMs,waitTimeoutMsandsignTimeoutMs.
Next steps
- Retries, ambiguity and recovery: what recovery does, and why it never signs.
- Nonces, leases and many processes: how many workers share one store.
- Go to production: the checklist.