Skip to main content

Retries

Implement retries by looping within the same neuron, storing attempt count in context, and backing off between tries. This preserves signal ownership and keeps orchestration local.

import { withCtx, collateral } from '@cnstra/core';

// Collaterals owned by the retry neuron
const tryFetch = collateral<{ url: string }>('retry:tryFetch');
const completed = collateral<{ ok: true; data: unknown }>('retry:completed');
const failed = collateral<{ ok: false; error: unknown }>('retry:failed');

const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));

export const fetchWithRetry = withCtx<{ attempt?: number }>()
.neuron('fetch-with-retry', { tryFetch, completed, failed })
.dendrite({
collateral: tryFetch,
response: async (payload, axon, ctx) => {
const attempt = (ctx.get()?.attempt ?? 0) + 1;
ctx.set({ attempt });

try {
const res = await fetch(payload.url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
return axon.completed.createSignal({ ok: true, data });
} catch (error) {
if (attempt < 3) {
await sleep(2 ** (attempt - 1) * 250); // backoff: 250ms, 500ms, ...
// self-loop: re-emit our own input collateral
return axon.tryFetch.createSignal({ url: payload.url });
}
return axon.failed.createSignal({ ok: false, error });
}
},
});

Notes

  • Self-loop uses the neuron's own input collateral (retry:tryFetch), complying with ownership.
  • Attempt count is stored in ctx; backoff grows per attempt.
  • Prefer queue-native retries (e.g., BullMQ, SQS) in production for visibility; use local retries for transient client/network work.