179 lines
4.6 KiB
TypeScript
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);
|
|
});
|