2022-10-20 18:50:25 -04:00
|
|
|
import { APIError } from "./APIError.ts";
|
2022-10-27 07:29:05 -04:00
|
|
|
import { S3S_NAMESPACE } from "./constant.ts";
|
2022-10-21 07:09:30 -04:00
|
|
|
import { base64, io, uuid } from "../deps.ts";
|
2022-10-18 09:16:51 -04:00
|
|
|
|
|
|
|
|
const stdinLines = io.readLines(Deno.stdin);
|
2022-10-18 08:08:26 -04:00
|
|
|
|
|
|
|
|
export function urlBase64Encode(data: ArrayBuffer) {
|
|
|
|
|
return base64.encode(data)
|
2022-10-24 03:15:46 -04:00
|
|
|
.replaceAll("+", "-")
|
|
|
|
|
.replaceAll("/", "_")
|
2022-10-18 08:08:26 -04:00
|
|
|
.replaceAll("=", "");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function urlBase64Decode(data: string) {
|
|
|
|
|
return base64.decode(
|
|
|
|
|
data
|
2022-10-24 03:15:46 -04:00
|
|
|
.replaceAll("_", "/")
|
|
|
|
|
.replaceAll("-", "+"),
|
2022-10-18 08:08:26 -04:00
|
|
|
);
|
|
|
|
|
}
|
2022-10-18 09:16:51 -04:00
|
|
|
|
2022-10-28 05:07:18 -04:00
|
|
|
export async function readline(
|
|
|
|
|
{ skipEmpty = true }: { skipEmpty?: boolean } = {},
|
|
|
|
|
) {
|
2022-10-18 09:16:51 -04:00
|
|
|
for await (const line of stdinLines) {
|
2022-10-28 05:07:18 -04:00
|
|
|
if (!skipEmpty || line !== "") {
|
2022-10-18 09:16:51 -04:00
|
|
|
return line;
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-10-20 09:45:59 -04:00
|
|
|
throw new Error("EOF");
|
2022-10-18 09:16:51 -04:00
|
|
|
}
|
2022-10-18 20:36:58 -04:00
|
|
|
|
|
|
|
|
type PromiseReturnType<T> = T extends () => Promise<infer R> ? R : never;
|
|
|
|
|
export async function retry<F extends () => Promise<unknown>>(
|
|
|
|
|
f: F,
|
|
|
|
|
times = 2,
|
|
|
|
|
): Promise<PromiseReturnType<F>> {
|
|
|
|
|
let lastError;
|
|
|
|
|
for (let i = 0; i < times; i++) {
|
|
|
|
|
try {
|
|
|
|
|
return await f() as PromiseReturnType<F>;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
lastError = e;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
throw lastError;
|
|
|
|
|
}
|
2022-10-19 04:56:18 -04:00
|
|
|
|
|
|
|
|
const GLOBAL_CACHE: Record<string, { ts: number; value: unknown }> = {};
|
|
|
|
|
export function cache<F extends () => Promise<unknown>>(
|
|
|
|
|
f: F,
|
|
|
|
|
{ key = f.name, expireIn = 3600 }: { key?: string; expireIn?: number } = {},
|
|
|
|
|
): () => Promise<PromiseReturnType<F>> {
|
|
|
|
|
return async () => {
|
|
|
|
|
const cached = GLOBAL_CACHE[key];
|
|
|
|
|
if (cached && cached.ts + expireIn * 1000 > Date.now()) {
|
|
|
|
|
return cached.value as PromiseReturnType<F>;
|
|
|
|
|
}
|
|
|
|
|
const value = await f();
|
|
|
|
|
GLOBAL_CACHE[key] = {
|
|
|
|
|
ts: Date.now(),
|
|
|
|
|
value,
|
|
|
|
|
};
|
|
|
|
|
return value as PromiseReturnType<F>;
|
|
|
|
|
};
|
|
|
|
|
}
|
2022-10-20 18:50:25 -04:00
|
|
|
|
2022-10-24 08:46:21 -04:00
|
|
|
export async function showError<T>(p: Promise<T>): Promise<T> {
|
2022-10-20 18:50:25 -04:00
|
|
|
try {
|
2022-10-24 08:46:21 -04:00
|
|
|
return await p;
|
2022-10-20 18:50:25 -04:00
|
|
|
} catch (e) {
|
|
|
|
|
if (e instanceof APIError) {
|
|
|
|
|
console.error(
|
|
|
|
|
`\n\nAPIError: ${e.message}`,
|
|
|
|
|
"\nResponse: ",
|
|
|
|
|
e.response,
|
|
|
|
|
"\nBody: ",
|
|
|
|
|
e.json,
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
console.error(e);
|
|
|
|
|
}
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-10-20 22:47:56 -04:00
|
|
|
|
2022-10-22 06:40:20 -04:00
|
|
|
/**
|
2022-10-24 14:45:36 -04:00
|
|
|
* @param id id of VsHistoryDetail or CoopHistoryDetail
|
2022-10-22 06:40:20 -04:00
|
|
|
* @param namespace uuid namespace
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
2022-10-24 14:45:36 -04:00
|
|
|
export function gameId(
|
2022-10-20 22:47:56 -04:00
|
|
|
id: string,
|
2022-10-27 07:29:05 -04:00
|
|
|
namespace = S3S_NAMESPACE,
|
2022-10-20 22:47:56 -04:00
|
|
|
): Promise<string> {
|
|
|
|
|
const fullId = base64.decode(id);
|
|
|
|
|
const tsUuid = fullId.slice(fullId.length - 52, fullId.length);
|
|
|
|
|
return uuid.v5.generate(namespace, tsUuid);
|
|
|
|
|
}
|
2022-10-22 06:40:20 -04:00
|
|
|
|
2022-10-24 14:45:36 -04:00
|
|
|
/**
|
|
|
|
|
* @param id VsHistoryDetail id or CoopHistoryDetail id
|
|
|
|
|
*/
|
|
|
|
|
export function parseHistoryDetailId(id: string) {
|
2022-10-22 06:40:20 -04:00
|
|
|
const plainText = new TextDecoder().decode(base64.decode(id));
|
|
|
|
|
|
2022-10-24 14:45:36 -04:00
|
|
|
const vsRE =
|
|
|
|
|
/VsHistoryDetail-([a-z0-9-]+):(\w+):(\d{8}T\d{6})_([0-9a-f-]{36})/;
|
|
|
|
|
const coopRE = /CoopHistoryDetail-([a-z0-9-]+):(\d{8}T\d{6})_([0-9a-f-]{36})/;
|
|
|
|
|
if (vsRE.test(plainText)) {
|
|
|
|
|
const [, uid, listType, timestamp, uuid] = plainText.match(vsRE)!;
|
2022-10-22 06:40:20 -04:00
|
|
|
|
2022-10-24 14:45:36 -04:00
|
|
|
return {
|
|
|
|
|
type: "VsHistoryDetail",
|
|
|
|
|
uid,
|
|
|
|
|
listType,
|
|
|
|
|
timestamp,
|
|
|
|
|
uuid,
|
|
|
|
|
};
|
|
|
|
|
} else if (coopRE.test(plainText)) {
|
|
|
|
|
const [, uid, timestamp, uuid] = plainText.match(coopRE)!;
|
2022-10-22 06:40:20 -04:00
|
|
|
|
2022-10-24 14:45:36 -04:00
|
|
|
return {
|
|
|
|
|
type: "CoopHistoryDetail",
|
|
|
|
|
uid,
|
|
|
|
|
timestamp,
|
|
|
|
|
uuid,
|
|
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error(`Invalid ID: ${plainText}`);
|
|
|
|
|
}
|
2022-10-22 06:40:20 -04:00
|
|
|
}
|
2022-10-22 18:52:08 -04:00
|
|
|
|
|
|
|
|
export const delay = (ms: number) =>
|
|
|
|
|
new Promise<void>((resolve) => setTimeout(resolve, ms));
|
2022-10-24 08:46:21 -04:00
|
|
|
|
|
|
|
|
export type RecoverableError = {
|
|
|
|
|
name: string;
|
|
|
|
|
is: (err: unknown) => boolean;
|
|
|
|
|
recovery: () => Promise<void>;
|
|
|
|
|
retryTimes?: number;
|
|
|
|
|
delayTime?: number;
|
|
|
|
|
};
|
|
|
|
|
export async function retryRecoverableError<F extends () => Promise<unknown>>(
|
|
|
|
|
f: F,
|
|
|
|
|
...errors: RecoverableError[]
|
|
|
|
|
): Promise<PromiseReturnType<F>> {
|
|
|
|
|
const retryTimes: Record<string, number> = Object.fromEntries(
|
|
|
|
|
errors.map(({ name, retryTimes }) => [name, retryTimes ?? 1]),
|
|
|
|
|
);
|
|
|
|
|
while (true) {
|
|
|
|
|
try {
|
|
|
|
|
return await f() as PromiseReturnType<F>;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
const error = errors.find((error) => error.is(e));
|
|
|
|
|
if (error) {
|
|
|
|
|
if (retryTimes[error.name] > 0) {
|
|
|
|
|
retryTimes[error.name]--;
|
|
|
|
|
await error.recovery();
|
|
|
|
|
await delay(error.delayTime ?? 1000);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|