@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.
Files changed (50) hide show
  1. package/README.md +34 -0
  2. package/lib/credentials.d.ts +2 -2
  3. package/lib/credentials.d.ts.map +1 -1
  4. package/lib/credentials.js +2 -2
  5. package/lib/credentials.js.map +1 -1
  6. package/lib/errors.d.ts.map +1 -1
  7. package/lib/errors.js +1 -1
  8. package/lib/errors.js.map +1 -1
  9. package/lib/protocol.d.ts +4 -4
  10. package/lib/protocol.d.ts.map +1 -1
  11. package/lib/protocol.js +58 -62
  12. package/lib/protocol.js.map +1 -1
  13. package/lib/protocol.test.js +38 -7
  14. package/lib/protocol.test.js.map +1 -1
  15. package/lib/retry.d.ts +1 -1
  16. package/lib/retry.d.ts.map +1 -1
  17. package/lib/retry.js +1 -1
  18. package/lib/retry.js.map +1 -1
  19. package/lib/services/addons.d.ts +58 -8
  20. package/lib/services/addons.d.ts.map +1 -1
  21. package/lib/services/addons.js +40 -85
  22. package/lib/services/addons.js.map +1 -1
  23. package/lib/services/machines.d.ts +209 -60
  24. package/lib/services/machines.d.ts.map +1 -1
  25. package/lib/services/machines.js +316 -692
  26. package/lib/services/machines.js.map +1 -1
  27. package/lib/services/mpg.d.ts +2 -2
  28. package/lib/services/mpg.d.ts.map +1 -1
  29. package/lib/services/mpg.js +37 -153
  30. package/lib/services/mpg.js.map +1 -1
  31. package/lib/services/sprites.d.ts +687 -144
  32. package/lib/services/sprites.d.ts.map +1 -1
  33. package/lib/services/sprites.js +667 -183
  34. package/lib/services/sprites.js.map +1 -1
  35. package/lib/sprites-mint.test.js +4 -5
  36. package/lib/sprites-mint.test.js.map +1 -1
  37. package/lib/traits.d.ts.map +1 -1
  38. package/lib/traits.js.map +1 -1
  39. package/package.json +14 -12
  40. package/src/credentials.ts +8 -12
  41. package/src/errors.ts +6 -10
  42. package/src/protocol.test.ts +83 -43
  43. package/src/protocol.ts +119 -135
  44. package/src/retry.ts +2 -8
  45. package/src/services/addons.ts +135 -154
  46. package/src/services/machines.ts +694 -1222
  47. package/src/services/mpg.ts +59 -217
  48. package/src/services/sprites.ts +1573 -354
  49. package/src/sprites-mint.test.ts +25 -67
  50. 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
- ? b.message
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
- makeRestProtocol<Config>({
177
- credentials: resolveFlyCreds,
178
- baseUrl: () => DEFAULT_FLY_API_BASE_URL,
179
- headers: (creds) => ({
180
- Authorization: `Bearer ${Redacted.value(creds.apiKey)}`,
181
- }),
182
- errorEnvelope: flyErrorEnvelope,
183
- unknownError: ({ message, body }) =>
184
- new UnknownFlyIoError({ message, body }),
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 ${token}`,
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 ${token}`,
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
- // Cache the raw string, not a Redacted whose registry entry may be erased.
438
- return token;
412
+ return Redacted.make(token);
439
413
  });
440
414
 
441
- const mintedTokens = new Map<string, string>();
442
- const mintInFlight = new Map<
443
- string,
444
- Deferred.Deferred<string, Effect.Error<ReturnType<typeof mintOnce>>>
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 = Redacted.value(fly.apiKey);
452
- const hit = mintedTokens.get(key);
453
- if (hit !== undefined) return Effect.succeed(hit);
454
- const pending = mintInFlight.get(key);
455
- if (pending !== undefined) return restore(Deferred.await(pending));
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
- mintInFlight.set(key, result);
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)) mintedTokens.set(key, exit.value);
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
- mintInFlight.delete(key);
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
- env.message ??
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(errorClasses, status, [
541
- {
542
- code: typeof env.code === "number" ? env.code : undefined,
543
- message,
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
- if (execFrames !== undefined) {
581
- return wrapSensitive(outputAst, mapKeys(outputAst, execFrames, "decode"));
582
- }
583
-
584
- let body: unknown = nonJson ? text : (json ?? {});
585
- body = parseMaybeNdjson(body);
586
- return wrapSensitive(outputAst, mapKeys(outputAst, body, "decode"));
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
- return yield* fail(
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
- return payload === undefined ? null : payload;
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);