1import * as compute from "@distilled.cloud/gcp/compute_v1";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4import { waitForOperation } from "../Operation.ts";
5
6/**
7 * Compute operations settle in seconds; the slowest (managed instance
8 * groups, interconnect attachments, VPN gateways, large disks/images)
9 * take a few minutes.
10 */
11const BUDGET = "15 minutes";
12const INTERVAL = "2 seconds";
13
14export interface WaitComputeOptions {
15 /**
16 * Compute error codes (`error.errors[].code`) that mean the desired state
17 * already holds — e.g. `RESOURCE_ALREADY_EXISTS` on an insert or
18 * `RESOURCE_NOT_FOUND` on a delete. The settled operation is returned
19 * instead of failing with `GCP.OperationFailed`.
20 */
21 readonly ignore?: ReadonlyArray<string>;
22}
23
24/** The Compute error codes an operation finished with. */
25export const operationErrorCodes = (operation: compute.Operation) =>
26 (operation.error?.errors ?? []).flatMap((error) =>
27 error.code === undefined ? [] : [error.code],
28 );
29
30const settle = <E extends { readonly _tag: string }, R>(
31 operation: compute.Operation,
32 get: (name: string) => Effect.Effect<compute.Operation, E, R>,
33 options: WaitComputeOptions | undefined,
34) =>
35 Effect.suspend(() => {
36 let last = operation;
37 const ignore = options?.ignore ?? [];
38 return waitForOperation(
39 operation,
40 (name) =>
41 get(name).pipe(
42 // A freshly started operation can 404 for a moment.
43 Effect.retry({
44 while: (error) => error._tag === "NotFound",
45 times: 5,
46 schedule: Schedule.exponential("250 millis"),
47 }),
48 Effect.tap((current) =>
49 Effect.sync(() => {
50 last = current;
51 }),
52 ),
53 ),
54 { budget: BUDGET, interval: INTERVAL },
55 ).pipe(
56 Effect.map(() => last),
57 Effect.catchIf(
58 (error) =>
59 error._tag === "GCP.OperationFailed" &&
60 operationErrorCodes(last).some((code) => ignore.includes(code)),
61 () => Effect.succeed(last),
62 ),
63 );
64 });
65
66/** Wait for a zonal Compute operation. Fails with `GCP.OperationFailed` if it errored. */
67export const waitZoneOperation = (
68 project: string,
69 zone: string,
70 operation: compute.Operation,
71 options?: WaitComputeOptions,
72) =>
73 settle(
74 operation,
75 (name) => compute.getZoneOperations({ project, zone, operation: name }),
76 options,
77 );
78
79/** Wait for a regional Compute operation. Fails with `GCP.OperationFailed` if it errored. */
80export const waitRegionOperation = (
81 project: string,
82 region: string,
83 operation: compute.Operation,
84 options?: WaitComputeOptions,
85) =>
86 settle(
87 operation,
88 (name) => compute.getRegionOperations({ project, region, operation: name }),
89 options,
90 );
91
92/** Wait for a global Compute operation. Fails with `GCP.OperationFailed` if it errored. */
93export const waitGlobalOperation = (
94 project: string,
95 operation: compute.Operation,
96 options?: WaitComputeOptions,
97) =>
98 settle(
99 operation,
100 (name) => compute.getGlobalOperations({ project, operation: name }),
101 options,
102 );
103
104/**
105 * Wait for an organization-scoped Compute operation (hierarchical firewall
106 * and security policies). Fails with `GCP.OperationFailed` if it errored.
107 */
108export const waitOrganizationOperation = (
109 operation: compute.Operation,
110 parentId: string | undefined,
111 options?: WaitComputeOptions,
112) =>
113 settle(
114 operation,
115 (name) =>
116 compute.getGlobalOrganizationOperations({ operation: name, parentId }),
117 options,
118 );