| 1 | import * as translate from "@distilled.cloud/gcp/translate_v3"; |
| 2 | import type * as Duration from "effect/Duration"; |
| 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as Predicate from "effect/Predicate"; |
| 5 | import { |
| 6 | OperationFailed, |
| 7 | waitForOperation as waitForGcpOperation, |
| 8 | } from "../Operation.ts"; |
| 9 | |
| 10 | /** |
| 11 | * Translate reports some failures (e.g. a model created from an empty |
| 12 | * dataset) as `done: true` with the error in `metadata.error` and no |
| 13 | * top-level `error`. |
| 14 | */ |
| 15 | const metadataFailure = (operation: translate.Operation) => { |
| 16 | const error = operation.metadata?.error; |
| 17 | if (!Predicate.isObject(error)) return undefined; |
| 18 | const code = |
| 19 | Predicate.hasProperty(error, "code") && Predicate.isNumber(error.code) |
| 20 | ? error.code |
| 21 | : undefined; |
| 22 | const message = |
| 23 | Predicate.hasProperty(error, "message") && Predicate.isString(error.message) |
| 24 | ? error.message |
| 25 | : "operation failed"; |
| 26 | return new OperationFailed({ |
| 27 | operation: operation.name ?? "", |
| 28 | code, |
| 29 | reason: undefined, |
| 30 | message, |
| 31 | }); |
| 32 | }; |
| 33 | |
| 34 | /** |
| 35 | * Wait for a Translate long-running operation, then re-read it so the |
| 36 | * returned operation carries its `response`. Model create/delete and |
| 37 | * glossary create/delete are LROs; Adaptive MT datasets and glossary |
| 38 | * entries are synchronous. ALREADY_EXISTS (code 6) counts as success |
| 39 | * (create race); with `notFoundOk`, so does NOT_FOUND (code 5, delete race) |
| 40 | * and an operation that is already gone. |
| 41 | */ |
| 42 | export const waitForOperation = ( |
| 43 | operation: translate.Operation, |
| 44 | options?: { notFoundOk?: boolean; budget?: Duration.Input }, |
| 45 | ) => |
| 46 | waitForGcpOperation( |
| 47 | operation, |
| 48 | (name) => translate.getProjectsLocationsOperations({ name }), |
| 49 | { budget: options?.budget ?? "10 minutes" }, |
| 50 | ).pipe( |
| 51 | Effect.flatMap(() => |
| 52 | operation.name |
| 53 | ? translate.getProjectsLocationsOperations({ name: operation.name }) |
| 54 | : Effect.succeed(operation), |
| 55 | ), |
| 56 | Effect.flatMap((done) => { |
| 57 | const failed = metadataFailure(done); |
| 58 | return failed ? Effect.fail(failed) : Effect.succeed(done); |
| 59 | }), |
| 60 | Effect.catchIf( |
| 61 | (error) => |
| 62 | (error._tag === "GCP.OperationFailed" && |
| 63 | (error.code === 6 || |
| 64 | (options?.notFoundOk === true && error.code === 5))) || |
| 65 | (options?.notFoundOk === true && error._tag === "NotFound"), |
| 66 | () => Effect.succeed(operation), |
| 67 | ), |
| 68 | ); |