@distilled.cloud/fly-io 1.0.0-rc.12 → 1.0.0-rc.14
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/README.md +34 -0
- package/lib/credentials.d.ts +2 -2
- package/lib/credentials.d.ts.map +1 -1
- package/lib/credentials.js +2 -2
- package/lib/credentials.js.map +1 -1
- package/lib/errors.d.ts.map +1 -1
- package/lib/errors.js +1 -1
- package/lib/errors.js.map +1 -1
- package/lib/protocol.d.ts +4 -4
- package/lib/protocol.d.ts.map +1 -1
- package/lib/protocol.js +58 -62
- package/lib/protocol.js.map +1 -1
- package/lib/protocol.test.js +38 -7
- package/lib/protocol.test.js.map +1 -1
- package/lib/retry.d.ts +1 -1
- package/lib/retry.d.ts.map +1 -1
- package/lib/retry.js +1 -1
- package/lib/retry.js.map +1 -1
- package/lib/services/addons.d.ts +58 -8
- package/lib/services/addons.d.ts.map +1 -1
- package/lib/services/addons.js +40 -85
- package/lib/services/addons.js.map +1 -1
- package/lib/services/machines.d.ts +209 -60
- package/lib/services/machines.d.ts.map +1 -1
- package/lib/services/machines.js +316 -692
- package/lib/services/machines.js.map +1 -1
- package/lib/services/mpg.d.ts +2 -2
- package/lib/services/mpg.d.ts.map +1 -1
- package/lib/services/mpg.js +37 -153
- package/lib/services/mpg.js.map +1 -1
- package/lib/services/sprites.d.ts +687 -144
- package/lib/services/sprites.d.ts.map +1 -1
- package/lib/services/sprites.js +667 -183
- package/lib/services/sprites.js.map +1 -1
- package/lib/sprites-mint.test.js +4 -5
- package/lib/sprites-mint.test.js.map +1 -1
- package/lib/traits.d.ts.map +1 -1
- package/lib/traits.js.map +1 -1
- package/package.json +14 -12
- package/src/credentials.ts +8 -12
- package/src/errors.ts +6 -10
- package/src/protocol.test.ts +83 -43
- package/src/protocol.ts +119 -135
- package/src/retry.ts +2 -8
- package/src/services/addons.ts +135 -154
- package/src/services/machines.ts +694 -1222
- package/src/services/mpg.ts +59 -217
- package/src/services/sprites.ts +1573 -354
- package/src/sprites-mint.test.ts +25 -67
- package/src/traits.ts +5 -12
package/src/protocol.ts
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
import * as API from "@distilled.cloud/core/api";
|
|
2
|
+
import {
|
|
3
|
+
HTTP_STATUS_MAP,
|
|
4
|
+
InternalServerError,
|
|
5
|
+
type ConfigError,
|
|
6
|
+
} from "@distilled.cloud/core/errors";
|
|
7
|
+
import {
|
|
8
|
+
buildRequest,
|
|
9
|
+
getAnn,
|
|
10
|
+
mapKeys,
|
|
11
|
+
matchTypedError,
|
|
12
|
+
} from "@distilled.cloud/core/protocol-http";
|
|
13
|
+
import {
|
|
14
|
+
makeRestProtocol,
|
|
15
|
+
unwrapRedactedDeep,
|
|
16
|
+
wrapSensitive,
|
|
17
|
+
} from "@distilled.cloud/core/protocol-rest";
|
|
18
|
+
import { failIfStrict, validateResponse } from "@distilled.cloud/core/response-validation";
|
|
19
|
+
import { parseRetryAfterForStatus } from "@distilled.cloud/core/retry-after";
|
|
1
20
|
/**
|
|
2
21
|
* Fly.io protocols — Machines REST, UI-EX REST (MPG), GraphQL add-ons, and
|
|
3
22
|
* Sprites REST.
|
|
@@ -22,33 +41,16 @@ import * as Context from "effect/Context";
|
|
|
22
41
|
import * as Deferred from "effect/Deferred";
|
|
23
42
|
import * as Effect from "effect/Effect";
|
|
24
43
|
import * as Exit from "effect/Exit";
|
|
44
|
+
import * as HttpClient from "effect/http/HttpClient";
|
|
45
|
+
import type * as HttpClientError from "effect/http/HttpClientError";
|
|
46
|
+
import * as HttpClientRequest from "effect/http/HttpClientRequest";
|
|
47
|
+
import type * as HttpClientResponse from "effect/http/HttpClientResponse";
|
|
25
48
|
import * as Layer from "effect/Layer";
|
|
49
|
+
import * as MutableHashMap from "effect/MutableHashMap";
|
|
26
50
|
import * as Option from "effect/Option";
|
|
27
51
|
import * as Redacted from "effect/Redacted";
|
|
28
52
|
import * as Schema from "effect/Schema";
|
|
29
53
|
import type * as AST from "effect/SchemaAST";
|
|
30
|
-
import * as HttpClient from "effect/unstable/http/HttpClient";
|
|
31
|
-
import type * as HttpClientError from "effect/unstable/http/HttpClientError";
|
|
32
|
-
import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
|
|
33
|
-
import type * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
|
|
34
|
-
import * as API from "@distilled.cloud/core/api";
|
|
35
|
-
import {
|
|
36
|
-
makeRestProtocol,
|
|
37
|
-
unwrapRedactedDeep,
|
|
38
|
-
wrapSensitive,
|
|
39
|
-
} from "@distilled.cloud/core/protocol-rest";
|
|
40
|
-
import {
|
|
41
|
-
buildRequest,
|
|
42
|
-
getAnn,
|
|
43
|
-
mapKeys,
|
|
44
|
-
matchTypedError,
|
|
45
|
-
} from "@distilled.cloud/core/protocol-http";
|
|
46
|
-
import {
|
|
47
|
-
HTTP_STATUS_MAP,
|
|
48
|
-
InternalServerError,
|
|
49
|
-
type ConfigError,
|
|
50
|
-
} from "@distilled.cloud/core/errors";
|
|
51
|
-
import { parseRetryAfterForStatus } from "@distilled.cloud/core/retry-after";
|
|
52
54
|
import {
|
|
53
55
|
Credentials,
|
|
54
56
|
DEFAULT_FLY_API_BASE_URL,
|
|
@@ -97,15 +99,10 @@ const flyErrorEnvelope = (body: unknown) => {
|
|
|
97
99
|
if (body === null || typeof body !== "object") return undefined;
|
|
98
100
|
const b = body as Record<string, unknown>;
|
|
99
101
|
const errors = b.errors as Record<string, unknown> | undefined;
|
|
100
|
-
const detail =
|
|
101
|
-
errors && typeof errors.detail === "string" ? errors.detail : undefined;
|
|
102
|
+
const detail = errors && typeof errors.detail === "string" ? errors.detail : undefined;
|
|
102
103
|
return {
|
|
103
104
|
message:
|
|
104
|
-
typeof b.error === "string"
|
|
105
|
-
? b.error
|
|
106
|
-
: typeof b.message === "string"
|
|
107
|
-
? b.message
|
|
108
|
-
: detail,
|
|
105
|
+
typeof b.error === "string" ? b.error : typeof b.message === "string" ? b.message : detail,
|
|
109
106
|
};
|
|
110
107
|
};
|
|
111
108
|
|
|
@@ -114,15 +111,8 @@ const spritesErrorEnvelope = (body: unknown) => {
|
|
|
114
111
|
if (body === null || typeof body !== "object") return undefined;
|
|
115
112
|
const b = body as Record<string, unknown>;
|
|
116
113
|
const message =
|
|
117
|
-
typeof b.message === "string"
|
|
118
|
-
|
|
119
|
-
: typeof b.error === "string"
|
|
120
|
-
? b.error
|
|
121
|
-
: undefined;
|
|
122
|
-
const code =
|
|
123
|
-
typeof b.error === "string" && typeof b.message === "string"
|
|
124
|
-
? b.error
|
|
125
|
-
: undefined;
|
|
114
|
+
typeof b.message === "string" ? b.message : typeof b.error === "string" ? b.error : undefined;
|
|
115
|
+
const code = typeof b.error === "string" && typeof b.message === "string" ? b.error : undefined;
|
|
126
116
|
return { message, code };
|
|
127
117
|
};
|
|
128
118
|
|
|
@@ -149,6 +139,7 @@ const FlyIoProtocolRest: Layer.Layer<API.Protocol> = makeRestProtocol<Config>({
|
|
|
149
139
|
}),
|
|
150
140
|
errorEnvelope: flyErrorEnvelope,
|
|
151
141
|
unknownError: ({ message, body }) => new UnknownFlyIoError({ message, body }),
|
|
142
|
+
parseError: ({ body, cause }) => new FlyIoParseError({ body, cause }),
|
|
152
143
|
});
|
|
153
144
|
|
|
154
145
|
export const FlyIoProtocol: Layer.Layer<API.Protocol> = Layer.effect(
|
|
@@ -172,17 +163,16 @@ export const FlyIoProtocol: Layer.Layer<API.Protocol> = Layer.effect(
|
|
|
172
163
|
).pipe(Layer.provide(FlyIoProtocolRest));
|
|
173
164
|
|
|
174
165
|
/** MPG UI-EX REST at `https://api.fly.io` with the Machines bearer token. */
|
|
175
|
-
export const FlyApiProtocol: Layer.Layer<API.Protocol> =
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
});
|
|
166
|
+
export const FlyApiProtocol: Layer.Layer<API.Protocol> = makeRestProtocol<Config>({
|
|
167
|
+
credentials: resolveFlyCreds,
|
|
168
|
+
baseUrl: () => DEFAULT_FLY_API_BASE_URL,
|
|
169
|
+
headers: (creds) => ({
|
|
170
|
+
Authorization: `Bearer ${Redacted.value(creds.apiKey)}`,
|
|
171
|
+
}),
|
|
172
|
+
errorEnvelope: flyErrorEnvelope,
|
|
173
|
+
unknownError: ({ message, body }) => new UnknownFlyIoError({ message, body }),
|
|
174
|
+
parseError: ({ body, cause }) => new FlyIoParseError({ body, cause }),
|
|
175
|
+
});
|
|
186
176
|
|
|
187
177
|
const parseMaybeNdjson = (body: unknown): unknown => {
|
|
188
178
|
if (typeof body !== "string") return body;
|
|
@@ -228,8 +218,7 @@ const parseSpritesExecFrames = (
|
|
|
228
218
|
return { stdout, stderr, exit_code };
|
|
229
219
|
};
|
|
230
220
|
|
|
231
|
-
const fail = (e: unknown): Effect.Effect<never> =>
|
|
232
|
-
Effect.fail(e) as Effect.Effect<never>;
|
|
221
|
+
const fail = (e: unknown): Effect.Effect<never> => Effect.fail(e) as Effect.Effect<never>;
|
|
233
222
|
|
|
234
223
|
/** Strip a leading `FlyV1` / `Bearer` scheme so we can re-prefix `FlyV1`. */
|
|
235
224
|
const stripFlyAuthScheme = (token: string): string => {
|
|
@@ -250,9 +239,7 @@ const isMintOp = (http: HttpTrait | undefined): boolean =>
|
|
|
250
239
|
|
|
251
240
|
const SPRITES_NOT_ENABLED = /sprites not enabled/i;
|
|
252
241
|
|
|
253
|
-
const readResponseText = (
|
|
254
|
-
response: HttpClientResponse.HttpClientResponse,
|
|
255
|
-
): Effect.Effect<string> =>
|
|
242
|
+
const readResponseText = (response: HttpClientResponse.HttpClientResponse): Effect.Effect<string> =>
|
|
256
243
|
response.text.pipe(
|
|
257
244
|
Effect.orDie,
|
|
258
245
|
Effect.map((text) => text ?? ""),
|
|
@@ -287,8 +274,7 @@ const failHttp = (
|
|
|
287
274
|
headers: Record<string, string | undefined>,
|
|
288
275
|
): Effect.Effect<never> => {
|
|
289
276
|
const env = errorEnv(body);
|
|
290
|
-
const message =
|
|
291
|
-
env.message ?? (text.trim().length > 0 ? text.trim() : `HTTP ${status}`);
|
|
277
|
+
const message = env.message ?? (text.trim().length > 0 ? text.trim() : `HTTP ${status}`);
|
|
292
278
|
if (status === 401 && SPRITES_NOT_ENABLED.test(haystackOf(body, message))) {
|
|
293
279
|
return fail(new SpritesNotEnabled({ message }));
|
|
294
280
|
}
|
|
@@ -329,13 +315,11 @@ const orgFromGraphql = (body: unknown): string | undefined => {
|
|
|
329
315
|
if (data === null || typeof data !== "object") return undefined;
|
|
330
316
|
const viewer = (data as { viewer?: unknown }).viewer;
|
|
331
317
|
const root = viewer !== null && typeof viewer === "object" ? viewer : data;
|
|
332
|
-
const personal = (root as { personalOrganization?: { slug?: unknown } })
|
|
333
|
-
.personalOrganization;
|
|
318
|
+
const personal = (root as { personalOrganization?: { slug?: unknown } }).personalOrganization;
|
|
334
319
|
if (typeof personal?.slug === "string" && personal.slug.length > 0) {
|
|
335
320
|
return personal.slug;
|
|
336
321
|
}
|
|
337
|
-
const nodes = (root as { organizations?: { nodes?: unknown } }).organizations
|
|
338
|
-
?.nodes;
|
|
322
|
+
const nodes = (root as { organizations?: { nodes?: unknown } }).organizations?.nodes;
|
|
339
323
|
if (Array.isArray(nodes)) {
|
|
340
324
|
for (const node of nodes) {
|
|
341
325
|
if (
|
|
@@ -354,13 +338,10 @@ const orgFromGraphql = (body: unknown): string | undefined => {
|
|
|
354
338
|
const discoverOrgSlug = (fly: Config) =>
|
|
355
339
|
Effect.gen(function* () {
|
|
356
340
|
const http = yield* HttpClient.HttpClient;
|
|
357
|
-
const token = Redacted.value(fly.apiKey);
|
|
358
341
|
const machinesRoot = fly.apiBaseUrl.replace(/\/+$/, "");
|
|
359
|
-
const currentReq = HttpClientRequest.make("GET")(
|
|
360
|
-
`${machinesRoot}/v1/tokens/current`,
|
|
361
|
-
).pipe(
|
|
342
|
+
const currentReq = HttpClientRequest.make("GET")(`${machinesRoot}/v1/tokens/current`).pipe(
|
|
362
343
|
HttpClientRequest.setHeaders({
|
|
363
|
-
Authorization: `Bearer ${
|
|
344
|
+
Authorization: `Bearer ${Redacted.value(fly.apiKey)}`,
|
|
364
345
|
Accept: "application/json",
|
|
365
346
|
}),
|
|
366
347
|
);
|
|
@@ -372,16 +353,13 @@ const discoverOrgSlug = (fly: Config) =>
|
|
|
372
353
|
if (slug !== undefined) return slug;
|
|
373
354
|
}
|
|
374
355
|
|
|
375
|
-
const gqlReq = HttpClientRequest.make("POST")(
|
|
376
|
-
`${DEFAULT_FLY_API_BASE_URL}/graphql`,
|
|
377
|
-
).pipe(
|
|
356
|
+
const gqlReq = HttpClientRequest.make("POST")(`${DEFAULT_FLY_API_BASE_URL}/graphql`).pipe(
|
|
378
357
|
HttpClientRequest.setHeaders({
|
|
379
|
-
Authorization: `Bearer ${
|
|
358
|
+
Authorization: `Bearer ${Redacted.value(fly.apiKey)}`,
|
|
380
359
|
Accept: "application/json",
|
|
381
360
|
}),
|
|
382
361
|
HttpClientRequest.bodyJsonUnsafe({
|
|
383
|
-
query:
|
|
384
|
-
"{ viewer { personalOrganization { slug } organizations { nodes { slug } } } }",
|
|
362
|
+
query: "{ viewer { personalOrganization { slug } organizations { nodes { slug } } } }",
|
|
385
363
|
}),
|
|
386
364
|
);
|
|
387
365
|
const gqlRes = yield* http.execute(gqlReq);
|
|
@@ -393,8 +371,7 @@ const discoverOrgSlug = (fly: Config) =>
|
|
|
393
371
|
}
|
|
394
372
|
|
|
395
373
|
return yield* new FlyConfigError({
|
|
396
|
-
message:
|
|
397
|
-
"Could not discover a Fly organization slug from /tokens/current or GraphQL",
|
|
374
|
+
message: "Could not discover a Fly organization slug from /tokens/current or GraphQL",
|
|
398
375
|
});
|
|
399
376
|
});
|
|
400
377
|
|
|
@@ -423,9 +400,7 @@ const mintOnce = (fly: Config) =>
|
|
|
423
400
|
return yield* failHttp(response.status, json, text, headers);
|
|
424
401
|
}
|
|
425
402
|
const token =
|
|
426
|
-
json !== null && typeof json === "object"
|
|
427
|
-
? (json as { token?: unknown }).token
|
|
428
|
-
: undefined;
|
|
403
|
+
json !== null && typeof json === "object" ? (json as { token?: unknown }).token : undefined;
|
|
429
404
|
if (typeof token !== "string" || token.length === 0) {
|
|
430
405
|
return yield* fail(
|
|
431
406
|
new FlyIoParseError({
|
|
@@ -434,36 +409,40 @@ const mintOnce = (fly: Config) =>
|
|
|
434
409
|
}),
|
|
435
410
|
);
|
|
436
411
|
}
|
|
437
|
-
|
|
438
|
-
return token;
|
|
412
|
+
return Redacted.make(token);
|
|
439
413
|
});
|
|
440
414
|
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
415
|
+
// Keyed by the Fly token itself: `Redacted` hashes and compares by value.
|
|
416
|
+
const mintedTokens = MutableHashMap.empty<Redacted.Redacted<string>, Redacted.Redacted<string>>();
|
|
417
|
+
const mintInFlight = MutableHashMap.empty<
|
|
418
|
+
Redacted.Redacted<string>,
|
|
419
|
+
Deferred.Deferred<Redacted.Redacted<string>, Effect.Error<ReturnType<typeof mintOnce>>>
|
|
445
420
|
>();
|
|
446
421
|
|
|
447
422
|
/** Mint (and process-wide cache) a Sprites bearer from `FLY_API_TOKEN`. */
|
|
448
423
|
const mintSpritesToken = (fly: Config) =>
|
|
449
424
|
Effect.uninterruptibleMask((restore) =>
|
|
450
425
|
Effect.suspend(() => {
|
|
451
|
-
const key =
|
|
452
|
-
const hit =
|
|
453
|
-
if (hit
|
|
454
|
-
const pending =
|
|
455
|
-
if (
|
|
426
|
+
const key = fly.apiKey;
|
|
427
|
+
const hit = MutableHashMap.get(mintedTokens, key);
|
|
428
|
+
if (Option.isSome(hit)) return Effect.succeed(hit.value);
|
|
429
|
+
const pending = MutableHashMap.get(mintInFlight, key);
|
|
430
|
+
if (Option.isSome(pending)) {
|
|
431
|
+
return restore(Deferred.await(pending.value));
|
|
432
|
+
}
|
|
456
433
|
const result = Deferred.makeUnsafe<
|
|
457
|
-
string
|
|
434
|
+
Redacted.Redacted<string>,
|
|
458
435
|
Effect.Error<ReturnType<typeof mintOnce>>
|
|
459
436
|
>();
|
|
460
|
-
|
|
437
|
+
MutableHashMap.set(mintInFlight, key, result);
|
|
461
438
|
return restore(mintOnce(fly)).pipe(
|
|
462
439
|
Effect.onExit((exit) =>
|
|
463
440
|
Effect.gen(function* () {
|
|
464
|
-
if (Exit.isSuccess(exit))
|
|
441
|
+
if (Exit.isSuccess(exit)) {
|
|
442
|
+
MutableHashMap.set(mintedTokens, key, exit.value);
|
|
443
|
+
}
|
|
465
444
|
// Release all waiters, including on defects and owner interruption.
|
|
466
|
-
|
|
445
|
+
MutableHashMap.remove(mintInFlight, key);
|
|
467
446
|
yield* Deferred.done(result, exit);
|
|
468
447
|
}),
|
|
469
448
|
),
|
|
@@ -487,7 +466,7 @@ const encodeSpritesRequest = ({
|
|
|
487
466
|
authorization = `FlyV1 ${stripFlyAuthScheme(Redacted.value(fly.apiKey))}`;
|
|
488
467
|
} else {
|
|
489
468
|
const token = yield* mintSpritesToken(fly);
|
|
490
|
-
authorization = `Bearer ${token}`;
|
|
469
|
+
authorization = `Bearer ${Redacted.value(token)}`;
|
|
491
470
|
}
|
|
492
471
|
return buildRequest({
|
|
493
472
|
input: unwrapRedactedDeep(input),
|
|
@@ -510,9 +489,7 @@ const decodeSpritesResponse = ({
|
|
|
510
489
|
readonly errors: ReadonlyArray<unknown>;
|
|
511
490
|
}) =>
|
|
512
491
|
Effect.gen(function* () {
|
|
513
|
-
const buffer = yield* response.arrayBuffer.pipe(
|
|
514
|
-
Effect.map((bytes) => new Uint8Array(bytes)),
|
|
515
|
-
);
|
|
492
|
+
const buffer = yield* response.arrayBuffer.pipe(Effect.map((bytes) => new Uint8Array(bytes)));
|
|
516
493
|
const text = new TextDecoder().decode(buffer);
|
|
517
494
|
const execFrames = parseSpritesExecFrames(buffer);
|
|
518
495
|
let json: unknown;
|
|
@@ -529,25 +506,24 @@ const decodeSpritesResponse = ({
|
|
|
529
506
|
|
|
530
507
|
if (status >= 400) {
|
|
531
508
|
const env = nonJson ? {} : errorEnv(json);
|
|
532
|
-
const message =
|
|
533
|
-
|
|
534
|
-
(nonJson && text.trim() ? text.trim() : `HTTP ${status}`);
|
|
535
|
-
if (
|
|
536
|
-
SPRITES_NOT_ENABLED.test(haystackOf(nonJson ? text : json, message))
|
|
537
|
-
) {
|
|
509
|
+
const message = env.message ?? (nonJson && text.trim() ? text.trim() : `HTTP ${status}`);
|
|
510
|
+
if (SPRITES_NOT_ENABLED.test(haystackOf(nonJson ? text : json, message))) {
|
|
538
511
|
return yield* fail(new SpritesNotEnabled({ message }));
|
|
539
512
|
}
|
|
540
|
-
const typed = matchTypedError(
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
513
|
+
const typed = matchTypedError(
|
|
514
|
+
errorClasses,
|
|
515
|
+
status,
|
|
516
|
+
[
|
|
517
|
+
{
|
|
518
|
+
code: typeof env.code === "number" ? env.code : undefined,
|
|
519
|
+
message,
|
|
520
|
+
},
|
|
521
|
+
],
|
|
522
|
+
{ body: nonJson ? text : json, headers },
|
|
523
|
+
);
|
|
546
524
|
if (typed !== undefined) return yield* fail(typed);
|
|
547
525
|
|
|
548
|
-
const StatusErrorClass = (HTTP_STATUS_MAP as Record<number, unknown>)[
|
|
549
|
-
status
|
|
550
|
-
] as
|
|
526
|
+
const StatusErrorClass = (HTTP_STATUS_MAP as Record<number, unknown>)[status] as
|
|
551
527
|
| (new (args: {
|
|
552
528
|
message: string;
|
|
553
529
|
retryAfter?: ReturnType<typeof parseRetryAfterForStatus>;
|
|
@@ -577,13 +553,19 @@ const decodeSpritesResponse = ({
|
|
|
577
553
|
);
|
|
578
554
|
}
|
|
579
555
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
556
|
+
// Strict mode checks the mapped body (or parsed exec frames) against the
|
|
557
|
+
// output schema.
|
|
558
|
+
const body: unknown = execFrames ?? parseMaybeNdjson(nonJson ? text : (json ?? {}));
|
|
559
|
+
const mapped = yield* validateResponse(
|
|
560
|
+
outputAst,
|
|
561
|
+
mapKeys(outputAst, body, "decode"),
|
|
562
|
+
(cause) =>
|
|
563
|
+
new FlyIoParseError({
|
|
564
|
+
body: execFrames ?? (nonJson ? text : json),
|
|
565
|
+
cause,
|
|
566
|
+
}),
|
|
567
|
+
).pipe(Effect.catch(fail));
|
|
568
|
+
return wrapSensitive(outputAst, mapped);
|
|
587
569
|
});
|
|
588
570
|
|
|
589
571
|
/** Sprites REST at `https://api.sprites.dev/v1`, minted from `FLY_API_TOKEN`. */
|
|
@@ -591,9 +573,7 @@ export const SpritesProtocol: Layer.Layer<API.Protocol> = Layer.succeed(
|
|
|
591
573
|
API.Protocol,
|
|
592
574
|
API.Protocol.of({
|
|
593
575
|
encode: (args) =>
|
|
594
|
-
encodeSpritesRequest(
|
|
595
|
-
args,
|
|
596
|
-
) as Effect.Effect<HttpClientRequest.HttpClientRequest>,
|
|
576
|
+
encodeSpritesRequest(args) as Effect.Effect<HttpClientRequest.HttpClientRequest>,
|
|
597
577
|
decode: (args) => decodeSpritesResponse(args) as Effect.Effect<unknown>,
|
|
598
578
|
}),
|
|
599
579
|
);
|
|
@@ -629,11 +609,12 @@ const matchGraphqlError = (
|
|
|
629
609
|
if (envelope._tag === "Some" && envelope.value.errors?.length) {
|
|
630
610
|
const first = envelope.value.errors[0]!;
|
|
631
611
|
const message = first.message;
|
|
632
|
-
const typed = matchTypedError(errorClasses, status, [{ message }]
|
|
612
|
+
const typed = matchTypedError(errorClasses, status, [{ message }], {
|
|
613
|
+
body: errorBody,
|
|
614
|
+
headers,
|
|
615
|
+
});
|
|
633
616
|
if (typed !== undefined) return fail(typed);
|
|
634
|
-
if (
|
|
635
|
-
/not authorized to access this createextensiontosagreement/i.test(message)
|
|
636
|
-
) {
|
|
617
|
+
if (/not authorized to access this createextensiontosagreement/i.test(message)) {
|
|
637
618
|
return fail(new CreateExtensionTosAgreementNotAuthorized({ message }));
|
|
638
619
|
}
|
|
639
620
|
const StatusClass = (HTTP_STATUS_MAP as Record<number, unknown>)[status] as
|
|
@@ -692,9 +673,7 @@ const graphqlEncode = ({
|
|
|
692
673
|
}
|
|
693
674
|
const http = getAnn(inputAst, httpSymbol) as HttpTrait | undefined;
|
|
694
675
|
const variables: Record<string, unknown> = {};
|
|
695
|
-
for (const [k, v] of Object.entries(
|
|
696
|
-
(input ?? {}) as Record<string, unknown>,
|
|
697
|
-
)) {
|
|
676
|
+
for (const [k, v] of Object.entries((input ?? {}) as Record<string, unknown>)) {
|
|
698
677
|
if (v !== undefined) variables[k] = v;
|
|
699
678
|
}
|
|
700
679
|
const url = `${DEFAULT_FLY_API_BASE_URL}${http?.uri ?? "/graphql"}`;
|
|
@@ -740,12 +719,14 @@ const graphqlDecode = ({
|
|
|
740
719
|
if (status >= 400) {
|
|
741
720
|
return yield* matchGraphqlError(status, text, headers, errors);
|
|
742
721
|
}
|
|
743
|
-
|
|
722
|
+
// Lenient mode returns the body as read.
|
|
723
|
+
return yield* failIfStrict(
|
|
744
724
|
new FlyIoParseError({
|
|
745
725
|
body: text,
|
|
746
726
|
cause: "response body is not valid JSON",
|
|
747
727
|
}),
|
|
748
|
-
|
|
728
|
+
text,
|
|
729
|
+
).pipe(Effect.catch(fail));
|
|
749
730
|
}
|
|
750
731
|
|
|
751
732
|
const envelope = json as { data?: unknown; errors?: unknown[] } | null;
|
|
@@ -760,9 +741,7 @@ const graphqlDecode = ({
|
|
|
760
741
|
|
|
761
742
|
const path = getAnn(outputAst, responsePathSymbol) as string | undefined;
|
|
762
743
|
let payload: unknown =
|
|
763
|
-
envelope !== null && typeof envelope === "object"
|
|
764
|
-
? envelope.data
|
|
765
|
-
: undefined;
|
|
744
|
+
envelope !== null && typeof envelope === "object" ? envelope.data : undefined;
|
|
766
745
|
if (path !== undefined) {
|
|
767
746
|
for (const seg of path.split(".")) {
|
|
768
747
|
payload =
|
|
@@ -771,14 +750,19 @@ const graphqlDecode = ({
|
|
|
771
750
|
: undefined;
|
|
772
751
|
}
|
|
773
752
|
}
|
|
774
|
-
|
|
753
|
+
// Strict mode checks the payload at the response path against the
|
|
754
|
+
// output schema.
|
|
755
|
+
return yield* validateResponse(
|
|
756
|
+
outputAst,
|
|
757
|
+
payload === undefined ? null : payload,
|
|
758
|
+
(cause) => new FlyIoParseError({ body: json, cause }),
|
|
759
|
+
).pipe(Effect.catch(fail));
|
|
775
760
|
});
|
|
776
761
|
|
|
777
762
|
export const FlyGraphqlProtocol: Layer.Layer<API.Protocol> = Layer.succeed(
|
|
778
763
|
API.Protocol,
|
|
779
764
|
API.Protocol.of({
|
|
780
|
-
encode: (args) =>
|
|
781
|
-
graphqlEncode(args) as Effect.Effect<HttpClientRequest.HttpClientRequest>,
|
|
765
|
+
encode: (args) => graphqlEncode(args) as Effect.Effect<HttpClientRequest.HttpClientRequest>,
|
|
782
766
|
decode: graphqlDecode,
|
|
783
767
|
}),
|
|
784
768
|
);
|
package/src/retry.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Policy, throttlingFactory, transientFactory } from "@distilled.cloud/core/retry";
|
|
1
2
|
/**
|
|
2
3
|
* FlyIo retry configuration.
|
|
3
4
|
*
|
|
@@ -17,11 +18,6 @@
|
|
|
17
18
|
import * as Context from "effect/Context";
|
|
18
19
|
import * as Effect from "effect/Effect";
|
|
19
20
|
import * as Layer from "effect/Layer";
|
|
20
|
-
import {
|
|
21
|
-
type Policy,
|
|
22
|
-
throttlingFactory,
|
|
23
|
-
transientFactory,
|
|
24
|
-
} from "@distilled.cloud/core/retry";
|
|
25
21
|
|
|
26
22
|
export {
|
|
27
23
|
type Options,
|
|
@@ -44,9 +40,7 @@ export const policy = (optionsOrFactory: Policy) =>
|
|
|
44
40
|
Effect.provide(Layer.succeed(Retry, optionsOrFactory));
|
|
45
41
|
|
|
46
42
|
/** Disables all automatic retries. */
|
|
47
|
-
export const none = Effect.provide(
|
|
48
|
-
Layer.succeed(Retry, { while: () => false }),
|
|
49
|
-
);
|
|
43
|
+
export const none = Effect.provide(Layer.succeed(Retry, { while: () => false }));
|
|
50
44
|
|
|
51
45
|
/** Apply the throttling retry policy (retries throttling errors indefinitely). */
|
|
52
46
|
export const throttling = policy(throttlingFactory);
|