import * as ps from "@distilled.cloud/planetscale";
import * as Clock from "effect/Clock";
import * as Data from "effect/Data";
import * as Effect from "effect/Effect";
import * as Schedule from "effect/Schedule";
import type { ScopedPlanStatusSession } from "../Cli/Cli.ts";
/**
* Tagged error raised when polling for a state predicate that has not yet
* been reached. Used internally with `Effect.retry` to drive long-running
* status waits.
*/
export class NotReady extends Data.TaggedError("Planetscale::NotReady")<{
description: string;
}> {}
/**
* Tagged error raised by reconcilers when an immutable property of a live
* PlanetScale resource does not match the desired configuration (e.g.
* region, kind, parent_branch). User-recoverable: change the config to
* match, or replace/delete the existing resource and retry.
*/
export class PlanetscaleConflict extends Data.TaggedError(
"Planetscale::Conflict",
)<{
message: string;
}> {}
/**
* Default polling schedule: 5s spaced retries with a 30-minute total
* budget (360 × 5s). Avoids the exponential-blowup trap where later
* iterations would wait hours, indistinguishable from a hang. Postgres
* database creates routinely run 10-12 minutes, so a 10-minute budget
* regularly false-positives as "stuck".
*/
const defaultSchedule = Schedule.max([
Schedule.spaced("5 seconds"),
Schedule.recurs(360),
]);
/**
* Generic polling helper that retries until `predicate(value)` returns true
* (or until the schedule is exhausted). Engine-specific helpers (e.g.
* `waitForKeyspaceReady`, `waitForPendingPostgresChanges`) are built on top
* of this primitive.
*/
export const pollUntil = (
description: string,
fn: Effect.Effect,
predicate: (value: A) => boolean,
schedule: Schedule.Schedule = defaultSchedule,
): Effect.Effect =>
fn.pipe(
Effect.flatMap((value) =>
predicate(value)
? Effect.succeed(value)
: Effect.fail(new NotReady({ description })),
),
Effect.retry({
while: (e: any) => e?._tag === "Planetscale::NotReady",
schedule,
}),
) as Effect.Effect;
/**
* Polls a branch via `getBranch` until it reports `ready === true`. Returns
* the final branch shape. `NotFound` during polling is treated as
* not-yet-ready (the branch is being provisioned by an upstream operation).
*
* If a `session` is supplied, each poll emits a note with elapsed seconds and
* expectation-setting so the CLI surfaces progress while we sit in the spaced
* retry loop.
*/
export const waitForBranchReady = Effect.fn(function* (
organization: string,
database: string,
branch: string,
session?: ScopedPlanStatusSession,
) {
const startedAt = yield* Clock.currentTimeMillis;
return yield* pollUntil(
`branch "${branch}" ready`,
Effect.gen(function* () {
if (session) {
const now = yield* Clock.currentTimeMillis;
const seconds = Math.floor((now - startedAt) / 1000);
yield* session.note(
`Waiting for branch to be ready... (${seconds} seconds elapsed; this can take a few minutes)`,
);
}
return yield* ps.getBranch({ organization, database, branch });
}).pipe(
Effect.catchTag("NotFound", () =>
Effect.fail(
new NotReady({ description: `branch "${branch}" not found yet` }),
),
),
),
(data) => data.ready,
);
});
/**
* Polls a database via `getDatabase` until it reports `ready === true`.
* `NotFound` during polling is treated as not-yet-ready (the database is
* being provisioned by an upstream operation).
*
* If a `session` is supplied, each poll emits a note with elapsed seconds and
* expectation-setting so the CLI surfaces progress while we sit in the spaced
* retry loop.
*/
export const waitForDatabaseReady = Effect.fn(function* (
organization: string,
database: string,
session?: ScopedPlanStatusSession,
) {
const startedAt = yield* Clock.currentTimeMillis;
return yield* pollUntil(
`database "${database}" ready`,
Effect.gen(function* () {
if (session) {
const now = yield* Clock.currentTimeMillis;
const seconds = Math.floor((now - startedAt) / 1000);
yield* session.note(
`Waiting for database to be ready... (${seconds} seconds elapsed; this can take a few minutes)`,
);
}
return yield* ps.getDatabase({ organization, database });
}).pipe(
Effect.catchTag("NotFound", () =>
Effect.fail(
new NotReady({
description: `database "${database}" not found yet`,
}),
),
),
),
(data) => data.ready,
);
});
export const isKnownError =
(tag: string, message: string) => (error: unknown) =>
typeof error === "object" &&
error !== null &&
(error as { readonly _tag?: unknown })._tag === tag &&
(error as { readonly message?: unknown }).message === message;