1import * as appengine from "@distilled.cloud/gcp/appengine_v1";
2import * as Effect from "effect/Effect";
3import { waitForOperation as waitForGcpOperation } from "../Operation.ts";
4
5const lastSegment = (value: string) => {
6 const trimmed = value.replace(/\/+$/, "");
7 const parts = trimmed.split("/");
8 return parts[parts.length - 1] || trimmed;
9};
10
11export const parseOperationName = (name: string) => {
12 const parts = name.split("/").filter((part) => part.length > 0);
13 const appsAt = parts.indexOf("apps");
14 const operationsAt = parts.lastIndexOf("operations");
15 return {
16 appsId: appsAt >= 0 ? parts[appsAt + 1] : undefined,
17 operationsId:
18 operationsAt >= 0 ? parts[operationsAt + 1] : lastSegment(name),
19 };
20};
21
22/**
23 * Wait for an App Engine operation. Version deploys take several minutes.
24 * `notFoundOk` accepts an operation that finished with `NOT_FOUND` (5) or
25 * has itself been garbage-collected.
26 */
27export const waitForOperation = (
28 operation: appengine.Operation,
29 options: { appsId: string; notFoundOk?: boolean },
30) =>
31 waitForGcpOperation(
32 operation,
33 (name) => {
34 const parsed = parseOperationName(name);
35 return appengine
36 .getAppsOperations({
37 appsId: parsed.appsId ?? options.appsId,
38 operationsId: parsed.operationsId ?? lastSegment(name),
39 })
40 .pipe(
41 Effect.catchTag("NotFound", (error) =>
42 options.notFoundOk === true
43 ? Effect.succeed<appengine.Operation>({ name, done: true })
44 : Effect.fail(error),
45 ),
46 );
47 },
48 { budget: "20 minutes" },
49 ).pipe(
50 Effect.catchIf(
51 (error) =>
52 error._tag === "GCP.OperationFailed" &&
53 options.notFoundOk === true &&
54 error.code === 5,
55 () => Effect.void,
56 ),
57 Effect.asVoid,
58 );