fix(economy): make catalog translation atomic
This commit is contained in:
1 parent
daf272bad4
commit
3b568e30b4
2 files changed
+472
-50
No files matched your search
+323
@@ -0,0 +1,323 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import type { HousekeepingCapabilityContext } from "../../../foundation/contracts";
|
||||||
|
|
||||||
|
const doubles = vi.hoisted(() => ({
|
||||||
|
events: [] as string[],
|
||||||
|
globalSelect: vi.fn(),
|
||||||
|
globalUpdate: vi.fn(),
|
||||||
|
invalidateFurniDataCache: vi.fn(),
|
||||||
|
patchFurniEntryNames: vi.fn(),
|
||||||
|
rconUpdateCatalog: vi.fn(),
|
||||||
|
transaction: vi.fn(),
|
||||||
|
transactionSelect: vi.fn(),
|
||||||
|
transactionUpdate: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const transactionToken = {
|
||||||
|
select: doubles.transactionSelect,
|
||||||
|
update: doubles.transactionUpdate,
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mock("@/lib/db", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("@/lib/db")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
db: {
|
||||||
|
...actual.db,
|
||||||
|
select: doubles.globalSelect,
|
||||||
|
transaction: doubles.transaction,
|
||||||
|
update: doubles.globalUpdate,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("@/lib/services/catalog-items-loader", () => ({
|
||||||
|
invalidateFurniDataCache: doubles.invalidateFurniDataCache,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/lib/services/furni-data", () => ({
|
||||||
|
patchFurniEntryNames: doubles.patchFurniEntryNames,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/lib/services/rcon", () => ({
|
||||||
|
rcon: { updateCatalog: doubles.rconUpdateCatalog },
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { executeEconomyCatalogMutation } from "./mutation-runtime-catalog";
|
||||||
|
import { EconomyCommittedExternalFailure } from "./mutations-production";
|
||||||
|
|
||||||
|
const capability: HousekeepingCapabilityContext = {
|
||||||
|
actor: { id: 42, username: "operator", rank: 7 },
|
||||||
|
isSuperAdmin: false,
|
||||||
|
has: () => true,
|
||||||
|
hasAny: () => true,
|
||||||
|
hasAll: () => true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
capability,
|
||||||
|
correlationId: "catalog-translation",
|
||||||
|
legacy: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseOne = {
|
||||||
|
id: 1,
|
||||||
|
publicName: "Old chair",
|
||||||
|
itemName: "chair_class",
|
||||||
|
type: "s",
|
||||||
|
spriteId: 10,
|
||||||
|
};
|
||||||
|
const baseTwo = {
|
||||||
|
id: 2,
|
||||||
|
publicName: "Old table",
|
||||||
|
itemName: "table_class",
|
||||||
|
type: "s",
|
||||||
|
spriteId: 20,
|
||||||
|
};
|
||||||
|
|
||||||
|
function selection(rows: readonly unknown[], event: string) {
|
||||||
|
const resolve = () => {
|
||||||
|
doubles.events.push(event);
|
||||||
|
return Promise.resolve(rows);
|
||||||
|
};
|
||||||
|
const thenable = {
|
||||||
|
for: resolve,
|
||||||
|
limit: resolve,
|
||||||
|
};
|
||||||
|
return { from: () => ({ where: () => thenable }) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(event: string) {
|
||||||
|
return {
|
||||||
|
set: () => ({
|
||||||
|
where: async () => {
|
||||||
|
doubles.events.push(event);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
for (const mock of Object.values(doubles)) {
|
||||||
|
if (Array.isArray(mock)) mock.length = 0;
|
||||||
|
else mock.mockReset();
|
||||||
|
}
|
||||||
|
doubles.transaction.mockImplementation(async (run) => {
|
||||||
|
doubles.events.push("transaction:begin");
|
||||||
|
const result = await run(transactionToken);
|
||||||
|
doubles.events.push("transaction:commit");
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
doubles.transactionUpdate.mockImplementation(() =>
|
||||||
|
update("transaction:update"),
|
||||||
|
);
|
||||||
|
doubles.globalUpdate.mockImplementation(() => update("global:update"));
|
||||||
|
doubles.patchFurniEntryNames.mockImplementation(async () => {
|
||||||
|
doubles.events.push("external:furni");
|
||||||
|
return { updated: 2, inserted: 0 };
|
||||||
|
});
|
||||||
|
doubles.invalidateFurniDataCache.mockImplementation(() => {
|
||||||
|
doubles.events.push("external:invalidate");
|
||||||
|
});
|
||||||
|
doubles.rconUpdateCatalog.mockImplementation(async () => {
|
||||||
|
doubles.events.push("external:rcon");
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("catalog item translation atomicity", () => {
|
||||||
|
it("reports every missing or rejected target before issuing any update", async () => {
|
||||||
|
doubles.globalSelect
|
||||||
|
.mockImplementationOnce(() => selection([baseOne], "global:base-1"))
|
||||||
|
.mockImplementationOnce(() => selection([], "global:catalog-1"))
|
||||||
|
.mockImplementationOnce(() => selection([], "global:base-999"));
|
||||||
|
doubles.transactionSelect.mockImplementationOnce(() =>
|
||||||
|
selection([baseOne], "transaction:bases"),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
executeEconomyCatalogMutation(
|
||||||
|
"catalog-item.translate",
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{ id: 1, publicName: "New chair" },
|
||||||
|
{ id: 999, publicName: "Missing" },
|
||||||
|
{ id: "bad", publicName: "Rejected" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
),
|
||||||
|
).rejects.toMatchObject({
|
||||||
|
name: "EconomyMutationFailure",
|
||||||
|
code: "VALIDATION",
|
||||||
|
fieldErrors: {
|
||||||
|
missingIds: ["999"],
|
||||||
|
rejectedIds: ["bad"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(doubles.transaction).toHaveBeenCalledOnce();
|
||||||
|
expect(doubles.transactionUpdate).not.toHaveBeenCalled();
|
||||||
|
expect(doubles.globalUpdate).not.toHaveBeenCalled();
|
||||||
|
expect(doubles.patchFurniEntryNames).not.toHaveBeenCalled();
|
||||||
|
expect(doubles.rconUpdateCatalog).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preloads every target, commits exact before/after changes atomically, then patches external state", async () => {
|
||||||
|
doubles.globalSelect
|
||||||
|
.mockImplementationOnce(() => selection([baseOne], "global:base-1"))
|
||||||
|
.mockImplementationOnce(() =>
|
||||||
|
selection([{ id: 101, catalogName: "Old chair" }], "global:catalog-1"),
|
||||||
|
)
|
||||||
|
.mockImplementationOnce(() => selection([baseTwo], "global:base-2"))
|
||||||
|
.mockImplementationOnce(() =>
|
||||||
|
selection([{ id: 102, catalogName: "Old table" }], "global:catalog-2"),
|
||||||
|
);
|
||||||
|
doubles.transactionSelect
|
||||||
|
.mockImplementationOnce(() =>
|
||||||
|
selection([baseTwo, baseOne], "transaction:bases"),
|
||||||
|
)
|
||||||
|
.mockImplementationOnce(() =>
|
||||||
|
selection(
|
||||||
|
[{ id: 101, catalogName: "Old chair" }],
|
||||||
|
"transaction:catalog-1",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.mockImplementationOnce(() =>
|
||||||
|
selection(
|
||||||
|
[{ id: 102, catalogName: "Old table" }],
|
||||||
|
"transaction:catalog-2",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await executeEconomyCatalogMutation(
|
||||||
|
"catalog-item.translate",
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{ id: 2, publicName: "New table", description: "Table text" },
|
||||||
|
{ id: 1, publicName: "New chair", description: "Chair text" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
before: {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
publicName: "Old chair",
|
||||||
|
catalogItems: [{ id: "101", catalogName: "Old chair" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
publicName: "Old table",
|
||||||
|
catalogItems: [{ id: "102", catalogName: "Old table" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
after: {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
publicName: "New chair",
|
||||||
|
catalogItems: [{ id: "101", catalogName: "New chair" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
publicName: "New table",
|
||||||
|
catalogItems: [{ id: "102", catalogName: "New table" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
namesUpdated: 2,
|
||||||
|
descriptionsUpdated: 2,
|
||||||
|
updatedIds: ["1", "2"],
|
||||||
|
missingIds: [],
|
||||||
|
rejectedIds: [],
|
||||||
|
furniDataUpdated: 2,
|
||||||
|
furniDataInserted: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(doubles.globalSelect).not.toHaveBeenCalled();
|
||||||
|
expect(doubles.globalUpdate).not.toHaveBeenCalled();
|
||||||
|
expect(doubles.transactionUpdate).toHaveBeenCalledTimes(4);
|
||||||
|
expect(doubles.events).toEqual([
|
||||||
|
"transaction:begin",
|
||||||
|
"transaction:bases",
|
||||||
|
"transaction:catalog-1",
|
||||||
|
"transaction:catalog-2",
|
||||||
|
"transaction:update",
|
||||||
|
"transaction:update",
|
||||||
|
"transaction:update",
|
||||||
|
"transaction:update",
|
||||||
|
"transaction:commit",
|
||||||
|
"external:furni",
|
||||||
|
"external:invalidate",
|
||||||
|
"external:rcon",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rolls the batch back and skips external work when a later database update fails", async () => {
|
||||||
|
doubles.transactionSelect
|
||||||
|
.mockImplementationOnce(() =>
|
||||||
|
selection([baseOne, baseTwo], "transaction:bases"),
|
||||||
|
)
|
||||||
|
.mockImplementationOnce(() => selection([], "transaction:catalog-1"))
|
||||||
|
.mockImplementationOnce(() => selection([], "transaction:catalog-2"));
|
||||||
|
let updateCount = 0;
|
||||||
|
doubles.transactionUpdate.mockImplementation(() => ({
|
||||||
|
set: () => ({
|
||||||
|
where: async () => {
|
||||||
|
updateCount += 1;
|
||||||
|
doubles.events.push(`transaction:update-${updateCount}`);
|
||||||
|
if (updateCount === 2) throw new Error("database update failed");
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
executeEconomyCatalogMutation(
|
||||||
|
"catalog-item.translate",
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{ id: 1, publicName: "New chair" },
|
||||||
|
{ id: 2, publicName: "New table" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
),
|
||||||
|
).rejects.toThrow("database update failed");
|
||||||
|
expect(doubles.transaction).toHaveBeenCalledOnce();
|
||||||
|
expect(doubles.events).not.toContain("transaction:commit");
|
||||||
|
expect(doubles.patchFurniEntryNames).not.toHaveBeenCalled();
|
||||||
|
expect(doubles.rconUpdateCatalog).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the committed-external-failure envelope only after the database transaction commits", async () => {
|
||||||
|
doubles.globalSelect
|
||||||
|
.mockImplementationOnce(() => selection([baseOne], "global:base-1"))
|
||||||
|
.mockImplementationOnce(() => selection([], "global:catalog-1"));
|
||||||
|
doubles.transactionSelect
|
||||||
|
.mockImplementationOnce(() => selection([baseOne], "transaction:bases"))
|
||||||
|
.mockImplementationOnce(() => selection([], "transaction:catalog-1"));
|
||||||
|
doubles.patchFurniEntryNames.mockImplementationOnce(async () => {
|
||||||
|
doubles.events.push("external:furni-failed");
|
||||||
|
throw new Error("furni store unavailable");
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
executeEconomyCatalogMutation(
|
||||||
|
"catalog-item.translate",
|
||||||
|
{ items: [{ id: 1, publicName: "New chair" }] },
|
||||||
|
context,
|
||||||
|
),
|
||||||
|
).rejects.toBeInstanceOf(EconomyCommittedExternalFailure);
|
||||||
|
expect(doubles.transaction).toHaveBeenCalledOnce();
|
||||||
|
expect(doubles.events).toContain("transaction:commit");
|
||||||
|
expect(doubles.events.indexOf("transaction:commit")).toBeLessThan(
|
||||||
|
doubles.events.indexOf("external:furni-failed"),
|
||||||
|
);
|
||||||
|
expect(doubles.rconUpdateCatalog).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -799,20 +799,49 @@ async function catalogItemTranslate(
|
|||||||
data.items.length > 500
|
data.items.length > 500
|
||||||
)
|
)
|
||||||
throw validation();
|
throw validation();
|
||||||
const furniPatches: Array<{
|
|
||||||
classname: string;
|
type TranslationRequest = {
|
||||||
itemType: string;
|
readonly id: number;
|
||||||
name?: string;
|
readonly nextName: string;
|
||||||
description?: string;
|
readonly nextDescription: string;
|
||||||
spriteId?: number;
|
};
|
||||||
createIfMissing?: boolean;
|
const requests: TranslationRequest[] = [];
|
||||||
}> = [];
|
const rejectedIds: string[] = [];
|
||||||
let namesUpdated = 0;
|
const requestedIds = new Set<number>();
|
||||||
let descriptionsUpdated = 0;
|
for (const [index, candidate] of data.items.entries()) {
|
||||||
for (const candidate of data.items) {
|
const rejectedLabel =
|
||||||
const item = record(candidate);
|
typeof candidate === "object" &&
|
||||||
const id = positiveInteger(item.id);
|
candidate !== null &&
|
||||||
const [base] = await db
|
!Array.isArray(candidate) &&
|
||||||
|
"id" in candidate
|
||||||
|
? String((candidate as Record<string, unknown>).id).slice(0, 64)
|
||||||
|
: `index:${index}`;
|
||||||
|
try {
|
||||||
|
const item = record(candidate);
|
||||||
|
const id = positiveInteger(item.id);
|
||||||
|
const nextName = text(item.publicName, 56);
|
||||||
|
const nextDescription = text(item.description, 500);
|
||||||
|
if ((!nextName && !nextDescription) || requestedIds.has(id)) {
|
||||||
|
rejectedIds.push(String(id));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
requestedIds.add(id);
|
||||||
|
requests.push({ id, nextName, nextDescription });
|
||||||
|
} catch {
|
||||||
|
rejectedIds.push(rejectedLabel || `index:${index}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requests.sort((left, right) => left.id - right.id);
|
||||||
|
if (requests.length === 0) {
|
||||||
|
throw validation({
|
||||||
|
missingIds: [],
|
||||||
|
rejectedIds: [...new Set(rejectedIds)],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const committed = await db.transaction(async (transaction) => {
|
||||||
|
const connection = transaction as unknown as EconomyDatabase;
|
||||||
|
const bases = await connection
|
||||||
.select({
|
.select({
|
||||||
id: ItemsBase.id,
|
id: ItemsBase.id,
|
||||||
publicName: ItemsBase.publicName,
|
publicName: ItemsBase.publicName,
|
||||||
@@ -821,19 +850,36 @@ async function catalogItemTranslate(
|
|||||||
spriteId: ItemsBase.spriteId,
|
spriteId: ItemsBase.spriteId,
|
||||||
})
|
})
|
||||||
.from(ItemsBase)
|
.from(ItemsBase)
|
||||||
.where(eq(ItemsBase.id, id))
|
.where(
|
||||||
.limit(1);
|
inArray(
|
||||||
if (!base) continue;
|
ItemsBase.id,
|
||||||
const nextName = text(item.publicName, 56);
|
requests.map((request) => request.id),
|
||||||
const nextDescription = text(item.description, 500);
|
),
|
||||||
if (nextName && nextName !== base.publicName) {
|
)
|
||||||
await db
|
.for("update");
|
||||||
.update(ItemsBase)
|
const baseById = new Map(bases.map((base) => [base.id, base]));
|
||||||
.set({ publicName: nextName })
|
const missingIds = requests
|
||||||
.where(eq(ItemsBase.id, id));
|
.filter((request) => !baseById.has(request.id))
|
||||||
const idText = String(id);
|
.map((request) => String(request.id));
|
||||||
const related = await db
|
for (const request of requests) {
|
||||||
.select({ id: CatalogItems.id })
|
const base = baseById.get(request.id);
|
||||||
|
if (base && !base.itemName) rejectedIds.push(String(request.id));
|
||||||
|
}
|
||||||
|
if (missingIds.length > 0 || rejectedIds.length > 0) {
|
||||||
|
throw validation({
|
||||||
|
missingIds,
|
||||||
|
rejectedIds: [...new Set(rejectedIds)],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const relatedByBaseId = new Map<
|
||||||
|
number,
|
||||||
|
Array<{ id: number; catalogName: string }>
|
||||||
|
>();
|
||||||
|
for (const request of requests) {
|
||||||
|
const idText = String(request.id);
|
||||||
|
const related = await connection
|
||||||
|
.select({ id: CatalogItems.id, catalogName: CatalogItems.catalogName })
|
||||||
.from(CatalogItems)
|
.from(CatalogItems)
|
||||||
.where(
|
.where(
|
||||||
or(
|
or(
|
||||||
@@ -842,57 +888,110 @@ async function catalogItemTranslate(
|
|||||||
like(CatalogItems.itemIds, `%;${idText};%`),
|
like(CatalogItems.itemIds, `%;${idText};%`),
|
||||||
like(CatalogItems.itemIds, `%;${idText}`),
|
like(CatalogItems.itemIds, `%;${idText}`),
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
for (const row of related)
|
.for("update");
|
||||||
await db
|
relatedByBaseId.set(request.id, related);
|
||||||
.update(CatalogItems)
|
|
||||||
.set({ catalogName: nextName })
|
|
||||||
.where(eq(CatalogItems.id, row.id));
|
|
||||||
namesUpdated += 1;
|
|
||||||
}
|
}
|
||||||
if (nextDescription || nextName) {
|
|
||||||
if (nextDescription) descriptionsUpdated += 1;
|
const beforeItems: Array<Record<string, unknown>> = [];
|
||||||
|
const afterItems: Array<Record<string, unknown>> = [];
|
||||||
|
const furniPatches: Array<{
|
||||||
|
classname: string;
|
||||||
|
itemType: string;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
spriteId?: number;
|
||||||
|
createIfMissing?: boolean;
|
||||||
|
}> = [];
|
||||||
|
let namesUpdated = 0;
|
||||||
|
let descriptionsUpdated = 0;
|
||||||
|
for (const request of requests) {
|
||||||
|
const base = baseById.get(request.id);
|
||||||
|
if (!base) throw validation({ missingIds: [String(request.id)] });
|
||||||
|
const related = relatedByBaseId.get(request.id) ?? [];
|
||||||
|
const nameChanged =
|
||||||
|
Boolean(request.nextName) && request.nextName !== base.publicName;
|
||||||
|
const nextPublicName = nameChanged ? request.nextName : base.publicName;
|
||||||
|
beforeItems.push({
|
||||||
|
id: String(request.id),
|
||||||
|
publicName: base.publicName,
|
||||||
|
catalogItems: related.map((row) => ({
|
||||||
|
id: String(row.id),
|
||||||
|
catalogName: row.catalogName,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
afterItems.push({
|
||||||
|
id: String(request.id),
|
||||||
|
publicName: nextPublicName,
|
||||||
|
catalogItems: related.map((row) => ({
|
||||||
|
id: String(row.id),
|
||||||
|
catalogName: nameChanged ? request.nextName : row.catalogName,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
if (nameChanged) {
|
||||||
|
await connection
|
||||||
|
.update(ItemsBase)
|
||||||
|
.set({ publicName: request.nextName })
|
||||||
|
.where(eq(ItemsBase.id, request.id));
|
||||||
|
for (const row of related) {
|
||||||
|
await connection
|
||||||
|
.update(CatalogItems)
|
||||||
|
.set({ catalogName: request.nextName })
|
||||||
|
.where(eq(CatalogItems.id, row.id));
|
||||||
|
}
|
||||||
|
namesUpdated += 1;
|
||||||
|
}
|
||||||
|
if (request.nextDescription) descriptionsUpdated += 1;
|
||||||
furniPatches.push({
|
furniPatches.push({
|
||||||
classname: base.itemName,
|
classname: base.itemName,
|
||||||
itemType: base.type || "s",
|
itemType: base.type || "s",
|
||||||
name: nextName || base.publicName || base.itemName,
|
name: request.nextName || base.publicName || base.itemName,
|
||||||
description: nextDescription,
|
description: request.nextDescription,
|
||||||
spriteId: base.spriteId,
|
spriteId: base.spriteId,
|
||||||
createIfMissing: true,
|
createIfMissing: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
const output = {
|
||||||
const snapshot: EconomyMutationSnapshot = {
|
namesUpdated,
|
||||||
before: null,
|
descriptionsUpdated,
|
||||||
after: { namesUpdated, descriptionsUpdated },
|
updatedIds: requests.map((request) => String(request.id)),
|
||||||
output: { namesUpdated, descriptionsUpdated },
|
missingIds: [] as string[],
|
||||||
};
|
rejectedIds: [] as string[],
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
furniPatches,
|
||||||
|
snapshot: {
|
||||||
|
before: { items: beforeItems },
|
||||||
|
after: { items: afterItems },
|
||||||
|
output,
|
||||||
|
} satisfies EconomyMutationSnapshot,
|
||||||
|
};
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
let furniDataUpdated = 0;
|
let furniDataUpdated = 0;
|
||||||
let furniDataInserted = 0;
|
let furniDataInserted = 0;
|
||||||
if (furniPatches.length > 0) {
|
if (committed.furniPatches.length > 0) {
|
||||||
const [{ patchFurniEntryNames }, { invalidateFurniDataCache }] =
|
const [{ patchFurniEntryNames }, { invalidateFurniDataCache }] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
import("@/lib/services/furni-data"),
|
import("@/lib/services/furni-data"),
|
||||||
import("@/lib/services/catalog-items-loader"),
|
import("@/lib/services/catalog-items-loader"),
|
||||||
]);
|
]);
|
||||||
const result = await patchFurniEntryNames(furniPatches);
|
const result = await patchFurniEntryNames(committed.furniPatches);
|
||||||
if (result.updated > 0 || result.inserted > 0) invalidateFurniDataCache();
|
if (result.updated > 0 || result.inserted > 0) invalidateFurniDataCache();
|
||||||
furniDataUpdated = result.updated;
|
furniDataUpdated = result.updated;
|
||||||
furniDataInserted = result.inserted;
|
furniDataInserted = result.inserted;
|
||||||
}
|
}
|
||||||
return synchronizeCatalog({
|
return synchronizeCatalog({
|
||||||
...snapshot,
|
...committed.snapshot,
|
||||||
output: {
|
output: {
|
||||||
namesUpdated,
|
...committed.snapshot.output,
|
||||||
descriptionsUpdated,
|
|
||||||
furniDataUpdated,
|
furniDataUpdated,
|
||||||
furniDataInserted,
|
furniDataInserted,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof EconomyCommittedExternalFailure) throw error;
|
if (error instanceof EconomyCommittedExternalFailure) throw error;
|
||||||
throw new EconomyCommittedExternalFailure(snapshot);
|
throw new EconomyCommittedExternalFailure(committed.snapshot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user