@distilled.cloud/core 0.30.2 → 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.
- package/lib/api.d.ts +165 -0
- package/lib/api.d.ts.map +1 -0
- package/lib/api.js +178 -0
- package/lib/api.js.map +1 -0
- package/lib/codegen/cli.d.ts +29 -0
- package/lib/codegen/cli.d.ts.map +1 -0
- package/lib/codegen/cli.js +165 -0
- package/lib/codegen/cli.js.map +1 -0
- package/lib/codegen/emit.d.ts +129 -0
- package/lib/codegen/emit.d.ts.map +1 -0
- package/lib/codegen/emit.js +105 -0
- package/lib/codegen/emit.js.map +1 -0
- package/lib/codegen/format.d.ts +23 -0
- package/lib/codegen/format.d.ts.map +1 -0
- package/lib/codegen/format.js +28 -0
- package/lib/codegen/format.js.map +1 -0
- package/lib/codegen/generator.d.ts +334 -0
- package/lib/codegen/generator.d.ts.map +1 -0
- package/lib/codegen/generator.js +691 -0
- package/lib/codegen/generator.js.map +1 -0
- package/lib/codegen/graph.d.ts +36 -0
- package/lib/codegen/graph.d.ts.map +1 -0
- package/lib/codegen/graph.js +136 -0
- package/lib/codegen/graph.js.map +1 -0
- package/lib/codegen/members.d.ts +25 -0
- package/lib/codegen/members.d.ts.map +1 -0
- package/lib/codegen/members.js +55 -0
- package/lib/codegen/members.js.map +1 -0
- package/lib/codegen/naming.d.ts +29 -0
- package/lib/codegen/naming.d.ts.map +1 -0
- package/lib/codegen/naming.js +74 -0
- package/lib/codegen/naming.js.map +1 -0
- package/lib/codegen/openapi-cli.d.ts +38 -0
- package/lib/codegen/openapi-cli.d.ts.map +1 -0
- package/lib/codegen/openapi-cli.js +107 -0
- package/lib/codegen/openapi-cli.js.map +1 -0
- package/lib/codegen/openapi.d.ts +115 -0
- package/lib/codegen/openapi.d.ts.map +1 -0
- package/lib/codegen/openapi.js +1220 -0
- package/lib/codegen/openapi.js.map +1 -0
- package/lib/codegen/operations.d.ts +24 -0
- package/lib/codegen/operations.d.ts.map +1 -0
- package/lib/codegen/operations.js +56 -0
- package/lib/codegen/operations.js.map +1 -0
- package/lib/codegen/pagination.d.ts +39 -0
- package/lib/codegen/pagination.d.ts.map +1 -0
- package/lib/codegen/pagination.js +33 -0
- package/lib/codegen/pagination.js.map +1 -0
- package/lib/codegen/prelude.d.ts +15 -0
- package/lib/codegen/prelude.d.ts.map +1 -0
- package/lib/codegen/prelude.js +60 -0
- package/lib/codegen/prelude.js.map +1 -0
- package/lib/error-category.d.ts +28 -0
- package/lib/error-category.d.ts.map +1 -0
- package/lib/error-category.js +46 -0
- package/lib/error-category.js.map +1 -0
- package/lib/errors.d.ts +1 -0
- package/lib/errors.d.ts.map +1 -1
- package/lib/errors.js +1 -0
- package/lib/errors.js.map +1 -1
- package/lib/json-patch.d.ts +25 -32
- package/lib/json-patch.d.ts.map +1 -1
- package/lib/json-patch.js +23 -95
- package/lib/json-patch.js.map +1 -1
- package/lib/pagination.d.ts +37 -51
- package/lib/pagination.d.ts.map +1 -1
- package/lib/pagination.js +72 -90
- package/lib/pagination.js.map +1 -1
- package/lib/protocol-http.d.ts +74 -0
- package/lib/protocol-http.d.ts.map +1 -0
- package/lib/protocol-http.js +554 -0
- package/lib/protocol-http.js.map +1 -0
- package/lib/protocol-rest.d.ts +124 -0
- package/lib/protocol-rest.d.ts.map +1 -0
- package/lib/protocol-rest.js +242 -0
- package/lib/protocol-rest.js.map +1 -0
- package/lib/retry.d.ts +8 -2
- package/lib/retry.d.ts.map +1 -1
- package/lib/retry.js +21 -15
- package/lib/retry.js.map +1 -1
- package/lib/schema.d.ts +7 -8
- package/lib/schema.d.ts.map +1 -1
- package/lib/schema.js +7 -8
- package/lib/schema.js.map +1 -1
- package/lib/trait.d.ts +150 -0
- package/lib/trait.d.ts.map +1 -0
- package/lib/trait.js +107 -0
- package/lib/trait.js.map +1 -0
- package/package.json +18 -75
- package/src/api.ts +446 -0
- package/src/codegen/cli.ts +268 -0
- package/src/codegen/emit.ts +207 -0
- package/src/codegen/format.ts +47 -0
- package/src/codegen/generator.ts +1153 -0
- package/src/codegen/graph.ts +151 -0
- package/src/codegen/members.ts +71 -0
- package/src/codegen/naming.ts +86 -0
- package/src/codegen/openapi-cli.ts +166 -0
- package/src/codegen/openapi.ts +1450 -0
- package/src/codegen/operations.ts +76 -0
- package/src/codegen/pagination.ts +71 -0
- package/src/codegen/prelude.ts +70 -0
- package/src/error-category.ts +84 -0
- package/src/errors.ts +2 -0
- package/src/json-patch.ts +26 -110
- package/src/pagination.ts +86 -142
- package/src/protocol-http.ts +699 -0
- package/src/protocol-rest.ts +367 -0
- package/src/retry.ts +20 -21
- package/src/schema.ts +7 -8
- package/src/trait.ts +238 -0
- package/README.md +0 -30
- package/lib/client.d.ts +0 -167
- package/lib/client.d.ts.map +0 -1
- package/lib/client.js +0 -659
- package/lib/client.js.map +0 -1
- package/lib/schemas.d.ts +0 -60
- package/lib/schemas.d.ts.map +0 -1
- package/lib/schemas.js +0 -79
- package/lib/schemas.js.map +0 -1
- package/lib/sensitive.d.ts +0 -71
- package/lib/sensitive.d.ts.map +0 -1
- package/lib/sensitive.js +0 -96
- package/lib/sensitive.js.map +0 -1
- package/lib/traits.d.ts +0 -421
- package/lib/traits.d.ts.map +0 -1
- package/lib/traits.js +0 -737
- package/lib/traits.js.map +0 -1
- package/src/client.ts +0 -1177
- package/src/schemas.ts +0 -128
- package/src/sensitive.ts +0 -119
- 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
|
|
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
|
|
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(
|
|
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(
|
|
244
|
-
])
|
|
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
|
|
6
|
-
*
|
|
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>`
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|