Skip to content

refactor(core,sdks): unify Retry into a single shared Context.Service#225

Open
sam-goodwin wants to merge 4 commits into
mainfrom
feat/cloudflare-blanket-retry
Open

refactor(core,sdks): unify Retry into a single shared Context.Service#225
sam-goodwin wants to merge 4 commits into
mainfrom
feat/cloudflare-blanket-retry

Conversation

@sam-goodwin

@sam-goodwin sam-goodwin commented May 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

Per-SDK Retry Context.Service tags were never actually consumed: each packages/<sdk>/src/retry.ts declared its own Retry class but the shared makeAPI in @distilled.cloud/core/client had no way to look up N different tags and just ran a hard-coded throttling-only schedule. So Cloudflare.Retry.transient(eff), Turso.Retry.policy(...), etc. all compiled and provided a layer — but nothing read it.

This PR keeps the per-SDK tag (so policies don't bleed across SDKs — Cloudflare.Retry.transient only retries Cloudflare calls, not turso underneath) and just has each SDK's client wire its own tag into makeAPI automatically. End users just write myEffect.pipe(Cloudflare.Retry.transient) — no per-SDK config to remember.

This mirrors the AWS pattern in packages/aws/src/client/api.ts (outerFn reads Retry with Retry.makeDefault as fallback).

Changes

  • packages/core/src/client.tsClientConfig.retry is now a required Context.Key<any, RetryPolicy>. makeAPI always reads the configured tag with Effect.serviceOption and falls back to Retry.makeDefault (transient/throttling/server with capped exponential backoff + jitter, 5 attempts). Removed the legacy hard-coded throttling-only branch.
  • packages/core/src/retry.ts — kept makeRetryService as the primary API for SDKs, removed the short-lived shared Retry tag from the previous commit.
  • 18 per-SDK retry.ts files (cloudflare, turso, planetscale, neon, stripe, kubernetes, axiom, posthog, gcp, coinbase, supabase, expo-eas, fly-io, mongodb-atlas, typesense, workos, prisma-postgres, azure) — each declares its own Retry tag plus matching policy, none, throttling, transient helpers (added throttling/transient everywhere for parity — previously only cloudflare had them).
  • 18 per-SDK client.ts / client/api.ts files — each imports its local Retry and passes it as retry: Retry as any into the makeAPI config.

AWS continues to use its own client and Retry tag, unchanged.

Caller-facing result

import * as Cloudflare from "@distilled.cloud/cloudflare";
import * as Turso from "@distilled.cloud/turso";

myEffect.pipe(Cloudflare.Retry.transient);
myEffect.pipe(Turso.Retry.policy({ while: isMyError, schedule: mySchedule }));

Every Cloudflare/Turso/etc. API call below transparently retries transient/throttling/server errors — no per-resource Effect.retry blocks, and policies stay scoped to each SDK.

Test plan

  • tsgo typecheck clean for core, aws, and 17 of 18 SDKs (cloudflare, turso, planetscale, neon, stripe, kubernetes, axiom, posthog, coinbase, supabase, fly-io, mongodb-atlas, typesense, workos, prisma-postgres, azure, expo-eas). gcp has a pre-existing unrelated readonly-array error verified against main.
  • Cloudflare unit tests pass (client-api, credentials, spec-emission).
  • CI: live test suites that already call Retry.transient in their test harnesses now exercise the real wiring instead of being no-ops.

The shared core `makeAPI` only ran a hard-coded throttling-only retry,
so Cloudflare's exported `Retry` Context.Service (`policy`, `none`,
`throttling`, `transient`) was effectively dead code. Callers had to
wrap individual operations with `Effect.retry({ ... })` instead of
installing one blanket policy at the layer level (the pattern AWS has
in `packages/aws/src/client/api.ts`).

Add an optional `retry?: Context.Key<any, RetryPolicy>` field to
`ClientConfig`. When set, every operation reads the policy via
`Effect.serviceOption(retry)` with `Retry.makeDefault` as the fallback
and a per-call `lastError` Ref, mirroring AWS's `outerFn`. When unset,
the legacy throttling-only schedule still runs, so other consumers
(turso, planetscale, neon, stripe, kubernetes, etc.) are unchanged
until they opt in.

Wire `retry: Retry` into Cloudflare's `makeAPI` so callers can do
`myEffect.pipe(Cloudflare.Retry.transient)` once and have every
Cloudflare API call below it transparently retry transient errors.

Co-authored-by: Cursor <cursoragent@cursor.com>
@alchemy-version-bot

alchemy-version-bot Bot commented May 2, 2026

Copy link
Copy Markdown
Contributor

Install the packages built from this commit:

@distilled.cloud/core

bun add https://pkg.distilled.cloud/core/f9eb54f

@distilled.cloud/aws

bun add https://pkg.distilled.cloud/aws/f9eb54f

@distilled.cloud/cloudflare

bun add https://pkg.distilled.cloud/cloudflare/f9eb54f

@distilled.cloud/gcp

bun add https://pkg.distilled.cloud/gcp/f9eb54f

@distilled.cloud/neon

bun add https://pkg.distilled.cloud/neon/f9eb54f

@distilled.cloud/planetscale

bun add https://pkg.distilled.cloud/planetscale/f9eb54f

@distilled.cloud/prisma-postgres

bun add https://pkg.distilled.cloud/prisma-postgres/f9eb54f

@distilled.cloud/stripe

bun add https://pkg.distilled.cloud/stripe/f9eb54f

@distilled.cloud/supabase

bun add https://pkg.distilled.cloud/supabase/f9eb54f

@distilled.cloud/posthog

bun add https://pkg.distilled.cloud/posthog/f9eb54f

@distilled.cloud/axiom

bun add https://pkg.distilled.cloud/axiom/f9eb54f

@distilled.cloud/azure

bun add https://pkg.distilled.cloud/azure/f9eb54f

@distilled.cloud/kubernetes

bun add https://pkg.distilled.cloud/kubernetes/f9eb54f

@distilled.cloud/coinbase

bun add https://pkg.distilled.cloud/coinbase/f9eb54f

@distilled.cloud/mongodb-atlas

bun add https://pkg.distilled.cloud/mongodb-atlas/f9eb54f

@distilled.cloud/fly-io

bun add https://pkg.distilled.cloud/fly-io/f9eb54f

@distilled.cloud/turso

bun add https://pkg.distilled.cloud/turso/f9eb54f

@distilled.cloud/typesense

bun add https://pkg.distilled.cloud/typesense/f9eb54f

@distilled.cloud/workos

bun add https://pkg.distilled.cloud/workos/f9eb54f

@distilled.cloud/expo-eas

bun add https://pkg.distilled.cloud/expo-eas/f9eb54f

Per-SDK Retry tags were never actually consumed: each
`packages/<sdk>/src/retry.ts` declared its own `Retry extends
Context.Service<...>("FooRetry")`, but the shared `makeAPI` in
`@distilled.cloud/core/client` had no way to read from N different tags
and so just ran a hard-coded throttling-only schedule. The previous
commit on this branch threaded a per-SDK tag through `makeAPI` config —
that worked but it left every other SDK on the legacy throttling-only
path until they migrated.

Drop the per-SDK indirection entirely:

- `packages/core/src/retry.ts` now exports a single shared `Retry`
  Context.Service plus `policy`, `none`, `throttling`, `transient`
  helpers bound to it. `makeRetryService` is kept as a deprecated
  no-op shim for any external callers.
- `packages/core/src/client.ts`'s `makeAPI` always reads the shared
  `Retry` with `Effect.serviceOption`, falling back to
  `Retry.makeDefault` (transient/throttling/server with capped
  exponential backoff + jitter, 5 attempts). The `config.retry` field
  introduced in the previous commit and the legacy hard-coded
  throttling-only branch are removed. This mirrors AWS's pattern in
  `packages/aws/src/client/api.ts`.
- All 17 per-SDK `retry.ts` files (cloudflare, turso, planetscale,
  neon, stripe, kubernetes, axiom, posthog, gcp, coinbase, supabase,
  expo-eas, fly-io, mongodb-atlas, typesense, workos, prisma-postgres,
  azure) become thin re-exports of the shared core module so existing
  call sites (`Cloudflare.Retry.transient(eff)`,
  `Layer.succeed(Turso.Retry.Retry, customPolicy)`, etc.) keep working
  but now actually take effect.

AWS continues to use its own client and its own `Retry` tag —
unchanged in this PR.

Co-authored-by: Cursor <cursoragent@cursor.com>
@sam-goodwin sam-goodwin changed the title feat(core,cloudflare): honor Retry Context.Service in shared makeAPI refactor(core,sdks): unify Retry into a single shared Context.Service May 3, 2026
sam-goodwin and others added 2 commits May 3, 2026 17:59
…client

Reverts the shared-`Retry` approach from the previous commit. Each SDK
keeps its own `Retry` Context.Service tag (so policies don't bleed
across SDKs — `Cloudflare.Retry.transient` only retries Cloudflare
calls, not turso underneath), but each SDK's `client.ts` now wires its
own tag into `makeAPI` automatically. End users just write
`myEffect.pipe(Cloudflare.Retry.transient)` — no per-SDK config to
remember.

- `packages/core/src/client.ts`: `ClientConfig.retry` is now required.
  `makeAPI` always reads the configured tag with `Effect.serviceOption`
  and falls back to `Retry.makeDefault`. Removed the legacy
  throttling-only branch.
- `packages/core/src/retry.ts`: shared `Retry` tag/`policy`/`none`/
  `throttling`/`transient` removed; `makeRetryService` restored as the
  primary API for SDKs.
- 18 per-SDK `retry.ts` files: each declares its own `Retry` tag plus
  matching `policy`, `none`, `throttling`, `transient` helpers (added
  `throttling`/`transient` everywhere for parity — previously only
  cloudflare had them).
- 18 per-SDK `client.ts` / `client/api.ts` files: each imports its
  local `Retry` and passes it as `retry: Retry as any` into the
  `makeAPI` config.

AWS continues to use its own client and `Retry` tag, unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@Mkassabov

Copy link
Copy Markdown
Contributor

Make sure create-sdk/create-sdk-full are updated so this is automatic for future sdks (just tell Claude it can update the scripts).

The tests failing rn are fine, those are broken on main. Just make sure it doesn't get worse.

Most important one is supabase since its rate limit stuff is super noticable if you break it. If that test is fine the rest should be.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants