1import * as aiplatform from "@distilled.cloud/gcp/aiplatform_v1";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import * as Schedule from "effect/Schedule";
5import { tagRecord } from "../../Tags.ts";
6import { alchemyLabelKeys, stripInternalLabels } from "../Labels.ts";
7
8export const MAX_NAME_LENGTH = 63;
9
10export const DEFAULT_TABULAR_METADATA_SCHEMA_URI =
11 "gs://google-cloud-aiplatform/schema/dataset/metadata/tabular_1.0.0.yaml";
12
13export const lastSegment = (value: string) => {
14 const trimmed = value.replace(/\/+$/, "");
15 const parts = trimmed.split("/");
16 return parts[parts.length - 1] || trimmed;
17};
18
19export const rfc1035 = (name: string): string => {
20 let next = name
21 .toLowerCase()
22 .replace(/[^a-z0-9-]/g, "-")
23 .replace(/-+/g, "-")
24 .replace(/^-+|-+$/g, "");
25 if (!/^[a-z]/.test(next)) next = `a${next}`;
26 next = next.slice(0, MAX_NAME_LENGTH).replace(/-+$/g, "");
27 if (next.length === 0) return "resource";
28 if (!/[a-z0-9]$/.test(next)) next = `${next.slice(0, MAX_NAME_LENGTH - 1)}0`;
29 return next.slice(0, MAX_NAME_LENGTH);
30};
31
32export const normalizeLocation = (
33 location: string | undefined,
34 fallback: string,
35) => lastSegment(location ?? fallback).toLowerCase();
36
37export const parentOf = (project: string, location: string) =>
38 `projects/${project}/locations/${location}`;
39
40export const userLabels = (
41 labels: Record<string, string | undefined> | null | undefined,
42): Record<string, string> => stripInternalLabels(tagRecord(labels));
43
44export const stringMapOf = (
45 map: Record<string, string | undefined> | null | undefined,
46): Record<string, string> =>
47 Object.fromEntries(
48 Object.entries(map ?? {}).filter(
49 (entry): entry is [string, string] =>
50 entry[1] !== undefined && entry[1].length > 0,
51 ),
52 );
53
54export const hasAlchemyLabelKeys = (
55 labels: Record<string, string | undefined> | null | undefined,
56) => Object.keys(labels ?? {}).some((key) => key.startsWith("alchemy-"));
57
58export const parseResourceName = (name: string, collection: string) => {
59 const parts = name.split("/").filter((part) => part.length > 0);
60 const collectionAt = parts.lastIndexOf(collection);
61 const locationsAt = parts.lastIndexOf("locations");
62 const projectsAt = parts.lastIndexOf("projects");
63 return {
64 project:
65 projectsAt >= 0 && parts[projectsAt + 1] ? parts[projectsAt + 1]! : "",
66 location:
67 locationsAt >= 0 && parts[locationsAt + 1] ? parts[locationsAt + 1]! : "",
68 id:
69 collectionAt >= 0 && parts[collectionAt + 1]
70 ? parts[collectionAt + 1]!
71 : lastSegment(name),
72 };
73};
74
75export const encodeOwnership = (
76 labels: Record<string, string>,
77 text: string | undefined,
78 separator: "\n" | " " = "\n",
79): string => {
80 const marker = `[alchemy ${alchemyLabelKeys.stack}=${labels[alchemyLabelKeys.stack]} ${alchemyLabelKeys.stage}=${labels[alchemyLabelKeys.stage]} ${alchemyLabelKeys.id}=${labels[alchemyLabelKeys.id]}]`;
81 const trimmed = text?.trim();
82 return trimmed && trimmed.length > 0
83 ? `${marker}${separator}${trimmed}`
84 : marker;
85};
86
87export const parseOwnership = (
88 text: string | undefined,
89): {
90 labels: Record<string, string>;
91 text: string | undefined;
92} => {
93 if (!text?.startsWith("[alchemy ")) {
94 return { labels: {}, text };
95 }
96 const end = text.indexOf("]");
97 if (end < 0) return { labels: {}, text };
98 const labels: Record<string, string> = {};
99 for (const part of text.slice("[alchemy ".length, end).split(/\s+/)) {
100 const eq = part.indexOf("=");
101 if (eq > 0) {
102 labels[part.slice(0, eq)] = part.slice(eq + 1);
103 }
104 }
105 const rest = text.slice(end + 1).replace(/^[\s\n]+/, "");
106 return { labels, text: rest.length > 0 ? rest : undefined };
107};
108
109export const hasOwnershipMarker = (text: string | undefined) =>
110 Object.keys(parseOwnership(text).labels).some((key) =>
111 key.startsWith("alchemy-"),
112 );
113
114export const resourceNameFromOperation = (
115 operation: aiplatform.GoogleLongrunningOperation,
116): string | undefined => {
117 const response = operation.response;
118 if (response && typeof response === "object" && "name" in response) {
119 const name = (response as { name?: unknown }).name;
120 if (typeof name === "string" && name.length > 0) return name;
121 }
122 return undefined;
123};
124
125export { waitForOperation } from "./operations.ts";
126
127export const JOB_TERMINAL_STATES = new Set([
128 "JOB_STATE_SUCCEEDED",
129 "JOB_STATE_FAILED",
130 "JOB_STATE_CANCELLED",
131 "JOB_STATE_EXPIRED",
132 "JOB_STATE_PARTIALLY_SUCCEEDED",
133]);
134
135export const isJobTerminal = (state: string | undefined) =>
136 state !== undefined && JOB_TERMINAL_STATES.has(state);