Harden Linear XLSX end to end

This commit is contained in:
Julian Appel 2026-09-30 21:02:20 +02:00
parent 2581beee3c
commit 4ff01b2ccd
26 changed files with 522 additions and 246 deletions

View file

@ -0,0 +1,20 @@
import assert from "node:assert/strict";
import path from "node:path";
import { describe, it } from "node:test";
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
import { createDatabaseContext } from "../src/db/database-context.js";
import { insertManyInBatches } from "../src/db/repositories/batched-insert.persistence.js";
import { projects } from "../src/db/schema/projects.js";
describe("batched SQLite inserts", () => {
it("keeps every batch inside the caller transaction", () => {
const context = createDatabaseContext(":memory:");
try {
migrate(context.db, { migrationsFolder: path.resolve("src", "db", "migrations") });
const values = Array.from({ length: 401 }, (_, index) => ({ id: `project-${index}`, name: `Projekt ${index}` }));
values[400] = { id: "project-0", name: "Duplikat" };
assert.throws(() => context.db.transaction((tx) => insertManyInBatches(tx, projects, values)));
assert.equal(context.db.select().from(projects).all().length, 0);
} finally { context.close(); }
});
});

View file

@ -122,6 +122,32 @@ export async function createRevitXlsxFixture(options?: {
return new Uint8Array(await workbook.xlsx.writeBuffer());
}
export async function createLargeRevitXlsxFixture(objectCount: number) {
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("Linear Export");
worksheet.addRow([
"Worksharing-ID", "CAx_Auswahlkenner", "MEP-Raum: Nummer", "MEP-Raum: Name",
"Familie und Typ", "CAX_AnzahlObjekte", "CAx_Anmerkung", "kbp_Stromkreisnummer",
"kbp-E-Verteiler", "kbp-E-Stromkreis", "kbp-E-Bezeichnung",
"kbp-E-Elektrische Leistung", "kbp-E-Spannung", "kbp-E-Stromstärke", "IfcGUID",
]);
worksheet.addRow([]);
for (let index = 0; index < objectCount; index += 1) {
const suffix = String(index + 1).padStart(8, "0");
worksheet.addRow([
`AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE-${suffix}`,
`Steckdose ${index + 1}`,
String(100 + (index % 250)),
`Raum ${index % 250}`,
"Steckdose: Standard",
(index % 4) + 1,
null, null, null, null, null, null, null, null,
`IFC-${suffix}`,
]);
}
return new Uint8Array(await workbook.xlsx.writeBuffer());
}
function setRowValues(
worksheet: ExcelJS.Worksheet,
rowNumber: number,

View file

@ -0,0 +1,143 @@
import assert from "node:assert/strict";
import { existsSync, unlinkSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { randomUUID } from "node:crypto";
import { describe, it } from "node:test";
import ExcelJS from "exceljs";
import { eq } from "drizzle-orm";
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
import { createDatabaseContext } from "../src/db/database-context.js";
import { ExternalFollowUpImportProjectCommandRepository } from "../src/db/repositories/external-follow-up-import-project-command.repository.js";
import { ExternalInitialImportProjectCommandRepository } from "../src/db/repositories/external-initial-import-project-command.repository.js";
import { ExternalModelStateRepository } from "../src/db/repositories/external-model-state.repository.js";
import { ExternalObjectPlanningProjectCommandRepository } from "../src/db/repositories/external-object-planning-project-command.repository.js";
import { ExternalObjectRowAssignmentProjectCommandRepository } from "../src/db/repositories/external-object-row-assignment-project-command.repository.js";
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
import { ProjectSnapshotRepository } from "../src/db/repositories/project-snapshot.repository.js";
import { ProjectStateRestoreCommandRepository } from "../src/db/repositories/project-state-restore-command.repository.js";
import { ProjectTransferRepository } from "../src/db/repositories/project-transfer.repository.js";
import { readProjectStateSnapshot } from "../src/db/repositories/project-state-snapshot.persistence.js";
import { toCircuitDeviceRowSnapshot } from "../src/db/repositories/circuit-device-row-structure.persistence.js";
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
import { circuitLists } from "../src/db/schema/circuit-lists.js";
import { circuitProtectionDevices } from "../src/db/schema/circuit-protection-devices.js";
import { circuitSections } from "../src/db/schema/circuit-sections.js";
import { circuits } from "../src/db/schema/circuits.js";
import { distributionBoards } from "../src/db/schema/distribution-boards.js";
import { externalModelConfigurations } from "../src/db/schema/external-model-configurations.js";
import { projects } from "../src/db/schema/projects.js";
import { createExternalFollowUpImportProjectCommand } from "../src/domain/models/external-follow-up-import-project-command.model.js";
import { createEmptyExternalModelState, createExternalInitialImportProjectCommand } from "../src/domain/models/external-initial-import-project-command.model.js";
import { createExternalObjectPlanningUpdateProjectCommand } from "../src/domain/models/external-object-planning-project-command.model.js";
import { createExternalObjectRowAssignmentProjectCommand } from "../src/domain/models/external-object-row-assignment-project-command.model.js";
import { assertProjectStateRestoreCommand } from "../src/domain/models/project-state-restore-command.model.js";
import { createExternalXlsxFollowUpImportPlan } from "../src/external-model/application/external-xlsx-follow-up-import.js";
import { buildExternalXlsxFollowUpTarget } from "../src/external-model/application/external-xlsx-follow-up-target.js";
import { buildExternalXlsxInitialImportTarget } from "../src/external-model/application/external-xlsx-initial-import-target.js";
import { createExternalXlsxInitialImportPlan } from "../src/external-model/application/external-xlsx-initial-import.js";
import { projectExternalXlsxReturn } from "../src/external-model/application/external-xlsx-return-projection.js";
import { createDefaultExternalModelConfiguration } from "../src/external-model/configuration/external-model-configuration.js";
import { createExternalXlsxReturn } from "../src/external-model/xlsx/external-xlsx-transport.js";
import { createRevitXlsxFixture, externalXlsxTestConfiguration, firstWorksharingId, secondWorksharingId } from "./fixtures/revit-xlsx-fixtures.js";
const projectId = "project-e2e";
const boardId = "board-e2e";
const listId = "list-e2e";
const sectionId = "section-e2e";
const circuitId = "circuit-e2e";
const rowId = "row-e2e";
const deviceTypeA = "10000000-0000-4000-8000-000000000001";
const deviceTypeB = "10000000-0000-4000-8000-000000000002";
const configuration = { ...createDefaultExternalModelConfiguration(), ...externalXlsxTestConfiguration, deviceTypes: [{ id: deviceTypeA, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: 125, simultaneityFactor: 1, cosPhi: 0.9, costGroup: "440" } }, { id: deviceTypeB, name: "Leuchte", category: "lighting" as const, defaults: { powerPerUnitW: 40, simultaneityFactor: 0.8, cosPhi: null, costGroup: null } }], familyTypeRules: [{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId: deviceTypeA, quantityRule: "socketCount" as const }, { id: "20000000-0000-4000-8000-000000000002", exactFamilyAndType: "Leuchte: Decke", deviceTypeId: deviceTypeB, quantityRule: "one" as const }] };
describe("Linear XLSX end-to-end", () => {
it("survives return, follow-up, snapshot restore, duplicate and restart undo/redo", async () => {
const databasePath = path.join(tmpdir(), `leistungsbilanz-r10-${randomUUID()}.sqlite`);
let context = createDatabaseContext(databasePath);
try {
migrate(context.db, { migrationsFolder: path.resolve("src", "db", "migrations") });
seedProject(context.db);
const configurationSnapshot = { id: "config-e2e", projectId, configuration };
context.db.insert(externalModelConfigurations).values(configurationSnapshot).run();
const initialBytes = await createRevitXlsxFixture();
const initial = await createExternalXlsxInitialImportPlan({ fileName: "initial.xlsx", bytes: initialBytes, configuration, catalogs: emptyCatalogs() });
let sequence = 0;
const initialState = buildExternalXlsxInitialImportTarget({ projectId, expectedRevision: 0, sourceName: "Revit", importedAtIso: "2026-09-30T08:00:00.000Z", configuration, originalContentBase64: Buffer.from(initialBytes).toString("base64"), document: initial.document, plan: initial.plan, roomDecisions: initial.plan.sourceRooms.map((room) => ({ sourceRoomKey: room.sourceRoomKey, roomId: null, defaultDistributionBoardId: boardId })), familyProjectDeviceDecisions: initial.plan.familyGroups.map((family) => ({ familyAndType: family.familyAndType, projectDeviceId: null })), objectsWithoutRoomDefaultDistributionBoardId: boardId, createId: () => nextId("3", ++sequence) });
new ExternalInitialImportProjectCommandRepository(context.db).execute({ projectId, expectedRevision: 0, source: "user", command: createExternalInitialImportProjectCommand(configurationSnapshot, configurationSnapshot, createEmptyExternalModelState(), initialState) });
let state = new ExternalModelStateRepository(context.db).getByProject(projectId).state;
const first = state.objects.find((object) => object.worksharingId === firstWorksharingId)!;
const plannedFirst = { ...first, planningValues: { ...first.planningValues, remark: "Geprüft" }, overriddenFields: [...first.overriddenFields, "remark" as const] };
new ExternalObjectPlanningProjectCommandRepository(context.db).execute({ projectId, expectedRevision: 1, source: "user", command: createExternalObjectPlanningUpdateProjectCommand([{ expected: first, target: plannedFirst }]) });
const rowBeforeAssignment = currentRow(context.db);
const assignedFirst = { ...plannedFirst, distributionBoardId: boardId, circuitDeviceRowId: rowId };
new ExternalObjectRowAssignmentProjectCommandRepository(context.db).execute({ projectId, expectedRevision: 2, source: "user", command: createExternalObjectRowAssignmentProjectCommand({ rows: [{ expected: rowBeforeAssignment, target: { ...rowBeforeAssignment, quantity: 3 } }], objects: [{ expected: plannedFirst, target: assignedFirst }], confirmedConflictObjectIds: [first.id] }) });
state = new ExternalModelStateRepository(context.db).getByProject(projectId).state;
const projected = state.objects.map((object) => projectExternalXlsxReturn({ object, row: object.circuitDeviceRowId ? { rowId, displayName: "Steckdose", powerPerUnitKw: 0.125, cosPhi: 0.9, phaseType: "single_phase", category: "single_phase", circuitIdentifier: "-2F1.1", circuitVoltageV: 230, distributionBoardName: "UV 1" } : null, circuitMode: configuration.circuitMode, singlePhaseVoltageV: 230, threePhaseVoltageV: 400 }));
const latestBatch = state.importBatches.at(-1)!;
const returned = await createExternalXlsxReturn({ fileName: latestBatch.fileName, bytes: Buffer.from(latestBatch.originalContentBase64!, "base64"), configuration, selectedWorksharingIds: projected.map((entry) => entry.worksharingId), updates: projected.map((entry) => ({ worksharingId: entry.worksharingId, values: entry.values })) });
const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(returned.bytes); workbook.worksheets[0]!.getCell(3, 6).value = 3;
const followBytes = new Uint8Array(await workbook.xlsx.writeBuffer());
const returnValues = new Map(projected.map((entry) => [entry.objectId, entry.values]));
const follow = await createExternalXlsxFollowUpImportPlan({ fileName: "follow.xlsx", bytes: followBytes, configuration, catalogs: emptyCatalogs(), currentState: state, currentReturnValuesByObjectId: returnValues });
const built = buildExternalXlsxFollowUpTarget({ projectId, expectedRevision: 3, importedAtIso: "2026-09-30T09:00:00.000Z", configuration, originalContentBase64: Buffer.from(followBytes).toString("base64"), document: follow.document, plan: follow.plan, currentState: state, sourceChangeDecisions: follow.plan.changes.map((change) => ({ objectId: change.objectId, accept: true })), roomDecisions: follow.plan.newSourceRoomKeys.map((sourceRoomKey) => ({ sourceRoomKey, roomId: null, defaultDistributionBoardId: boardId })), familyProjectDeviceDecisions: follow.plan.newFamilyAndTypes.map((familyAndType) => ({ familyAndType, projectDeviceId: null })), objectsWithoutRoomDefaultDistributionBoardId: boardId, rowsById: new Map([[rowId, currentRow(context.db)]]), createId: () => nextId("4", ++sequence) });
new ExternalFollowUpImportProjectCommandRepository(context.db).execute({ projectId, expectedRevision: 3, source: "user", command: createExternalFollowUpImportProjectCommand({ expectedConfiguration: configurationSnapshot, targetConfiguration: configurationSnapshot, expectedState: state, targetState: built.targetState, rows: built.rowTransitions }) });
assert.equal(currentRow(context.db).quantity, 4);
state = new ExternalModelStateRepository(context.db).getByProject(projectId).state;
assert.equal(state.importBatches.length, 2);
assert.equal(state.importBatches[0]!.originalContentBase64, null);
const snapshot = new ProjectSnapshotRepository(context.db).createNamed({ projectId, expectedRevision: 4, name: "Nach Folgeimport" });
assert.ok(snapshot);
const second = state.objects.find((object) => object.worksharingId === secondWorksharingId)!;
const changedSecond = { ...second, planningValues: { ...second.planningValues, simultaneityFactor: 0.55 }, overriddenFields: [...second.overriddenFields, "simultaneityFactor" as const] };
new ExternalObjectPlanningProjectCommandRepository(context.db).execute({ projectId, expectedRevision: 4, source: "user", command: createExternalObjectPlanningUpdateProjectCommand([{ expected: second, target: changedSecond }]) });
const prepared = new ProjectSnapshotRepository(context.db).prepareRestore(projectId, snapshot.id);
assert.ok(prepared);
new ProjectStateRestoreCommandRepository(context.db).execute({ projectId, expectedRevision: 5, source: "restore", command: prepared.command });
const transferRepository = new ProjectTransferRepository(context.db);
const transfer = transferRepository.exportProject(projectId);
assert.ok(transfer);
const duplicate = transferRepository.importDuplicate(transfer);
const duplicateState = readProjectStateSnapshot(context.db, duplicate.projectId)!.state;
const restoredState = readProjectStateSnapshot(context.db, projectId)!.state;
assert.deepEqual(duplicateState.externalModel.objects.map((object) => object.worksharingId).sort(), restoredState.externalModel.objects.map((object) => object.worksharingId).sort());
assert.notEqual(duplicateState.externalModel.objects[0]!.id, restoredState.externalModel.objects[0]!.id);
assert.equal(duplicateState.externalModel.importBatches.at(-1)!.configurationSnapshot!.deviceTypes[0]!.id, duplicateState.externalModelConfiguration!.configuration.deviceTypes[0]!.id);
context.close();
context = createDatabaseContext(databasePath);
const history = new ProjectHistoryRepository(context.db);
const undo = history.getNextCommand(projectId, "undo");
assert.ok(undo); assertProjectStateRestoreCommand(undo.command);
new ProjectStateRestoreCommandRepository(context.db).execute({ projectId, expectedRevision: 6, source: "undo", historyTargetChangeSetId: undo.changeSetId, command: undo.command });
assert.equal(new ExternalModelStateRepository(context.db).getByProject(projectId).state.objects.find((object) => object.worksharingId === secondWorksharingId)!.planningValues.simultaneityFactor, 0.55);
const redo = history.getNextCommand(projectId, "redo");
assert.ok(redo); assertProjectStateRestoreCommand(redo.command);
new ProjectStateRestoreCommandRepository(context.db).execute({ projectId, expectedRevision: 7, source: "redo", historyTargetChangeSetId: redo.changeSetId, command: redo.command });
assert.equal(new ExternalModelStateRepository(context.db).getByProject(projectId).state.objects.find((object) => object.worksharingId === secondWorksharingId)!.planningValues.simultaneityFactor, 0.8);
assert.equal(currentRow(context.db).quantity, 4);
} finally {
context.close();
for (const file of [databasePath, `${databasePath}-wal`, `${databasePath}-shm`]) if (existsSync(file)) unlinkSync(file);
}
});
});
function seedProject(database: ReturnType<typeof createDatabaseContext>["db"]) {
database.insert(projects).values({ id: projectId, name: "R10 Projekt" }).run();
database.insert(distributionBoards).values({ id: boardId, projectId, name: "UV 1" }).run();
database.insert(circuitLists).values({ id: listId, projectId, distributionBoardId: boardId, name: "Liste" }).run();
database.insert(circuitSections).values({ id: sectionId, circuitListId: listId, key: "single-1", displayName: "1-phasig", prefix: "-2F1.", sortOrder: 10, category: "single_phase", groupNumber: 1 }).run();
database.insert(circuits).values({ id: circuitId, circuitListId: listId, sectionId, equipmentIdentifier: "-2F1.1", sortOrder: 10, voltage: 230 }).run();
database.insert(circuitProtectionDevices).values({ circuitId, type: "LS", ratedCurrentA: 16, fuseUtilizationCategory: null, tripCharacteristic: "B", rcdType: null, ratedResidualCurrentMa: null }).run();
database.insert(circuitDeviceRows).values({ id: rowId, circuitId, sortOrder: 10, name: "Steckdose", displayName: "Steckdose", phaseType: "single_phase", category: "single_phase", quantity: 1, manualQuantity: 1, powerPerUnit: 0.125, simultaneityFactor: 1, cosPhi: 0.9 }).run();
}
function currentRow(database: ReturnType<typeof createDatabaseContext>["db"]) { return toCircuitDeviceRowSnapshot(database.select().from(circuitDeviceRows).where(eq(circuitDeviceRows.id, rowId)).get()!); }
function emptyCatalogs() { return { floors: [], rooms: [], distributionBoards: [{ id: boardId, name: "UV 1" }], projectDevices: [] }; }
function nextId(prefix: string, sequence: number) { return `${prefix}0000000-0000-4000-8000-${String(sequence).padStart(12, "0")}`; }

View file

@ -0,0 +1,112 @@
import assert from "node:assert/strict";
import path from "node:path";
import { performance } from "node:perf_hooks";
import { describe, it } from "node:test";
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
import { createDatabaseContext } from "../src/db/database-context.js";
import { ExternalInitialImportProjectCommandRepository } from "../src/db/repositories/external-initial-import-project-command.repository.js";
import { ExternalFollowUpImportProjectCommandRepository } from "../src/db/repositories/external-follow-up-import-project-command.repository.js";
import { ExternalModelStateRepository } from "../src/db/repositories/external-model-state.repository.js";
import { ProjectSnapshotRepository } from "../src/db/repositories/project-snapshot.repository.js";
import { ProjectStateRestoreCommandRepository } from "../src/db/repositories/project-state-restore-command.repository.js";
import { ProjectTransferRepository } from "../src/db/repositories/project-transfer.repository.js";
import { readProjectStateSnapshot } from "../src/db/repositories/project-state-snapshot.persistence.js";
import { externalModelConfigurations } from "../src/db/schema/external-model-configurations.js";
import { projects } from "../src/db/schema/projects.js";
import { createEmptyExternalModelState, createExternalInitialImportProjectCommand } from "../src/domain/models/external-initial-import-project-command.model.js";
import { createExternalFollowUpImportProjectCommand } from "../src/domain/models/external-follow-up-import-project-command.model.js";
import { createExternalXlsxFollowUpImportPlan } from "../src/external-model/application/external-xlsx-follow-up-import.js";
import { buildExternalXlsxFollowUpTarget } from "../src/external-model/application/external-xlsx-follow-up-target.js";
import { buildExternalXlsxInitialImportTarget } from "../src/external-model/application/external-xlsx-initial-import-target.js";
import { createExternalXlsxInitialImportPlan } from "../src/external-model/application/external-xlsx-initial-import.js";
import { createDefaultExternalModelConfiguration } from "../src/external-model/configuration/external-model-configuration.js";
import { createVirtualRevitObjectWindow, filterAndSortRevitObjects } from "../src/frontend/components/revit-object-table-model.js";
import type { ExternalProjectObjectDto } from "../src/frontend/types.js";
import { createLargeRevitXlsxFixture, externalXlsxTestConfiguration } from "./fixtures/revit-xlsx-fixtures.js";
const objectCount = 5_000;
const deviceTypeId = "10000000-0000-4000-8000-000000000001";
const configuration = {
...createDefaultExternalModelConfiguration(),
...externalXlsxTestConfiguration,
deviceTypes: [{ id: deviceTypeId, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: 125, simultaneityFactor: 1, cosPhi: 0.9, costGroup: "440" } }],
familyTypeRules: [{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId, quantityRule: "socketCount" as const }],
};
describe("Linear XLSX scale", () => {
it("imports, persists, filters and virtualizes 5,000 synthetic objects within bounded resources", async () => {
const heapBefore = process.memoryUsage().heapUsed;
const bytes = await createLargeRevitXlsxFixture(objectCount);
const parseStarted = performance.now();
const { plan, document } = await createExternalXlsxInitialImportPlan({ fileName: "large.xlsx", bytes, configuration, catalogs: { floors: [], rooms: [], distributionBoards: [], projectDevices: [] } });
const parseMs = performance.now() - parseStarted;
assert.equal(plan.objectCount, objectCount);
assert.ok(bytes.byteLength < 10 * 1024 * 1024);
let sequence = 0;
const target = buildExternalXlsxInitialImportTarget({ projectId: "project-scale", expectedRevision: 0, sourceName: "Revit", importedAtIso: "2026-09-30T12:00:00.000Z", configuration, originalContentBase64: Buffer.from(bytes).toString("base64"), document, plan, roomDecisions: plan.sourceRooms.map((room) => ({ sourceRoomKey: room.sourceRoomKey, roomId: null, defaultDistributionBoardId: null })), familyProjectDeviceDecisions: plan.familyGroups.map((family) => ({ familyAndType: family.familyAndType, projectDeviceId: null })), objectsWithoutRoomDefaultDistributionBoardId: null, createId: () => `30000000-0000-4000-8000-${String(++sequence).padStart(12, "0")}` });
const configurationSnapshot = { id: "config-scale", projectId: "project-scale", configuration };
const command = createExternalInitialImportProjectCommand(configurationSnapshot, configurationSnapshot, createEmptyExternalModelState(), target);
const commandBytes = Buffer.byteLength(JSON.stringify(command));
const context = createDatabaseContext(":memory:");
try {
migrate(context.db, { migrationsFolder: path.resolve("src", "db", "migrations") });
context.db.insert(projects).values({ id: "project-scale", name: "Lasttest" }).run();
context.db.insert(externalModelConfigurations).values(configurationSnapshot).run();
const transactionStarted = performance.now();
new ExternalInitialImportProjectCommandRepository(context.db).execute({ projectId: "project-scale", expectedRevision: 0, source: "user", command });
const transactionMs = performance.now() - transactionStarted;
let snapshot = readProjectStateSnapshot(context.db, "project-scale");
assert.ok(snapshot);
assert.equal(snapshot.state.externalModel.objects.length, objectCount);
const currentState = new ExternalModelStateRepository(context.db).getByProject("project-scale").state;
const returnValues = new Map(currentState.objects.map((object) => [object.id, { circuitCombined: object.acceptedSourceValues.circuitCombined, boardName: object.acceptedSourceValues.boardName, circuitIdentifier: object.acceptedSourceValues.circuitIdentifier, designation: object.acceptedSourceValues.designation, electricalPowerW: object.acceptedSourceValues.electricalPowerW, voltageV: object.acceptedSourceValues.voltageV, currentA: object.acceptedSourceValues.currentA }]));
const followParseStarted = performance.now();
const follow = await createExternalXlsxFollowUpImportPlan({ fileName: "large-follow.xlsx", bytes, configuration, catalogs: { floors: [], rooms: [], distributionBoards: [], projectDevices: [] }, currentState, currentReturnValuesByObjectId: returnValues });
const followParseMs = performance.now() - followParseStarted;
const followTarget = buildExternalXlsxFollowUpTarget({ projectId: "project-scale", expectedRevision: 1, importedAtIso: "2026-09-30T13:00:00.000Z", configuration, originalContentBase64: Buffer.from(bytes).toString("base64"), document: follow.document, plan: follow.plan, currentState, sourceChangeDecisions: [], roomDecisions: [], familyProjectDeviceDecisions: [], objectsWithoutRoomDefaultDistributionBoardId: null, rowsById: new Map(), createId: () => `40000000-0000-4000-8000-${String(++sequence).padStart(12, "0")}` });
const followCommand = createExternalFollowUpImportProjectCommand({ expectedConfiguration: configurationSnapshot, targetConfiguration: configurationSnapshot, expectedState: currentState, targetState: followTarget.targetState, rows: followTarget.rowTransitions });
const followTransactionStarted = performance.now();
new ExternalFollowUpImportProjectCommandRepository(context.db).execute({ projectId: "project-scale", expectedRevision: 1, source: "user", command: followCommand });
const followTransactionMs = performance.now() - followTransactionStarted;
const followedState = new ExternalModelStateRepository(context.db).getByProject("project-scale").state;
assert.equal(followedState.importBatches.length, 2);
assert.equal(followedState.importBatches[0]!.document, null);
assert.equal(followedState.objects.length, objectCount);
const namedSnapshot = new ProjectSnapshotRepository(context.db).createNamed({ projectId: "project-scale", expectedRevision: 2, name: "Lasttest" });
assert.ok(namedSnapshot);
const preparedRestore = new ProjectSnapshotRepository(context.db).prepareRestore("project-scale", namedSnapshot.id);
assert.ok(preparedRestore);
new ProjectStateRestoreCommandRepository(context.db).execute({ projectId: "project-scale", expectedRevision: 2, source: "restore", command: preparedRestore.command });
const transferRepository = new ProjectTransferRepository(context.db);
const transfer = transferRepository.exportProject("project-scale");
assert.ok(transfer);
const duplicate = transferRepository.importDuplicate(transfer);
assert.equal(readProjectStateSnapshot(context.db, duplicate.projectId)!.state.externalModel.objects.length, objectCount);
snapshot = readProjectStateSnapshot(context.db, "project-scale");
assert.ok(snapshot);
const clientRows: ExternalProjectObjectDto[] = target.objects.map((object) => ({ id: object.id, worksharingId: object.worksharingId, ifcGuid: object.ifcGuid, deviceTypeId: object.deviceTypeId, familyAndType: object.acceptedSourceValues.familyAndType, selectionMarker: String(object.acceptedSourceValues.selectionMarker ?? ""), sourceCircuitIdentifier: "", sourceRoomNumber: String(object.acceptedSourceValues.roomNumber ?? ""), sourceRoomName: String(object.acceptedSourceValues.roomName ?? ""), roomId: null, distributionBoardId: object.distributionBoardId, linkedProjectDeviceId: object.linkedProjectDeviceId, circuitDeviceRowId: object.circuitDeviceRowId, presenceStatus: object.presenceStatus, planningValues: object.planningValues }));
const clientStarted = performance.now();
const filtered = filterAndSortRevitObjects(clientRows, { search: "Raum 42", boardFilter: "all", assignmentFilter: "all", sort: "worksharing" });
const window = createVirtualRevitObjectWindow(filtered, 4_400, 44, 520);
const clientMs = performance.now() - clientStarted;
assert.ok(filtered.length > 0);
assert.ok(window.rows.length <= 22);
const heapDeltaBytes = Math.max(0, process.memoryUsage().heapUsed - heapBefore);
const snapshotBytes = Buffer.byteLength(snapshot.payloadJson);
assert.ok(parseMs < 15_000, `XLSX parse took ${parseMs.toFixed(0)} ms`);
assert.ok(transactionMs < 15_000, `Import transaction took ${transactionMs.toFixed(0)} ms`);
assert.ok(followParseMs < 15_000, `Follow-up parse took ${followParseMs.toFixed(0)} ms`);
assert.ok(followTransactionMs < 15_000, `Follow-up transaction took ${followTransactionMs.toFixed(0)} ms`);
assert.ok(clientMs < 1_000, `Client projection took ${clientMs.toFixed(0)} ms`);
assert.ok(heapDeltaBytes < 768 * 1024 * 1024, `Heap grew by ${heapDeltaBytes} bytes`);
assert.ok(commandBytes < 100 * 1024 * 1024);
console.info("R10 scale metrics", { objectCount, workbookBytes: bytes.byteLength, commandBytes, snapshotBytes, parseMs: Math.round(parseMs), transactionMs: Math.round(transactionMs), followParseMs: Math.round(followParseMs), followTransactionMs: Math.round(followTransactionMs), clientMs: Math.round(clientMs), heapDeltaBytes });
} finally { context.close(); }
});
});