@distilled.cloud/fly-io 1.0.0-rc.13 → 1.0.0-rc.15

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 (54) hide show
  1. package/lib/credentials.d.ts +2 -2
  2. package/lib/credentials.d.ts.map +1 -1
  3. package/lib/credentials.js +2 -2
  4. package/lib/credentials.js.map +1 -1
  5. package/lib/errors.d.ts.map +1 -1
  6. package/lib/errors.js +1 -1
  7. package/lib/errors.js.map +1 -1
  8. package/lib/protocol.d.ts +3 -3
  9. package/lib/protocol.d.ts.map +1 -1
  10. package/lib/protocol.js +37 -52
  11. package/lib/protocol.js.map +1 -1
  12. package/lib/protocol.test.js +5 -4
  13. package/lib/protocol.test.js.map +1 -1
  14. package/lib/retry.d.ts +1 -1
  15. package/lib/retry.d.ts.map +1 -1
  16. package/lib/retry.js +1 -1
  17. package/lib/retry.js.map +1 -1
  18. package/lib/services/addons.d.ts +58 -8
  19. package/lib/services/addons.d.ts.map +1 -1
  20. package/lib/services/addons.js +40 -85
  21. package/lib/services/addons.js.map +1 -1
  22. package/lib/services/machines.d.ts +14 -3
  23. package/lib/services/machines.d.ts.map +1 -1
  24. package/lib/services/machines.js +179 -679
  25. package/lib/services/machines.js.map +1 -1
  26. package/lib/services/mpg.d.ts +2 -2
  27. package/lib/services/mpg.d.ts.map +1 -1
  28. package/lib/services/mpg.js +37 -153
  29. package/lib/services/mpg.js.map +1 -1
  30. package/lib/services/sprites.d.ts +687 -144
  31. package/lib/services/sprites.d.ts.map +1 -1
  32. package/lib/services/sprites.js +667 -183
  33. package/lib/services/sprites.js.map +1 -1
  34. package/lib/sprites-mint.test.js +3 -4
  35. package/lib/sprites-mint.test.js.map +1 -1
  36. package/lib/traits.d.ts.map +1 -1
  37. package/lib/traits.js.map +1 -1
  38. package/package.json +13 -11
  39. package/src/credentials.ts +8 -12
  40. package/src/errors.ts +6 -10
  41. package/src/protocol.test.ts +12 -32
  42. package/src/protocol.ts +80 -125
  43. package/src/retry.ts +2 -8
  44. package/src/services/addons.ts +135 -154
  45. package/src/services/machines.ts +338 -1189
  46. package/src/services/mpg.ts +59 -217
  47. package/src/services/sprites.ts +1573 -354
  48. package/src/sprites-mint.test.ts +23 -65
  49. package/src/traits.ts +5 -12
  50. package/lib/response-validation.test.d.ts +0 -4
  51. package/lib/response-validation.test.d.ts.map +0 -1
  52. package/lib/response-validation.test.js +0 -32
  53. package/lib/response-validation.test.js.map +0 -1
  54. package/src/response-validation.test.ts +0 -46
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,37 +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/http/HttpClient";
31
- import type * as HttpClientError from "effect/http/HttpClientError";
32
- import * as HttpClientRequest from "effect/http/HttpClientRequest";
33
- import type * as HttpClientResponse from "effect/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 {
52
- failIfStrict,
53
- validateResponse,
54
- } from "@distilled.cloud/core/response-validation";
55
- import { parseRetryAfterForStatus } from "@distilled.cloud/core/retry-after";
56
54
  import {
57
55
  Credentials,
58
56
  DEFAULT_FLY_API_BASE_URL,
@@ -101,15 +99,10 @@ const flyErrorEnvelope = (body: unknown) => {
101
99
  if (body === null || typeof body !== "object") return undefined;
102
100
  const b = body as Record<string, unknown>;
103
101
  const errors = b.errors as Record<string, unknown> | undefined;
104
- const detail =
105
- errors && typeof errors.detail === "string" ? errors.detail : undefined;
102
+ const detail = errors && typeof errors.detail === "string" ? errors.detail : undefined;
106
103
  return {
107
104
  message:
108
- typeof b.error === "string"
109
- ? b.error
110
- : typeof b.message === "string"
111
- ? b.message
112
- : detail,
105
+ typeof b.error === "string" ? b.error : typeof b.message === "string" ? b.message : detail,
113
106
  };
114
107
  };
115
108
 
@@ -118,15 +111,8 @@ const spritesErrorEnvelope = (body: unknown) => {
118
111
  if (body === null || typeof body !== "object") return undefined;
119
112
  const b = body as Record<string, unknown>;
120
113
  const message =
121
- typeof b.message === "string"
122
- ? b.message
123
- : typeof b.error === "string"
124
- ? b.error
125
- : undefined;
126
- const code =
127
- typeof b.error === "string" && typeof b.message === "string"
128
- ? b.error
129
- : 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;
130
116
  return { message, code };
131
117
  };
132
118
 
@@ -177,18 +163,16 @@ export const FlyIoProtocol: Layer.Layer<API.Protocol> = Layer.effect(
177
163
  ).pipe(Layer.provide(FlyIoProtocolRest));
178
164
 
179
165
  /** MPG UI-EX REST at `https://api.fly.io` with the Machines bearer token. */
180
- export const FlyApiProtocol: Layer.Layer<API.Protocol> =
181
- makeRestProtocol<Config>({
182
- credentials: resolveFlyCreds,
183
- baseUrl: () => DEFAULT_FLY_API_BASE_URL,
184
- headers: (creds) => ({
185
- Authorization: `Bearer ${Redacted.value(creds.apiKey)}`,
186
- }),
187
- errorEnvelope: flyErrorEnvelope,
188
- unknownError: ({ message, body }) =>
189
- new UnknownFlyIoError({ message, body }),
190
- parseError: ({ body, cause }) => new FlyIoParseError({ body, cause }),
191
- });
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
+ });
192
176
 
193
177
  const parseMaybeNdjson = (body: unknown): unknown => {
194
178
  if (typeof body !== "string") return body;
@@ -234,8 +218,7 @@ const parseSpritesExecFrames = (
234
218
  return { stdout, stderr, exit_code };
235
219
  };
236
220
 
237
- const fail = (e: unknown): Effect.Effect<never> =>
238
- Effect.fail(e) as Effect.Effect<never>;
221
+ const fail = (e: unknown): Effect.Effect<never> => Effect.fail(e) as Effect.Effect<never>;
239
222
 
240
223
  /** Strip a leading `FlyV1` / `Bearer` scheme so we can re-prefix `FlyV1`. */
241
224
  const stripFlyAuthScheme = (token: string): string => {
@@ -256,9 +239,7 @@ const isMintOp = (http: HttpTrait | undefined): boolean =>
256
239
 
257
240
  const SPRITES_NOT_ENABLED = /sprites not enabled/i;
258
241
 
259
- const readResponseText = (
260
- response: HttpClientResponse.HttpClientResponse,
261
- ): Effect.Effect<string> =>
242
+ const readResponseText = (response: HttpClientResponse.HttpClientResponse): Effect.Effect<string> =>
262
243
  response.text.pipe(
263
244
  Effect.orDie,
264
245
  Effect.map((text) => text ?? ""),
@@ -293,8 +274,7 @@ const failHttp = (
293
274
  headers: Record<string, string | undefined>,
294
275
  ): Effect.Effect<never> => {
295
276
  const env = errorEnv(body);
296
- const message =
297
- env.message ?? (text.trim().length > 0 ? text.trim() : `HTTP ${status}`);
277
+ const message = env.message ?? (text.trim().length > 0 ? text.trim() : `HTTP ${status}`);
298
278
  if (status === 401 && SPRITES_NOT_ENABLED.test(haystackOf(body, message))) {
299
279
  return fail(new SpritesNotEnabled({ message }));
300
280
  }
@@ -335,13 +315,11 @@ const orgFromGraphql = (body: unknown): string | undefined => {
335
315
  if (data === null || typeof data !== "object") return undefined;
336
316
  const viewer = (data as { viewer?: unknown }).viewer;
337
317
  const root = viewer !== null && typeof viewer === "object" ? viewer : data;
338
- const personal = (root as { personalOrganization?: { slug?: unknown } })
339
- .personalOrganization;
318
+ const personal = (root as { personalOrganization?: { slug?: unknown } }).personalOrganization;
340
319
  if (typeof personal?.slug === "string" && personal.slug.length > 0) {
341
320
  return personal.slug;
342
321
  }
343
- const nodes = (root as { organizations?: { nodes?: unknown } }).organizations
344
- ?.nodes;
322
+ const nodes = (root as { organizations?: { nodes?: unknown } }).organizations?.nodes;
345
323
  if (Array.isArray(nodes)) {
346
324
  for (const node of nodes) {
347
325
  if (
@@ -360,13 +338,10 @@ const orgFromGraphql = (body: unknown): string | undefined => {
360
338
  const discoverOrgSlug = (fly: Config) =>
361
339
  Effect.gen(function* () {
362
340
  const http = yield* HttpClient.HttpClient;
363
- const token = Redacted.value(fly.apiKey);
364
341
  const machinesRoot = fly.apiBaseUrl.replace(/\/+$/, "");
365
- const currentReq = HttpClientRequest.make("GET")(
366
- `${machinesRoot}/v1/tokens/current`,
367
- ).pipe(
342
+ const currentReq = HttpClientRequest.make("GET")(`${machinesRoot}/v1/tokens/current`).pipe(
368
343
  HttpClientRequest.setHeaders({
369
- Authorization: `Bearer ${token}`,
344
+ Authorization: `Bearer ${Redacted.value(fly.apiKey)}`,
370
345
  Accept: "application/json",
371
346
  }),
372
347
  );
@@ -378,16 +353,13 @@ const discoverOrgSlug = (fly: Config) =>
378
353
  if (slug !== undefined) return slug;
379
354
  }
380
355
 
381
- const gqlReq = HttpClientRequest.make("POST")(
382
- `${DEFAULT_FLY_API_BASE_URL}/graphql`,
383
- ).pipe(
356
+ const gqlReq = HttpClientRequest.make("POST")(`${DEFAULT_FLY_API_BASE_URL}/graphql`).pipe(
384
357
  HttpClientRequest.setHeaders({
385
- Authorization: `Bearer ${token}`,
358
+ Authorization: `Bearer ${Redacted.value(fly.apiKey)}`,
386
359
  Accept: "application/json",
387
360
  }),
388
361
  HttpClientRequest.bodyJsonUnsafe({
389
- query:
390
- "{ viewer { personalOrganization { slug } organizations { nodes { slug } } } }",
362
+ query: "{ viewer { personalOrganization { slug } organizations { nodes { slug } } } }",
391
363
  }),
392
364
  );
393
365
  const gqlRes = yield* http.execute(gqlReq);
@@ -399,8 +371,7 @@ const discoverOrgSlug = (fly: Config) =>
399
371
  }
400
372
 
401
373
  return yield* new FlyConfigError({
402
- message:
403
- "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",
404
375
  });
405
376
  });
406
377
 
@@ -429,9 +400,7 @@ const mintOnce = (fly: Config) =>
429
400
  return yield* failHttp(response.status, json, text, headers);
430
401
  }
431
402
  const token =
432
- json !== null && typeof json === "object"
433
- ? (json as { token?: unknown }).token
434
- : undefined;
403
+ json !== null && typeof json === "object" ? (json as { token?: unknown }).token : undefined;
435
404
  if (typeof token !== "string" || token.length === 0) {
436
405
  return yield* fail(
437
406
  new FlyIoParseError({
@@ -440,36 +409,40 @@ const mintOnce = (fly: Config) =>
440
409
  }),
441
410
  );
442
411
  }
443
- // Cache the raw string, not a Redacted whose registry entry may be erased.
444
- return token;
412
+ return Redacted.make(token);
445
413
  });
446
414
 
447
- const mintedTokens = new Map<string, string>();
448
- const mintInFlight = new Map<
449
- string,
450
- 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>>>
451
420
  >();
452
421
 
453
422
  /** Mint (and process-wide cache) a Sprites bearer from `FLY_API_TOKEN`. */
454
423
  const mintSpritesToken = (fly: Config) =>
455
424
  Effect.uninterruptibleMask((restore) =>
456
425
  Effect.suspend(() => {
457
- const key = Redacted.value(fly.apiKey);
458
- const hit = mintedTokens.get(key);
459
- if (hit !== undefined) return Effect.succeed(hit);
460
- const pending = mintInFlight.get(key);
461
- 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
+ }
462
433
  const result = Deferred.makeUnsafe<
463
- string,
434
+ Redacted.Redacted<string>,
464
435
  Effect.Error<ReturnType<typeof mintOnce>>
465
436
  >();
466
- mintInFlight.set(key, result);
437
+ MutableHashMap.set(mintInFlight, key, result);
467
438
  return restore(mintOnce(fly)).pipe(
468
439
  Effect.onExit((exit) =>
469
440
  Effect.gen(function* () {
470
- if (Exit.isSuccess(exit)) mintedTokens.set(key, exit.value);
441
+ if (Exit.isSuccess(exit)) {
442
+ MutableHashMap.set(mintedTokens, key, exit.value);
443
+ }
471
444
  // Release all waiters, including on defects and owner interruption.
472
- mintInFlight.delete(key);
445
+ MutableHashMap.remove(mintInFlight, key);
473
446
  yield* Deferred.done(result, exit);
474
447
  }),
475
448
  ),
@@ -493,7 +466,7 @@ const encodeSpritesRequest = ({
493
466
  authorization = `FlyV1 ${stripFlyAuthScheme(Redacted.value(fly.apiKey))}`;
494
467
  } else {
495
468
  const token = yield* mintSpritesToken(fly);
496
- authorization = `Bearer ${token}`;
469
+ authorization = `Bearer ${Redacted.value(token)}`;
497
470
  }
498
471
  return buildRequest({
499
472
  input: unwrapRedactedDeep(input),
@@ -516,9 +489,7 @@ const decodeSpritesResponse = ({
516
489
  readonly errors: ReadonlyArray<unknown>;
517
490
  }) =>
518
491
  Effect.gen(function* () {
519
- const buffer = yield* response.arrayBuffer.pipe(
520
- Effect.map((bytes) => new Uint8Array(bytes)),
521
- );
492
+ const buffer = yield* response.arrayBuffer.pipe(Effect.map((bytes) => new Uint8Array(bytes)));
522
493
  const text = new TextDecoder().decode(buffer);
523
494
  const execFrames = parseSpritesExecFrames(buffer);
524
495
  let json: unknown;
@@ -535,12 +506,8 @@ const decodeSpritesResponse = ({
535
506
 
536
507
  if (status >= 400) {
537
508
  const env = nonJson ? {} : errorEnv(json);
538
- const message =
539
- env.message ??
540
- (nonJson && text.trim() ? text.trim() : `HTTP ${status}`);
541
- if (
542
- SPRITES_NOT_ENABLED.test(haystackOf(nonJson ? text : json, message))
543
- ) {
509
+ const message = env.message ?? (nonJson && text.trim() ? text.trim() : `HTTP ${status}`);
510
+ if (SPRITES_NOT_ENABLED.test(haystackOf(nonJson ? text : json, message))) {
544
511
  return yield* fail(new SpritesNotEnabled({ message }));
545
512
  }
546
513
  const typed = matchTypedError(
@@ -556,9 +523,7 @@ const decodeSpritesResponse = ({
556
523
  );
557
524
  if (typed !== undefined) return yield* fail(typed);
558
525
 
559
- const StatusErrorClass = (HTTP_STATUS_MAP as Record<number, unknown>)[
560
- status
561
- ] as
526
+ const StatusErrorClass = (HTTP_STATUS_MAP as Record<number, unknown>)[status] as
562
527
  | (new (args: {
563
528
  message: string;
564
529
  retryAfter?: ReturnType<typeof parseRetryAfterForStatus>;
@@ -590,8 +555,7 @@ const decodeSpritesResponse = ({
590
555
 
591
556
  // Strict mode checks the mapped body (or parsed exec frames) against the
592
557
  // output schema.
593
- const body: unknown =
594
- execFrames ?? parseMaybeNdjson(nonJson ? text : (json ?? {}));
558
+ const body: unknown = execFrames ?? parseMaybeNdjson(nonJson ? text : (json ?? {}));
595
559
  const mapped = yield* validateResponse(
596
560
  outputAst,
597
561
  mapKeys(outputAst, body, "decode"),
@@ -609,9 +573,7 @@ export const SpritesProtocol: Layer.Layer<API.Protocol> = Layer.succeed(
609
573
  API.Protocol,
610
574
  API.Protocol.of({
611
575
  encode: (args) =>
612
- encodeSpritesRequest(
613
- args,
614
- ) as Effect.Effect<HttpClientRequest.HttpClientRequest>,
576
+ encodeSpritesRequest(args) as Effect.Effect<HttpClientRequest.HttpClientRequest>,
615
577
  decode: (args) => decodeSpritesResponse(args) as Effect.Effect<unknown>,
616
578
  }),
617
579
  );
@@ -652,9 +614,7 @@ const matchGraphqlError = (
652
614
  headers,
653
615
  });
654
616
  if (typed !== undefined) return fail(typed);
655
- if (
656
- /not authorized to access this createextensiontosagreement/i.test(message)
657
- ) {
617
+ if (/not authorized to access this createextensiontosagreement/i.test(message)) {
658
618
  return fail(new CreateExtensionTosAgreementNotAuthorized({ message }));
659
619
  }
660
620
  const StatusClass = (HTTP_STATUS_MAP as Record<number, unknown>)[status] as
@@ -713,9 +673,7 @@ const graphqlEncode = ({
713
673
  }
714
674
  const http = getAnn(inputAst, httpSymbol) as HttpTrait | undefined;
715
675
  const variables: Record<string, unknown> = {};
716
- for (const [k, v] of Object.entries(
717
- (input ?? {}) as Record<string, unknown>,
718
- )) {
676
+ for (const [k, v] of Object.entries((input ?? {}) as Record<string, unknown>)) {
719
677
  if (v !== undefined) variables[k] = v;
720
678
  }
721
679
  const url = `${DEFAULT_FLY_API_BASE_URL}${http?.uri ?? "/graphql"}`;
@@ -783,9 +741,7 @@ const graphqlDecode = ({
783
741
 
784
742
  const path = getAnn(outputAst, responsePathSymbol) as string | undefined;
785
743
  let payload: unknown =
786
- envelope !== null && typeof envelope === "object"
787
- ? envelope.data
788
- : undefined;
744
+ envelope !== null && typeof envelope === "object" ? envelope.data : undefined;
789
745
  if (path !== undefined) {
790
746
  for (const seg of path.split(".")) {
791
747
  payload =
@@ -806,8 +762,7 @@ const graphqlDecode = ({
806
762
  export const FlyGraphqlProtocol: Layer.Layer<API.Protocol> = Layer.succeed(
807
763
  API.Protocol,
808
764
  API.Protocol.of({
809
- encode: (args) =>
810
- graphqlEncode(args) as Effect.Effect<HttpClientRequest.HttpClientRequest>,
765
+ encode: (args) => graphqlEncode(args) as Effect.Effect<HttpClientRequest.HttpClientRequest>,
811
766
  decode: graphqlDecode,
812
767
  }),
813
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);