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>()( "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<ManagedEndpointAllocation | null, ManagedEndpointAllocationPersistenceError>; readonly reserve: ( input: ReserveManagedEndpointAllocationInput, ) => Effect.Effect<ManagedEndpointAllocation, ManagedEndpointAllocationPersistenceError>; readonly recordTunnel: ( input: RecordManagedEndpointTunnelInput, ) => Effect.Effect<void, ManagedEndpointAllocationPersistenceError>; readonly recordDns: ( input: RecordManagedEndpointDnsInput, ) => Effect.Effect<void, ManagedEndpointAllocationPersistenceError>; readonly markReady: ( input: ManagedEndpointAllocationKey, ) => Effect.Effect<void, ManagedEndpointAllocationPersistenceError>; readonly remove: ( input: ManagedEndpointAllocationKey, ) => Effect.Effect<void, ManagedEndpointAllocationPersistenceError>; }>()("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);
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>()(
"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<ManagedEndpointAllocation | null, ManagedEndpointAllocationPersistenceError>;
readonly reserve: (
input: ReserveManagedEndpointAllocationInput,
) => Effect.Effect<ManagedEndpointAllocation, ManagedEndpointAllocationPersistenceError>;
readonly recordTunnel: (
input: RecordManagedEndpointTunnelInput,
) => Effect.Effect<void, ManagedEndpointAllocationPersistenceError>;
readonly recordDns: (
input: RecordManagedEndpointDnsInput,
) => Effect.Effect<void, ManagedEndpointAllocationPersistenceError>;
readonly markReady: (
input: ManagedEndpointAllocationKey,
) => Effect.Effect<void, ManagedEndpointAllocationPersistenceError>;
readonly remove: (
input: ManagedEndpointAllocationKey,
) => Effect.Effect<void, ManagedEndpointAllocationPersistenceError>;
}
>()("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);