Add MineGeo gameplay systems and interface

This commit is contained in:
2026-10-10 10:27:47 +05:00
parent bf01d9801d
commit 9af71c85c2
47 changed files with 9954 additions and 93 deletions

View File

@@ -0,0 +1,154 @@
import {
FatalError,
RetryableError,
getWorkflowMetadata,
sleep,
} from "workflow";
import { admin, required } from "./supabase";
import {
DiscordClient,
DiscordDeliveryError,
discordConfiguration,
} from "./discord-client";
export async function cleanupClosedDiscordParty(event: string, claim: string) {
"use workflow";
const run = getWorkflowMetadata().workflowRunId;
try {
const deadline = await markClosed(event);
if (deadline) {
await sleep(new Date(deadline));
await deleteClosed(event);
}
} catch {
await releaseFailedCleanup(event, claim, run);
throw new Error(
"Discord party cleanup failed; the saved announcement can be retried.",
);
}
}
async function record(event: string) {
const result = await admin()
.from("discord_announcements")
.select("*")
.eq("id", event)
.maybeSingle();
if (result.error) required(result);
const job = result.data;
return job?.kind === "party" &&
job.state === "sent" &&
job.closed_at &&
!job.deleted_at &&
job.channel_id &&
job.message_id
? job
: null;
}
function client() {
const config = discordConfiguration(process.env);
if (!config.token || !config.origin)
throw new FatalError(
"Configure the Discord bot token and public site URL.",
);
return { client: new DiscordClient(config.token), origin: config.origin! };
}
function retry(error: unknown): never {
if (error instanceof DiscordDeliveryError) {
if (error.permanent) throw new FatalError(error.message);
throw new RetryableError(error.message, {
retryAfter: error.retryAfter * 1000,
});
}
throw new RetryableError("Discord cleanup could not be confirmed.", {
retryAfter: "30s",
});
}
async function markClosed(event: string): Promise<string | null> {
"use step";
try {
const job = await record(event);
if (!job) return null;
if (job.delete_after) return job.delete_after;
const setup = client();
const exists = await setup.client.closeParty(
job.channel_id!,
job.message_id!,
setup.origin,
job.target_id,
);
if (!exists) {
required(
await admin()
.from("discord_announcements")
.update({ deleted_at: new Date().toISOString(), cleanup_error: null })
.eq("id", event)
.select("id")
.single(),
);
return null;
}
const result = await admin().rpc("mark_discord_party_closed", {
p_event: event,
});
if (result.error) required(result);
return result.data;
} catch (error) {
if (error instanceof FatalError || error instanceof RetryableError)
throw error;
retry(error);
}
}
markClosed.maxRetries = 10;
async function deleteClosed(event: string) {
"use step";
try {
const job = await record(event);
if (!job) return;
if (!job.delete_after)
throw new FatalError("The closed notice has not been confirmed.");
const deadline = new Date(job.delete_after);
if (deadline.getTime() > Date.now())
throw new RetryableError("Keep the five-minute grace period.", {
retryAfter: deadline,
});
await client().client.remove(job.channel_id!, job.message_id!);
required(
await admin()
.from("discord_announcements")
.update({ deleted_at: new Date().toISOString(), cleanup_error: null })
.eq("id", event)
.select("id")
.single(),
);
} catch (error) {
if (error instanceof FatalError || error instanceof RetryableError)
throw error;
retry(error);
}
}
deleteClosed.maxRetries = 10;
async function releaseFailedCleanup(event: string, claim: string, run: string) {
"use step";
required(
await admin()
.from("discord_announcements")
.update({
cleanup_run_id: null,
cleanup_lease_until: new Date(Date.now() + 300000).toISOString(),
cleanup_error:
"Party cleanup failed. Check Discord setup and retry saved announcements.",
})
.eq("id", event)
.in("cleanup_run_id", [claim, run])
.is("deleted_at", null)
.select("id"),
);
console.error("Discord party cleanup failed", { event, run });
}
releaseFailedCleanup.maxRetries = 10;