@distilled.cloud/core 0.30.3 → 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/lib/api.d.ts +165 -0
  2. package/lib/api.d.ts.map +1 -0
  3. package/lib/api.js +178 -0
  4. package/lib/api.js.map +1 -0
  5. package/lib/codegen/cli.d.ts +29 -0
  6. package/lib/codegen/cli.d.ts.map +1 -0
  7. package/lib/codegen/cli.js +165 -0
  8. package/lib/codegen/cli.js.map +1 -0
  9. package/lib/codegen/emit.d.ts +129 -0
  10. package/lib/codegen/emit.d.ts.map +1 -0
  11. package/lib/codegen/emit.js +105 -0
  12. package/lib/codegen/emit.js.map +1 -0
  13. package/lib/codegen/format.d.ts +23 -0
  14. package/lib/codegen/format.d.ts.map +1 -0
  15. package/lib/codegen/format.js +28 -0
  16. package/lib/codegen/format.js.map +1 -0
  17. package/lib/codegen/generator.d.ts +334 -0
  18. package/lib/codegen/generator.d.ts.map +1 -0
  19. package/lib/codegen/generator.js +691 -0
  20. package/lib/codegen/generator.js.map +1 -0
  21. package/lib/codegen/graph.d.ts +36 -0
  22. package/lib/codegen/graph.d.ts.map +1 -0
  23. package/lib/codegen/graph.js +136 -0
  24. package/lib/codegen/graph.js.map +1 -0
  25. package/lib/codegen/members.d.ts +25 -0
  26. package/lib/codegen/members.d.ts.map +1 -0
  27. package/lib/codegen/members.js +55 -0
  28. package/lib/codegen/members.js.map +1 -0
  29. package/lib/codegen/naming.d.ts +29 -0
  30. package/lib/codegen/naming.d.ts.map +1 -0
  31. package/lib/codegen/naming.js +74 -0
  32. package/lib/codegen/naming.js.map +1 -0
  33. package/lib/codegen/openapi-cli.d.ts +38 -0
  34. package/lib/codegen/openapi-cli.d.ts.map +1 -0
  35. package/lib/codegen/openapi-cli.js +107 -0
  36. package/lib/codegen/openapi-cli.js.map +1 -0
  37. package/lib/codegen/openapi.d.ts +115 -0
  38. package/lib/codegen/openapi.d.ts.map +1 -0
  39. package/lib/codegen/openapi.js +1220 -0
  40. package/lib/codegen/openapi.js.map +1 -0
  41. package/lib/codegen/operations.d.ts +24 -0
  42. package/lib/codegen/operations.d.ts.map +1 -0
  43. package/lib/codegen/operations.js +56 -0
  44. package/lib/codegen/operations.js.map +1 -0
  45. package/lib/codegen/pagination.d.ts +39 -0
  46. package/lib/codegen/pagination.d.ts.map +1 -0
  47. package/lib/codegen/pagination.js +33 -0
  48. package/lib/codegen/pagination.js.map +1 -0
  49. package/lib/codegen/prelude.d.ts +15 -0
  50. package/lib/codegen/prelude.d.ts.map +1 -0
  51. package/lib/codegen/prelude.js +60 -0
  52. package/lib/codegen/prelude.js.map +1 -0
  53. package/lib/error-category.d.ts +28 -0
  54. package/lib/error-category.d.ts.map +1 -0
  55. package/lib/error-category.js +46 -0
  56. package/lib/error-category.js.map +1 -0
  57. package/lib/errors.d.ts +1 -0
  58. package/lib/errors.d.ts.map +1 -1
  59. package/lib/errors.js +1 -0
  60. package/lib/errors.js.map +1 -1
  61. package/lib/json-patch.d.ts +25 -32
  62. package/lib/json-patch.d.ts.map +1 -1
  63. package/lib/json-patch.js +23 -95
  64. package/lib/json-patch.js.map +1 -1
  65. package/lib/pagination.d.ts +37 -51
  66. package/lib/pagination.d.ts.map +1 -1
  67. package/lib/pagination.js +72 -90
  68. package/lib/pagination.js.map +1 -1
  69. package/lib/protocol-http.d.ts +74 -0
  70. package/lib/protocol-http.d.ts.map +1 -0
  71. package/lib/protocol-http.js +554 -0
  72. package/lib/protocol-http.js.map +1 -0
  73. package/lib/protocol-rest.d.ts +124 -0
  74. package/lib/protocol-rest.d.ts.map +1 -0
  75. package/lib/protocol-rest.js +242 -0
  76. package/lib/protocol-rest.js.map +1 -0
  77. package/lib/retry.d.ts +8 -2
  78. package/lib/retry.d.ts.map +1 -1
  79. package/lib/retry.js +21 -15
  80. package/lib/retry.js.map +1 -1
  81. package/lib/schema.d.ts +7 -8
  82. package/lib/schema.d.ts.map +1 -1
  83. package/lib/schema.js +7 -8
  84. package/lib/schema.js.map +1 -1
  85. package/lib/trait.d.ts +150 -0
  86. package/lib/trait.d.ts.map +1 -0
  87. package/lib/trait.js +107 -0
  88. package/lib/trait.js.map +1 -0
  89. package/package.json +18 -75
  90. package/src/api.ts +446 -0
  91. package/src/codegen/cli.ts +268 -0
  92. package/src/codegen/emit.ts +207 -0
  93. package/src/codegen/format.ts +47 -0
  94. package/src/codegen/generator.ts +1153 -0
  95. package/src/codegen/graph.ts +151 -0
  96. package/src/codegen/members.ts +71 -0
  97. package/src/codegen/naming.ts +86 -0
  98. package/src/codegen/openapi-cli.ts +166 -0
  99. package/src/codegen/openapi.ts +1450 -0
  100. package/src/codegen/operations.ts +76 -0
  101. package/src/codegen/pagination.ts +71 -0
  102. package/src/codegen/prelude.ts +70 -0
  103. package/src/error-category.ts +84 -0
  104. package/src/errors.ts +2 -0
  105. package/src/json-patch.ts +26 -110
  106. package/src/pagination.ts +86 -142
  107. package/src/protocol-http.ts +699 -0
  108. package/src/protocol-rest.ts +367 -0
  109. package/src/retry.ts +20 -21
  110. package/src/schema.ts +7 -8
  111. package/src/trait.ts +238 -0
  112. package/README.md +0 -30
  113. package/lib/client.d.ts +0 -167
  114. package/lib/client.d.ts.map +0 -1
  115. package/lib/client.js +0 -659
  116. package/lib/client.js.map +0 -1
  117. package/lib/schemas.d.ts +0 -60
  118. package/lib/schemas.d.ts.map +0 -1
  119. package/lib/schemas.js +0 -79
  120. package/lib/schemas.js.map +0 -1
  121. package/lib/sensitive.d.ts +0 -71
  122. package/lib/sensitive.d.ts.map +0 -1
  123. package/lib/sensitive.js +0 -96
  124. package/lib/sensitive.js.map +0 -1
  125. package/lib/traits.d.ts +0 -421
  126. package/lib/traits.d.ts.map +0 -1
  127. package/lib/traits.js +0 -737
  128. package/lib/traits.js.map +0 -1
  129. package/src/client.ts +0 -1177
  130. package/src/schemas.ts +0 -128
  131. package/src/sensitive.ts +0 -119
  132. package/src/traits.ts +0 -996
@@ -0,0 +1,367 @@
1
+ /**
2
+ * Generic REST protocol factory for simple bearer/header-auth JSON APIs
3
+ * (Neon, Fly.io, PlanetScale, Supabase, Turso, …).
4
+ *
5
+ * `makeRestProtocol(options)` builds a core {@link API.Protocol} layer:
6
+ *
7
+ * request: credentials resolved from the CALLING fiber's context on every
8
+ * request (the layer itself is memoized per process — see
9
+ * `core/api`), auth headers + base URL from the provider's
10
+ * options, request built by `buildRequest` from
11
+ * `core/protocol-http` (deep-unwrapping any `Redacted` input
12
+ * values first)
13
+ *
14
+ * response: 2xx JSON → optional `transformResponse` → recursive wire→TS
15
+ * key mapping (`mapKeys`) → `Redacted` wrapping of members
16
+ * marked with {@link SensitiveValue}; non-2xx → typed error:
17
+ * per-op matcher classes (`matchTypedError`), then the status
18
+ * map (default `HTTP_STATUS_MAP`), then an `InternalServerError`
19
+ * for unmapped 5xx, then the provider's `unknownError` fallback
20
+ * — with `retryAfter` durations stamped from the standard hint
21
+ * headers on retryable statuses.
22
+ *
23
+ * Anything provider-specific (envelope quirks, bespoke error codes) belongs
24
+ * in the provider package — via these options or a hand-written protocol.
25
+ */
26
+ import * as Effect from "effect/Effect";
27
+ import * as Layer from "effect/Layer";
28
+ import * as Redacted from "effect/Redacted";
29
+ import type * as AST from "effect/SchemaAST";
30
+ import type * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
31
+ import type * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
32
+ import * as API from "./api.ts";
33
+ import { makeAnnotation } from "./trait.ts";
34
+ import {
35
+ buildRequest,
36
+ getPropAnn,
37
+ getProps,
38
+ isOpaqueValue,
39
+ mapKeys,
40
+ matchTypedError,
41
+ resolveNode,
42
+ } from "./protocol-http.ts";
43
+ import { HTTP_STATUS_MAP, InternalServerError } from "./errors.ts";
44
+ import { parseRetryAfterForStatus } from "./retry-after.ts";
45
+
46
+ // =============================================================================
47
+ // Traits
48
+ // =============================================================================
49
+
50
+ export const sensitiveValueSymbol = Symbol.for(
51
+ "@distilled.cloud/core/sensitive-value",
52
+ );
53
+ /**
54
+ * Marks a string member as sensitive (mirrors `smithy.api#sensitive`).
55
+ * The REST protocol wraps decoded values in `Redacted` on the way out and
56
+ * accepts `string | Redacted<string>` on the way in (unwrapped before
57
+ * serialization). Takes an ignored argument so generators can inline the
58
+ * smithy trait value (`T.SensitiveValue({})`).
59
+ */
60
+ export const SensitiveValue = (_value?: unknown) =>
61
+ makeAnnotation(sensitiveValueSymbol, true);
62
+
63
+ export const rawResponseSymbol = Symbol.for(
64
+ "@distilled.cloud/core/raw-response",
65
+ );
66
+ /**
67
+ * Marks the sole output member that carries a bare (array/scalar) response
68
+ * body (mirrors `com.distilled.openapi#rawResponse`).
69
+ */
70
+ export const RawResponse = (_value?: unknown) =>
71
+ makeAnnotation(rawResponseSymbol, true);
72
+
73
+ export const rawResponseRootSymbol = Symbol.for(
74
+ "@distilled.cloud/core/raw-response-root",
75
+ );
76
+ /**
77
+ * Marks a response schema whose ENTIRE value is the response body (the
78
+ * generator's `rootPipe` for synthesized bare-payload wrappers): the emitted
79
+ * response type IS the payload type and the protocol returns the mapped body
80
+ * directly.
81
+ */
82
+ export const RawResponseRoot = () =>
83
+ makeAnnotation(rawResponseRootSymbol, true);
84
+
85
+ // =============================================================================
86
+ // Value helpers
87
+ // =============================================================================
88
+
89
+ const isPlainObject = (v: unknown): v is Record<string, unknown> =>
90
+ v !== null &&
91
+ typeof v === "object" &&
92
+ !Array.isArray(v) &&
93
+ !isOpaqueValue(v) &&
94
+ (Object.getPrototypeOf(v) === Object.prototype ||
95
+ Object.getPrototypeOf(v) === null);
96
+
97
+ /**
98
+ * Deep-unwrap `Redacted` values in an input (plain objects/arrays only —
99
+ * class instances, files, and binary payloads pass through untouched).
100
+ * Sensitive input members accept `string | Redacted<string>`; the wire wants
101
+ * the raw string.
102
+ */
103
+ export const unwrapRedactedDeep = (value: unknown): unknown => {
104
+ if (Redacted.isRedacted(value)) return Redacted.value(value);
105
+ if (Array.isArray(value)) return value.map(unwrapRedactedDeep);
106
+ if (isPlainObject(value)) {
107
+ const out: Record<string, unknown> = {};
108
+ for (const [k, v] of Object.entries(value)) {
109
+ if (v === undefined) continue;
110
+ out[k] = unwrapRedactedDeep(v);
111
+ }
112
+ return out;
113
+ }
114
+ return value;
115
+ };
116
+
117
+ /**
118
+ * Walk a decoded (TS-named) value alongside its schema AST, wrapping members
119
+ * marked {@link SensitiveValue} in `Redacted`. Keys the schema doesn't model
120
+ * pass through verbatim.
121
+ */
122
+ export const wrapSensitive = (ast: AST.AST, value: unknown): unknown => {
123
+ if (value === null || typeof value !== "object" || isOpaqueValue(value)) {
124
+ return value;
125
+ }
126
+ const node = resolveNode(ast);
127
+ if (node._tag === "Arrays") {
128
+ if (!Array.isArray(value)) return value;
129
+ const elem = (node as any).rest?.[0] as AST.AST | undefined;
130
+ return elem ? value.map((v) => wrapSensitive(elem, v)) : value;
131
+ }
132
+ if (node._tag === "Objects" && !Array.isArray(value)) {
133
+ const props = getProps(node);
134
+ if (props.length === 0) return value;
135
+ const byName = new Map(props.map((p) => [String(p.name), p]));
136
+ const out: Record<string, unknown> = {};
137
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
138
+ if (v === undefined) continue;
139
+ const prop = byName.get(k);
140
+ if (!prop) {
141
+ out[k] = v;
142
+ } else if (
143
+ getPropAnn(prop, sensitiveValueSymbol) !== undefined &&
144
+ typeof v === "string"
145
+ ) {
146
+ out[k] = Redacted.make(v);
147
+ } else {
148
+ out[k] = wrapSensitive(prop.type, v);
149
+ }
150
+ }
151
+ return out;
152
+ }
153
+ return value;
154
+ };
155
+
156
+ // =============================================================================
157
+ // Protocol factory
158
+ // =============================================================================
159
+
160
+ /** What the wire said about a failure, for the `unknownError` fallback. */
161
+ export interface RestErrorInfo {
162
+ readonly status: number;
163
+ /** Error code from the envelope (services vary between string and number). */
164
+ readonly code?: string | number;
165
+ readonly message: string;
166
+ /** Parsed JSON body, or the raw text when the body wasn't JSON. */
167
+ readonly body: unknown;
168
+ readonly headers: Record<string, string | undefined>;
169
+ }
170
+
171
+ export interface RestErrorEnvelope {
172
+ readonly code?: string | number;
173
+ readonly message?: string;
174
+ }
175
+
176
+ export interface RestProtocolOptions<C> {
177
+ /**
178
+ * Resolve credentials ON THE CALLING FIBER — evaluated per request, never
179
+ * at layer build time, so context-provided credentials and token refreshes
180
+ * are picked up. Typically `Effect.gen(function* () { const resolve =
181
+ * yield* Credentials; return yield* resolve; })` for the distilled
182
+ * credentials-service convention. Its error/requirement channels are
183
+ * erased at the protocol boundary (Protocol effects carry none) and
184
+ * reintroduced for callers by the generated `<Sdk>OpError` /
185
+ * `<Sdk>OpContext` annotations.
186
+ */
187
+ readonly credentials: Effect.Effect<C, any, any>;
188
+ /** API base URL from the resolved credentials. */
189
+ readonly baseUrl: (credentials: C) => string;
190
+ /** Auth (and any fixed) headers from the resolved credentials. */
191
+ readonly headers: (credentials: C) => Record<string, string>;
192
+ /**
193
+ * Extract `{ code?, message? }` from a parsed non-2xx JSON body. Default:
194
+ * lenient `{ code?, message? | error? }` (covers the common REST error
195
+ * envelopes).
196
+ */
197
+ readonly errorEnvelope?: (body: unknown) => RestErrorEnvelope | undefined;
198
+ /**
199
+ * HTTP status → error class constructed as `new Cls({ message, retryAfter
200
+ * })`. Default: core `HTTP_STATUS_MAP`. Consulted after per-op typed error
201
+ * matching; unmapped 5xx fall back to `InternalServerError`, everything
202
+ * else to {@link unknownError}.
203
+ */
204
+ readonly statusMap?: Readonly<Record<number, new (args: any) => any>>;
205
+ /** Fallback error for failures nothing else matched. */
206
+ readonly unknownError: (info: RestErrorInfo) => unknown;
207
+ /** Transform the parsed 2xx JSON before decoding (e.g. stripNulls). */
208
+ readonly transformResponse?: (body: unknown) => unknown;
209
+ /** Passed through to `buildRequest` (member-header transforms). */
210
+ readonly mapMemberHeader?: (name: string, value: string) => string;
211
+ /** Passed through to `buildRequest` (wire names for unmodeled input keys). */
212
+ readonly unknownKeyToWire?: (key: string) => string;
213
+ }
214
+
215
+ const defaultErrorEnvelope = (body: unknown): RestErrorEnvelope | undefined => {
216
+ if (body === null || typeof body !== "object") return undefined;
217
+ const b = body as Record<string, unknown>;
218
+ const code =
219
+ typeof b.code === "string" || typeof b.code === "number"
220
+ ? b.code
221
+ : undefined;
222
+ const message =
223
+ typeof b.message === "string"
224
+ ? b.message
225
+ : typeof b.error === "string"
226
+ ? b.error
227
+ : undefined;
228
+ return { code, message };
229
+ };
230
+
231
+ // Bridge: Protocol.decode is typed as Effect<unknown> (no error channel),
232
+ // but REST failures are real typed errors that operations re-surface via
233
+ // their `errors: [...]` lists. Fail with the instance and erase the type
234
+ // here; the generated operation annotations reintroduce it for callers.
235
+ const fail = (e: unknown): Effect.Effect<never> =>
236
+ Effect.fail(e) as Effect.Effect<never>;
237
+
238
+ /**
239
+ * Build a `Layer<Protocol>` for a simple REST JSON API. Assign the result to
240
+ * a module-level const in the provider's `protocol.ts` — `API.make` memoizes
241
+ * protocol layers by value identity.
242
+ */
243
+ export const makeRestProtocol = <C>(
244
+ options: RestProtocolOptions<C>,
245
+ ): Layer.Layer<API.Protocol> => {
246
+ const errorEnvelope = options.errorEnvelope ?? defaultErrorEnvelope;
247
+ const statusMap: Readonly<
248
+ Record<number, (new (args: any) => any) | undefined>
249
+ > = options.statusMap ?? HTTP_STATUS_MAP;
250
+
251
+ const encode = ({
252
+ input,
253
+ inputAst,
254
+ }: {
255
+ readonly input: unknown;
256
+ readonly inputAst: AST.AST;
257
+ }) =>
258
+ Effect.gen(function* () {
259
+ const creds = yield* options.credentials as Effect.Effect<C>;
260
+ return buildRequest({
261
+ input: unwrapRedactedDeep(input),
262
+ inputAst,
263
+ baseUrl: options.baseUrl(creds),
264
+ headers: options.headers(creds),
265
+ mapMemberHeader: options.mapMemberHeader,
266
+ unknownKeyToWire: options.unknownKeyToWire,
267
+ });
268
+ });
269
+
270
+ const decode = ({
271
+ response,
272
+ outputAst,
273
+ errors: errorClasses,
274
+ }: {
275
+ readonly response: HttpClientResponse.HttpClientResponse;
276
+ readonly outputAst: AST.AST;
277
+ readonly errors: ReadonlyArray<unknown>;
278
+ }) =>
279
+ Effect.gen(function* () {
280
+ // Read as text and parse tolerantly — error pages are often non-JSON.
281
+ const text = (yield* response.text.pipe(Effect.orDie)) ?? "";
282
+ if (process.env.DISTILLED_DEBUG_HTTP) {
283
+ console.error(
284
+ `[distilled] <- ${response.status} ${text.slice(0, 400)}`,
285
+ );
286
+ }
287
+ let json: unknown;
288
+ let nonJson = false;
289
+ if (text.trim().length > 0) {
290
+ try {
291
+ json = JSON.parse(text);
292
+ } catch {
293
+ nonJson = true;
294
+ }
295
+ }
296
+ const status = response.status;
297
+ const headers = response.headers as Record<string, string | undefined>;
298
+
299
+ if (status >= 400) {
300
+ const env = (nonJson ? undefined : errorEnvelope(json)) ?? {};
301
+ const message =
302
+ env.message ??
303
+ (nonJson && text.trim() ? text.trim() : `HTTP ${status}`);
304
+
305
+ // 1. Per-operation typed error (matcher metadata on the class).
306
+ const typed = matchTypedError(errorClasses, status, [
307
+ {
308
+ code: typeof env.code === "number" ? env.code : undefined,
309
+ message,
310
+ },
311
+ ]);
312
+ if (typed !== undefined) return yield* fail(typed);
313
+
314
+ // 2. Status-mapped class (retryAfter only stamps on retryable
315
+ // statuses — parseRetryAfterForStatus gates on that).
316
+ const StatusErrorClass = statusMap[status];
317
+ if (StatusErrorClass) {
318
+ return yield* fail(
319
+ new StatusErrorClass({
320
+ message,
321
+ retryAfter: parseRetryAfterForStatus(status, headers),
322
+ }),
323
+ );
324
+ }
325
+
326
+ // 3. Unmapped 5xx (e.g. proxy-specific statuses) → retryable server
327
+ // error rather than the unknown fallback.
328
+ if (status >= 500) {
329
+ return yield* fail(
330
+ new InternalServerError({
331
+ message,
332
+ retryAfter: parseRetryAfterForStatus(status, headers),
333
+ }),
334
+ );
335
+ }
336
+
337
+ // 4. Provider fallback.
338
+ return yield* fail(
339
+ options.unknownError({
340
+ status,
341
+ code: env.code,
342
+ message,
343
+ body: nonJson ? text : json,
344
+ headers,
345
+ }),
346
+ );
347
+ }
348
+
349
+ // 2xx: the response body IS the payload (no envelope). Wire→TS key
350
+ // mapping is schema-driven; `RawResponseRoot` responses are the body
351
+ // verbatim (mapKeys handles arrays/scalars structurally either way).
352
+ let body: unknown = nonJson ? text : (json ?? {});
353
+ if (options.transformResponse) body = options.transformResponse(body);
354
+ return wrapSensitive(outputAst, mapKeys(outputAst, body, "decode"));
355
+ });
356
+
357
+ return Layer.succeed(
358
+ API.Protocol,
359
+ API.Protocol.of({
360
+ // Erase encode's credentials requirement (resolved on the calling
361
+ // fiber; see RestProtocolOptions.credentials).
362
+ encode: (args) =>
363
+ encode(args) as Effect.Effect<HttpClientRequest.HttpClientRequest>,
364
+ decode,
365
+ }),
366
+ );
367
+ };
package/src/retry.ts CHANGED
@@ -8,7 +8,6 @@
8
8
  * for that SDK without wrapping every call with `Effect.retry`.
9
9
  */
10
10
  import * as Config from "effect/Config";
11
- import * as ConfigProvider from "effect/ConfigProvider";
12
11
  import * as Context from "effect/Context";
13
12
  import * as Duration from "effect/Duration";
14
13
  import * as Effect from "effect/Effect";
@@ -84,9 +83,7 @@ export const jittered = Schedule.addDelay(() =>
84
83
  */
85
84
  export const capped = (max: Duration.Duration) =>
86
85
  Schedule.modifyDelay(({ duration }) =>
87
- Effect.succeed(
88
- Duration.isGreaterThan(duration, max) ? Duration.millis(5000) : duration,
89
- ),
86
+ Effect.succeed(Duration.isGreaterThan(duration, max) ? max : duration),
90
87
  );
91
88
 
92
89
  // ============================================================================
@@ -148,12 +145,6 @@ export const readServerRetryHintCapMsFromEnv = (): number =>
148
145
  Effect.runSync(
149
146
  serverRetryHintCapMsConfig.pipe(
150
147
  Effect.orElseSucceed(() => DEFAULT_SERVER_RETRY_HINT_CAP_MS),
151
- // The default ConfigProvider caches env reads; use a fresh provider so
152
- // each call observes the current process.env value.
153
- Effect.provideService(
154
- ConfigProvider.ConfigProvider,
155
- ConfigProvider.fromEnv(),
156
- ),
157
148
  ),
158
149
  );
159
150
 
@@ -166,10 +157,6 @@ const resolveServerRetryHintCapMs = (): Effect.Effect<number, never, never> =>
166
157
  }
167
158
  return yield* serverRetryHintCapMsConfig.pipe(
168
159
  Effect.orElseSucceed(() => DEFAULT_SERVER_RETRY_HINT_CAP_MS),
169
- Effect.provideService(
170
- ConfigProvider.ConfigProvider,
171
- ConfigProvider.fromEnv(),
172
- ),
173
160
  );
174
161
  });
175
162
 
@@ -223,25 +210,37 @@ const honorServerHint = (
223
210
  * - Honors `error.retryAfter` (server-provided hint) with precedence, capped
224
211
  * by {@link DEFAULT_SERVER_RETRY_HINT_CAP_MS} by default; override with
225
212
  * `DISTILLED_SERVER_RETRY_HINT_CAP_MS` or {@link ServerRetryHintCapMs}
226
- * - Otherwise uses exponential backoff starting at 100ms with a factor of 2
213
+ * - Otherwise uses exponential backoff starting at 250ms with a factor of 2,
214
+ * capped at 5s per delay
227
215
  * - Ensures at least 500ms delay for throttling errors
228
- * - Limits to 5 retry attempts
216
+ * - Limits to 8 retry attempts
229
217
  * - Applies jitter to avoid thundering herd
218
+ *
219
+ * The 250ms/5s-cap/8-attempt shape (~20s of total patience) comes from
220
+ * alchemy's Cloudflare provider suite, where transient auth blips under
221
+ * high request concurrency routinely outlast a short 5-attempt/3s policy
222
+ * while a genuinely broken credential still fails within seconds.
230
223
  */
231
224
  export const makeDefault: Factory = (lastError) => ({
232
225
  while: (error) => isTransientError(error),
226
+ // The 5s cap applies to the exponential backoff BEFORE the server hint is
227
+ // considered, so a server-provided retryAfter longer than 5s is honored in
228
+ // full (bounded only by the 60s hint cap inside honorServerHint). Capping
229
+ // after the hint would silently clamp e.g. a Retry-After: 30 to 5s.
233
230
  schedule: Schedule.max([
234
231
  pipe(
235
- Schedule.exponential(100, 2),
232
+ Schedule.exponential(250, 2),
233
+ capped(Duration.seconds(5)),
236
234
  honorServerHint(lastError, (duration, error) => {
237
235
  if (isThrottling(error) && Duration.toMillis(duration) < 500) {
238
236
  return Duration.millis(500);
239
237
  }
240
238
  return duration;
241
239
  }),
240
+ jittered,
242
241
  ),
243
- Schedule.recurs(5),
244
- ]).pipe(jittered),
242
+ Schedule.recurs(8),
243
+ ]),
245
244
  });
246
245
 
247
246
  /**
@@ -252,8 +251,8 @@ export const throttlingFactory: Factory = (lastError) => ({
252
251
  while: (error) => isThrottling(error),
253
252
  schedule: pipe(
254
253
  Schedule.exponential(1000, 2),
255
- honorServerHint(lastError),
256
254
  capped(Duration.seconds(5)),
255
+ honorServerHint(lastError),
257
256
  jittered,
258
257
  ),
259
258
  });
@@ -282,8 +281,8 @@ export const transientFactory: Factory = (lastError) => ({
282
281
  while: isTransientError,
283
282
  schedule: pipe(
284
283
  Schedule.exponential(1000, 2),
285
- honorServerHint(lastError),
286
284
  capped(Duration.seconds(5)),
285
+ honorServerHint(lastError),
287
286
  jittered,
288
287
  ),
289
288
  });
package/src/schema.ts CHANGED
@@ -2,16 +2,15 @@
2
2
  * `any`-collapsing re-export of `effect/Schema` for generated service files.
3
3
  *
4
4
  * This is the single, shared definition consumed by every code-generated
5
- * provider package (AWS, Azure, Cloudflare, GCP, Kubernetes, …) via
6
- * `@distilled.cloud/core/schema`. Do not copy it into individual packages.
5
+ * provider package via `@distilled.cloud/core/schema`. Do not copy it into
6
+ * individual packages.
7
7
  *
8
8
  * Every real TYPE (`Schema`, `Codec`, `Top`, …) is re-exported untouched, so
9
- * the `Schema.Schema<Foo>` / `Schema.Codec<Foo>` annotations on generated
10
- * consts (and the public `.d.ts`) stay precise. The schema *construction
11
- * surface* (`Struct`, `optional`, `suspend`, …) and the leaf scalar schemas are
12
- * retyped to `any`, so the compiler instantiates none of the heavy Schema
13
- * generics while building a service file — the explicit annotations carry the
14
- * real types.
9
+ * the `Schema.Schema<Foo>` annotations on generated consts (and the public
10
+ * `.d.ts`) stay precise. The schema *construction surface* (`Struct`,
11
+ * `optional`, `suspend`, …) and the leaf scalar schemas are retyped to `any`,
12
+ * so the compiler instantiates none of the heavy Schema generics while
13
+ * building a service file — the explicit annotations carry the real types.
15
14
  *
16
15
  * Overriding a *value* export never affects the same-named *type* export (they
17
16
  * live in separate namespaces and continue to flow through `export *`), so