import type { RelayManagedEndpoint } from "@t3tools/contracts/relay"; import { and, eq } from "drizzle-orm"; import * as Context from "effect/Context"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Schema from "effect/Schema"; import * as RelayDb from "../db.ts"; import { isManagedEndpointHostname, managedEndpointForHostname } from "../deploymentConfig.ts"; import { relayManagedEndpointAllocations } from "../persistence/schema.ts"; export interface ManagedEndpointAllocation { readonly userId: string; readonly environmentId: string; readonly hostname: string; readonly tunnelId: string | null; readonly tunnelName: string; readonly dnsRecordId: string | null; readonly readyAt: string | null; } export function resolveReadyManagedEndpoint(input: { readonly allocation: ManagedEndpointAllocation; readonly baseDomain: string | undefined; }): RelayManagedEndpoint | null { if ( !input.baseDomain || input.allocation.readyAt === null || input.allocation.tunnelId === null || input.allocation.dnsRecordId === null || !isManagedEndpointHostname(input.allocation.hostname, input.baseDomain) ) { return null; } return managedEndpointForHostname(input.allocation.hostname); } export class ManagedEndpointAllocationPersistenceError extends Schema.TaggedErrorClass()( "ManagedEndpointAllocationPersistenceError", { operation: Schema.Literals([ "get", "reserve", "record-tunnel", "record-dns", "mark-ready", "remove", ]), stage: Schema.Literals(["database-request", "resolve-reservation"]), userId: Schema.String, environmentId: Schema.String, hostname: Schema.optionalKey(Schema.String), tunnelName: Schema.optionalKey(Schema.String), tunnelId: Schema.optionalKey(Schema.String), dnsRecordId: Schema.optionalKey(Schema.String), cause: Schema.optional(Schema.Defect()), }, ) { override get message(): string { return `Managed endpoint allocation '${this.operation}' failed during '${this.stage}' for user '${this.userId}', environment '${this.environmentId}'`; } } interface ManagedEndpointAllocationKey { readonly userId: string; readonly environmentId: string; } interface ReserveManagedEndpointAllocationInput extends ManagedEndpointAllocationKey { readonly hostname: string; readonly tunnelName: string; } interface RecordManagedEndpointTunnelInput extends ManagedEndpointAllocationKey { readonly tunnelId: string; } interface RecordManagedEndpointDnsInput extends ManagedEndpointAllocationKey { readonly dnsRecordId: string; } export class ManagedEndpointAllocations extends Context.Service< ManagedEndpointAllocations, { readonly get: ( input: ManagedEndpointAllocationKey, ) => Effect.Effect; readonly reserve: ( input: ReserveManagedEndpointAllocationInput, ) => Effect.Effect; readonly recordTunnel: ( input: RecordManagedEndpointTunnelInput, ) => Effect.Effect; readonly recordDns: ( input: RecordManagedEndpointDnsInput, ) => Effect.Effect; readonly markReady: ( input: ManagedEndpointAllocationKey, ) => Effect.Effect; readonly remove: ( input: ManagedEndpointAllocationKey, ) => Effect.Effect; } >()("t3code-relay/environments/ManagedEndpointAllocations") {} const allocationSelection = { userId: relayManagedEndpointAllocations.userId, environmentId: relayManagedEndpointAllocations.environmentId, hostname: relayManagedEndpointAllocations.hostname, tunnelId: relayManagedEndpointAllocations.tunnelId, tunnelName: relayManagedEndpointAllocations.tunnelName, dnsRecordId: relayManagedEndpointAllocations.dnsRecordId, readyAt: relayManagedEndpointAllocations.readyAt, }; const whereAllocation = (input: ManagedEndpointAllocationKey) => and( eq(relayManagedEndpointAllocations.userId, input.userId), eq(relayManagedEndpointAllocations.environmentId, input.environmentId), ); export const make = Effect.gen(function* () { const db = yield* RelayDb.RelayDb; return ManagedEndpointAllocations.of({ get: Effect.fn("relay.managed_endpoint_allocations.get")(function* ( input: ManagedEndpointAllocationKey, ) { return yield* db .select(allocationSelection) .from(relayManagedEndpointAllocations) .where(whereAllocation(input)) .limit(1) .pipe( Effect.map((rows) => rows[0] ?? null), Effect.mapError( (cause) => new ManagedEndpointAllocationPersistenceError({ operation: "get", stage: "database-request", ...input, cause, }), ), ); }), reserve: Effect.fn("relay.managed_endpoint_allocations.reserve")(function* ( input: ReserveManagedEndpointAllocationInput, ) { const now = DateTime.formatIso(yield* DateTime.now); const inserted = yield* db .insert(relayManagedEndpointAllocations) .values({ ...input, createdAt: now, updatedAt: now, }) .onConflictDoNothing() .returning(allocationSelection) .pipe( Effect.mapError( (cause) => new ManagedEndpointAllocationPersistenceError({ operation: "reserve", stage: "database-request", ...input, cause, }), ), ); const allocation = inserted[0] ?? (yield* db .select(allocationSelection) .from(relayManagedEndpointAllocations) .where(whereAllocation(input)) .limit(1) .pipe( Effect.map((rows) => rows[0]), Effect.mapError( (cause) => new ManagedEndpointAllocationPersistenceError({ operation: "reserve", stage: "database-request", ...input, cause, }), ), )); if (allocation === undefined) { return yield* new ManagedEndpointAllocationPersistenceError({ operation: "reserve", stage: "resolve-reservation", ...input, }); } return allocation; }), recordTunnel: Effect.fn("relay.managed_endpoint_allocations.record_tunnel")(function* ( input: RecordManagedEndpointTunnelInput, ) { yield* db .update(relayManagedEndpointAllocations) .set({ tunnelId: input.tunnelId, updatedAt: DateTime.formatIso(yield* DateTime.now), }) .where(whereAllocation(input)) .pipe( Effect.mapError( (cause) => new ManagedEndpointAllocationPersistenceError({ operation: "record-tunnel", stage: "database-request", ...input, cause, }), ), ); }), recordDns: Effect.fn("relay.managed_endpoint_allocations.record_dns")(function* ( input: RecordManagedEndpointDnsInput, ) { yield* db .update(relayManagedEndpointAllocations) .set({ dnsRecordId: input.dnsRecordId, updatedAt: DateTime.formatIso(yield* DateTime.now), }) .where(whereAllocation(input)) .pipe( Effect.mapError( (cause) => new ManagedEndpointAllocationPersistenceError({ operation: "record-dns", stage: "database-request", ...input, cause, }), ), ); }), markReady: Effect.fn("relay.managed_endpoint_allocations.mark_ready")(function* ( input: ManagedEndpointAllocationKey, ) { const now = DateTime.formatIso(yield* DateTime.now); yield* db .update(relayManagedEndpointAllocations) .set({ readyAt: now, updatedAt: now, }) .where(whereAllocation(input)) .pipe( Effect.mapError( (cause) => new ManagedEndpointAllocationPersistenceError({ operation: "mark-ready", stage: "database-request", ...input, cause, }), ), ); }), remove: Effect.fn("relay.managed_endpoint_allocations.remove")(function* ( input: ManagedEndpointAllocationKey, ) { yield* db .delete(relayManagedEndpointAllocations) .where(whereAllocation(input)) .pipe( Effect.mapError( (cause) => new ManagedEndpointAllocationPersistenceError({ operation: "remove", stage: "database-request", ...input, cause, }), ), ); }), }); }); export const layer = Layer.effect(ManagedEndpointAllocations, make);