feat(import): run catalog synchronizations as durable jobs
This commit is contained in:
1 parent
a2954e4408
commit
76f0420d64
14 files changed
+559
-801
No files matched your search
@@ -1,4 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { createStore, runWithStore } from "@/lib/foundation/request-context";
|
||||
import type { IpAddress, RequestId, UserId } from "@/lib/foundation/types";
|
||||
import type { ImportJob } from "@/lib/furni/import-job";
|
||||
import { redis } from "@/lib/redis";
|
||||
import { logServerError } from "@/lib/server-log";
|
||||
@@ -10,6 +12,7 @@ import { readFurnitureAttachment } from "./furni-attachment";
|
||||
import { patchLocalizedFurniDataEntries } from "./furni-data-i18n";
|
||||
import { ensureDirectories, importSingleFurni } from "./furni-import";
|
||||
import { ImportJobStore } from "./furni-job-store";
|
||||
import { runSyncJobItem } from "./furni-sync-item";
|
||||
import { repairFurniture } from "./furniture-repair";
|
||||
import { rcon } from "./rcon";
|
||||
|
||||
@@ -100,87 +103,108 @@ async function drain() {
|
||||
}
|
||||
job.state = "running";
|
||||
await saveOwned(job);
|
||||
await withCatalogExport(async () => {
|
||||
await ensureDirectories();
|
||||
const source = job.sourceId ? await getSource(job.sourceId) : null;
|
||||
for (const item of job.items) {
|
||||
await requireLease();
|
||||
if (item.state !== "pending") continue;
|
||||
if (await store.isCancellationRequested(job.id)) {
|
||||
for (const remaining of job.items)
|
||||
if (remaining.state === "pending") remaining.state = "cancelled";
|
||||
break;
|
||||
}
|
||||
item.state = "running";
|
||||
await saveOwned(job);
|
||||
try {
|
||||
if (job.sourceId && !source)
|
||||
throw Error("Import source no longer exists");
|
||||
const providedNitro = item.attachmentId
|
||||
? await readFurnitureAttachment(
|
||||
item.attachmentId,
|
||||
item.classname,
|
||||
job.userId,
|
||||
)
|
||||
: undefined;
|
||||
const operation = createStore("worker" as IpAddress);
|
||||
operation.requestId = (job.operationId ?? job.id) as RequestId;
|
||||
operation.userId = job.userId as UserId;
|
||||
await runWithStore(operation, () =>
|
||||
withCatalogExport(async () => {
|
||||
await ensureDirectories();
|
||||
const source = job.sourceId ? await getSource(job.sourceId) : null;
|
||||
for (const item of job.items) {
|
||||
await requireLease();
|
||||
const result = await (job.mode === "repair"
|
||||
? repairFurniture
|
||||
: importSingleFurni)({
|
||||
...item,
|
||||
repairExisting: true,
|
||||
providedNitro,
|
||||
sourceSwfBaseUrl: source?.sourceSwfBaseUrl,
|
||||
nitroBaseUrl: source?.nitroBaseUrl,
|
||||
iconBaseUrl: source?.iconBaseUrl,
|
||||
});
|
||||
await requireLease();
|
||||
item.warnings = result.warnings;
|
||||
item.itemId = result.itemId;
|
||||
if (!result.ok) throw Error(result.error || "Import failed");
|
||||
item.state = "done";
|
||||
logAudit({
|
||||
userId: job.userId,
|
||||
action: "furni_import",
|
||||
target: "ItemsBase",
|
||||
targetId: result.itemId ?? 0,
|
||||
after: {
|
||||
classname: item.classname,
|
||||
if (item.state !== "pending") continue;
|
||||
if (await store.isCancellationRequested(job.id)) {
|
||||
for (const remaining of job.items)
|
||||
if (remaining.state === "pending")
|
||||
remaining.state = "cancelled";
|
||||
break;
|
||||
}
|
||||
item.state = "running";
|
||||
await saveOwned(job);
|
||||
try {
|
||||
if (job.sourceId && !source)
|
||||
throw Error("Import source no longer exists");
|
||||
const providedNitro = item.attachmentId
|
||||
? await readFurnitureAttachment(
|
||||
item.attachmentId,
|
||||
item.classname,
|
||||
job.userId,
|
||||
)
|
||||
: undefined;
|
||||
await requireLease();
|
||||
const result = job.syncKind
|
||||
? await runSyncJobItem(item)
|
||||
: await (job.mode === "repair"
|
||||
? repairFurniture
|
||||
: importSingleFurni)({
|
||||
...item,
|
||||
repairExisting: true,
|
||||
providedNitro,
|
||||
sourceSwfBaseUrl: source?.sourceSwfBaseUrl,
|
||||
nitroBaseUrl: source?.nitroBaseUrl,
|
||||
iconBaseUrl: source?.iconBaseUrl,
|
||||
});
|
||||
await requireLease();
|
||||
item.warnings = result.warnings;
|
||||
item.itemId = result.itemId;
|
||||
if (!result.ok) throw Error(result.error || "Import failed");
|
||||
item.state = "done";
|
||||
await logAudit({
|
||||
userId: job.userId,
|
||||
action: "furni_import",
|
||||
target: "ItemsBase",
|
||||
targetId: result.itemId ?? 0,
|
||||
after: {
|
||||
classname: item.classname,
|
||||
jobId: job.id,
|
||||
operationId: job.operationId,
|
||||
syncKind: job.syncKind,
|
||||
repairExisting: true,
|
||||
},
|
||||
}).catch((auditError) => {
|
||||
item.warnings ??= [];
|
||||
item.warnings.push("Audit record could not be saved");
|
||||
logServerError("furni.audit_failed", auditError, {
|
||||
jobId: job.id,
|
||||
classname: item.classname,
|
||||
});
|
||||
});
|
||||
if (job.translate && job.mode !== "repair")
|
||||
try {
|
||||
await patchLocalizedFurniDataEntries([item], true, job.langs);
|
||||
} catch {
|
||||
item.warnings.push(
|
||||
"Translation failed; furniture imported successfully",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof ImportLeaseLostError) throw error;
|
||||
await requireLease();
|
||||
logServerError("furni.item_failed", error, {
|
||||
jobId: job.id,
|
||||
repairExisting: true,
|
||||
},
|
||||
});
|
||||
if (job.translate && job.mode !== "repair")
|
||||
try {
|
||||
await patchLocalizedFurniDataEntries([item], true, job.langs);
|
||||
} catch {
|
||||
item.warnings.push(
|
||||
"Translation failed; furniture imported successfully",
|
||||
);
|
||||
}
|
||||
classname: item.classname,
|
||||
});
|
||||
item.state = "failed";
|
||||
item.error =
|
||||
error instanceof Error ? error.message : "Import failed";
|
||||
}
|
||||
await saveOwned(job);
|
||||
}
|
||||
await requireLease();
|
||||
try {
|
||||
await rcon.updateCatalog();
|
||||
await requireLease();
|
||||
await rcon.updateItems();
|
||||
} catch (error) {
|
||||
if (error instanceof ImportLeaseLostError) throw error;
|
||||
await requireLease();
|
||||
item.state = "failed";
|
||||
item.error =
|
||||
error instanceof Error ? error.message : "Import failed";
|
||||
for (const item of job.items)
|
||||
if (item.state === "done") {
|
||||
item.warnings ??= [];
|
||||
item.warnings.push("Game cache refresh failed");
|
||||
}
|
||||
}
|
||||
await saveOwned(job);
|
||||
}
|
||||
await requireLease();
|
||||
try {
|
||||
await rcon.updateCatalog();
|
||||
await requireLease();
|
||||
await rcon.updateItems();
|
||||
} catch (error) {
|
||||
if (error instanceof ImportLeaseLostError) throw error;
|
||||
for (const item of job.items)
|
||||
if (item.state === "done") {
|
||||
item.warnings ??= [];
|
||||
item.warnings.push("Game cache refresh failed");
|
||||
}
|
||||
}
|
||||
});
|
||||
}),
|
||||
);
|
||||
await requireLease();
|
||||
job.state = job.items.some((item) => item.state === "cancelled")
|
||||
? "cancelled"
|
||||
|
||||
Reference in new issue
Block a user