1import * as cloudidentity from "@distilled.cloud/gcp/cloudidentity_v1";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import * as Schedule from "effect/Schedule";
5
6export class CloudidentityOperationFailed extends Data.TaggedError(
7 "GCP.CloudIdentity.OperationFailed",
8)<{
9 operation: string;
10 message: string;
11}> {}
12
13export class CloudidentityOperationPending extends Data.TaggedError(
14 "GCP.CloudIdentity.OperationPending",
15)<{
16 operation: string;
17}> {}
18
19const alreadyExists = (error: cloudidentity.Status | undefined) =>
20 error?.code === 6;
21
22const isNotFoundStatus = (error: cloudidentity.Status | undefined) =>
23 error?.code === 5;
24
25export const resourceNameFromOperation = (
26 operation: cloudidentity.Operation,
27): string | undefined => {
28 const response = operation.response;
29 const responseName = response?.name;
30 if (typeof responseName === "string" && responseName.length > 0) {
31 return responseName;
32 }
33 const metadata = operation.metadata;
34 const target = metadata?.target;
35 if (typeof target === "string" && target.length > 0) {
36 return target;
37 }
38 const metadataName = metadata?.name;
39 if (typeof metadataName === "string" && metadataName.length > 0) {
40 return metadataName;
41 }
42 return undefined;
43};
44
45/**
46 * Cloud Identity create/delete/patch return long-running Operations.
47 * Distilled does not expose operations.get, so this helper settles
48 * immediately when `done` is set and otherwise treats a resource name
49 * in `response`/`metadata` as success. Callers poll get/lookup when
50 * the operation is still pending.
51 */
52export const waitForOperation = (
53 operation: cloudidentity.Operation,
54 options?: { notFoundOk?: boolean },
55) =>
56 Effect.gen(function* () {
57 const name = operation.name;
58 if (operation.error) {
59 if (alreadyExists(operation.error)) return operation;
60 if (options?.notFoundOk === true && isNotFoundStatus(operation.error)) {
61 return operation;
62 }
63 return yield* new CloudidentityOperationFailed({
64 operation: name ?? "",
65 message: operation.error.message ?? "operation failed",
66 });
67 }
68 if (operation.done === true) {
69 return operation;
70 }
71 if (resourceNameFromOperation(operation) !== undefined) {
72 return operation;
73 }
74 if (options?.notFoundOk === true) {
75 return operation;
76 }
77 return yield* new CloudidentityOperationPending({
78 operation: name ?? "",
79 });
80 });
81
82export const waitUntilPresent = <A, E, R>(
83 fetch: Effect.Effect<A | undefined, E, R>,
84 operation = "",
85) =>
86 fetch.pipe(
87 Effect.filterOrFail(
88 (value): value is A => value !== undefined,
89 () => new CloudidentityOperationPending({ operation }),
90 ),
91 Effect.retry({
92 while: (error) => error instanceof CloudidentityOperationPending,
93 times: 8,
94 schedule: Schedule.spaced("1 second"),
95 }),
96 );