Files
jrshikoku/lib/voicepeakDebugLog.ts

179 lines
4.6 KiB
TypeScript

import { Platform } from "react-native";
import * as Updates from "expo-updates";
import Constants from "expo-constants";
import { AS } from "@/storageControl";
const STORAGE_KEY = "voicepeakDebugLogs";
const RETENTION_MILLISECONDS = 7 * 24 * 60 * 60 * 1000;
const MAX_LOG_ENTRIES = 5000;
export type VoicepeakDebugLogStatus = "attempting" | "success" | "error";
export type VoicepeakDebugLogEntry = {
id: string;
batchId?: string;
createdAt: string;
updatedAt: string;
status: VoicepeakDebugLogStatus;
text: string;
textLength: number;
codePointCount: number;
format: "mp3" | "wav";
baseUrl: string;
chunkIndex?: number;
totalChunks?: number;
attemptNumber?: number;
forceRequested?: boolean;
httpStatus?: number;
errorCode?: string;
requestId?: string;
cacheStatus?: string;
queueWaitMilliseconds?: number;
queueDepth?: number;
durationMilliseconds?: number;
responseBytes?: number;
warning?: string;
error?: string;
platform: string;
platformVersion: string;
appVersion?: string;
runtimeVersion?: string;
};
type NewVoicepeakDebugLog = Pick<
VoicepeakDebugLogEntry,
"text" | "format" | "baseUrl"
> &
Partial<
Pick<
VoicepeakDebugLogEntry,
| "batchId"
| "chunkIndex"
| "totalChunks"
| "attemptNumber"
| "forceRequested"
>
>;
type VoicepeakDebugLogResult = Partial<
Pick<
VoicepeakDebugLogEntry,
| "httpStatus"
| "errorCode"
| "requestId"
| "cacheStatus"
| "queueWaitMilliseconds"
| "queueDepth"
| "durationMilliseconds"
| "responseBytes"
| "warning"
| "error"
>
> & {
status: Exclude<VoicepeakDebugLogStatus, "attempting">;
};
let transactionQueue = Promise.resolve();
const serializeError = (error: unknown) => {
const message =
error instanceof Error ? `${error.name}: ${error.message}` : String(error);
return message.slice(0, 2000);
};
const readLogs = async (): Promise<VoicepeakDebugLogEntry[]> => {
try {
const stored = await AS.getItem(STORAGE_KEY);
return Array.isArray(stored) ? stored : [];
} catch {
return [];
}
};
const pruneLogs = (logs: VoicepeakDebugLogEntry[], now = Date.now()) =>
logs
.filter((log) => {
const timestamp = Date.parse(log.createdAt);
return Number.isFinite(timestamp) && now - timestamp < RETENTION_MILLISECONDS;
})
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
.slice(0, MAX_LOG_ENTRIES);
const runTransaction = <T>(operation: () => Promise<T>): Promise<T> => {
const result = transactionQueue.then(operation, operation);
transactionQueue = result.then(
() => undefined,
() => undefined
);
return result;
};
export const createVoicepeakDebugLog = async (
input: NewVoicepeakDebugLog
) => {
const now = new Date().toISOString();
const id = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
const entry: VoicepeakDebugLogEntry = {
id,
batchId: input.batchId,
createdAt: now,
updatedAt: now,
status: "attempting",
text: input.text,
textLength: input.text.length,
codePointCount: Array.from(input.text).length,
format: input.format,
baseUrl: input.baseUrl,
chunkIndex: input.chunkIndex,
totalChunks: input.totalChunks,
attemptNumber: input.attemptNumber,
forceRequested: input.forceRequested,
platform: Platform.OS,
platformVersion: String(Platform.Version),
appVersion: Constants.expoConfig?.version,
runtimeVersion: Updates.runtimeVersion ?? undefined,
};
await runTransaction(async () => {
const logs = pruneLogs([entry, ...(await readLogs())]);
await AS.setItem(STORAGE_KEY, logs);
});
return id;
};
export const completeVoicepeakDebugLog = async (
id: string,
result: VoicepeakDebugLogResult
) => {
await runTransaction(async () => {
const logs = await readLogs();
const updatedAt = new Date().toISOString();
const nextLogs = logs.map((log) =>
log.id === id
? {
...log,
...result,
warning: result.warning
? serializeError(result.warning)
: undefined,
error: result.error ? serializeError(result.error) : undefined,
updatedAt,
}
: log
);
await AS.setItem(STORAGE_KEY, pruneLogs(nextLogs));
});
};
export const getVoicepeakDebugLogs = async () =>
runTransaction(async () => {
const logs = pruneLogs(await readLogs());
await AS.setItem(STORAGE_KEY, logs);
return logs;
});
export const clearVoicepeakDebugLogs = async () =>
runTransaction(async () => {
await AS.removeItem(STORAGE_KEY).catch(() => undefined);
});