Add persistent history stacks
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
toCircuitDeviceRowPatchValues,
|
||||
type CircuitDeviceRowPatchInput,
|
||||
} from "./circuit-device-row.persistence.js";
|
||||
import { applyProjectHistoryTransition } from "./project-history.persistence.js";
|
||||
import { appendProjectRevision } from "./project-revision.persistence.js";
|
||||
|
||||
type CircuitDeviceRow = typeof circuitDeviceRows.$inferSelect;
|
||||
@@ -117,6 +118,12 @@ export class CircuitDeviceRowProjectCommandRepository
|
||||
forward: appliedForward,
|
||||
inverse,
|
||||
});
|
||||
applyProjectHistoryTransition(tx, {
|
||||
projectId: input.projectId,
|
||||
source: input.source,
|
||||
recordedChangeSetId: revision.changeSetId,
|
||||
targetChangeSetId: input.historyTargetChangeSetId,
|
||||
});
|
||||
|
||||
return { revision, inverse };
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
toCircuitPatchValues,
|
||||
type CircuitPatchPersistenceInput,
|
||||
} from "./circuit.persistence.js";
|
||||
import { applyProjectHistoryTransition } from "./project-history.persistence.js";
|
||||
import { appendProjectRevision } from "./project-revision.persistence.js";
|
||||
|
||||
type CircuitRow = typeof circuits.$inferSelect;
|
||||
@@ -96,6 +97,12 @@ export class CircuitProjectCommandRepository
|
||||
forward: input.command,
|
||||
inverse,
|
||||
});
|
||||
applyProjectHistoryTransition(tx, {
|
||||
projectId: input.projectId,
|
||||
source: input.source,
|
||||
recordedChangeSetId: revision.changeSetId,
|
||||
targetChangeSetId: input.historyTargetChangeSetId,
|
||||
});
|
||||
|
||||
return { revision, inverse };
|
||||
});
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { and, desc, eq, max } from "drizzle-orm";
|
||||
import type { ProjectRevisionSource } from "../../domain/ports/project-revision.store.js";
|
||||
import type { AppDatabase } from "../database-context.js";
|
||||
import { projectHistoryStackEntries } from "../schema/project-history-stack-entries.js";
|
||||
|
||||
export interface ApplyProjectHistoryTransitionInput {
|
||||
projectId: string;
|
||||
source: ProjectRevisionSource;
|
||||
recordedChangeSetId: string;
|
||||
targetChangeSetId?: string;
|
||||
}
|
||||
|
||||
export function applyProjectHistoryTransition(
|
||||
database: AppDatabase,
|
||||
input: ApplyProjectHistoryTransitionInput
|
||||
) {
|
||||
if (input.source === "user") {
|
||||
database
|
||||
.delete(projectHistoryStackEntries)
|
||||
.where(
|
||||
and(
|
||||
eq(projectHistoryStackEntries.projectId, input.projectId),
|
||||
eq(projectHistoryStackEntries.stack, "redo")
|
||||
)
|
||||
)
|
||||
.run();
|
||||
push(
|
||||
database,
|
||||
input.projectId,
|
||||
input.recordedChangeSetId,
|
||||
"undo"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.source === "undo" || input.source === "redo") {
|
||||
if (!input.targetChangeSetId) {
|
||||
throw new Error(
|
||||
`${input.source} requires a target change set.`
|
||||
);
|
||||
}
|
||||
const sourceStack = input.source;
|
||||
const targetStack = input.source === "undo" ? "redo" : "undo";
|
||||
const top = database
|
||||
.select({
|
||||
changeSetId: projectHistoryStackEntries.changeSetId,
|
||||
})
|
||||
.from(projectHistoryStackEntries)
|
||||
.where(
|
||||
and(
|
||||
eq(projectHistoryStackEntries.projectId, input.projectId),
|
||||
eq(projectHistoryStackEntries.stack, sourceStack)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(projectHistoryStackEntries.position))
|
||||
.limit(1)
|
||||
.get();
|
||||
if (!top || top.changeSetId !== input.targetChangeSetId) {
|
||||
throw new Error(
|
||||
`${input.source} target is not the top ${sourceStack} command.`
|
||||
);
|
||||
}
|
||||
|
||||
const nextPosition = getNextPosition(
|
||||
database,
|
||||
input.projectId,
|
||||
targetStack
|
||||
);
|
||||
const moved = database
|
||||
.update(projectHistoryStackEntries)
|
||||
.set({ stack: targetStack, position: nextPosition })
|
||||
.where(
|
||||
and(
|
||||
eq(projectHistoryStackEntries.projectId, input.projectId),
|
||||
eq(
|
||||
projectHistoryStackEntries.changeSetId,
|
||||
input.targetChangeSetId
|
||||
)
|
||||
)
|
||||
)
|
||||
.run();
|
||||
if (moved.changes !== 1) {
|
||||
throw new Error("Project history stack changed during transition.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function push(
|
||||
database: AppDatabase,
|
||||
projectId: string,
|
||||
changeSetId: string,
|
||||
stack: "undo" | "redo"
|
||||
) {
|
||||
database
|
||||
.insert(projectHistoryStackEntries)
|
||||
.values({
|
||||
projectId,
|
||||
changeSetId,
|
||||
stack,
|
||||
position: getNextPosition(database, projectId, stack),
|
||||
})
|
||||
.run();
|
||||
}
|
||||
|
||||
function getNextPosition(
|
||||
database: AppDatabase,
|
||||
projectId: string,
|
||||
stack: "undo" | "redo"
|
||||
) {
|
||||
const current = database
|
||||
.select({ position: max(projectHistoryStackEntries.position) })
|
||||
.from(projectHistoryStackEntries)
|
||||
.where(
|
||||
and(
|
||||
eq(projectHistoryStackEntries.projectId, projectId),
|
||||
eq(projectHistoryStackEntries.stack, stack)
|
||||
)
|
||||
)
|
||||
.get();
|
||||
return (current?.position ?? 0) + 1;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { asc, eq } from "drizzle-orm";
|
||||
import type {
|
||||
ProjectHistoryState,
|
||||
ProjectHistoryStore,
|
||||
} from "../../domain/ports/project-history.store.js";
|
||||
import type { AppDatabase } from "../database-context.js";
|
||||
import { projectHistoryStackEntries } from "../schema/project-history-stack-entries.js";
|
||||
import { projects } from "../schema/projects.js";
|
||||
|
||||
export class ProjectHistoryRepository implements ProjectHistoryStore {
|
||||
constructor(private readonly database: AppDatabase) {}
|
||||
|
||||
getState(projectId: string): ProjectHistoryState | null {
|
||||
const project = this.database
|
||||
.select({ currentRevision: projects.currentRevision })
|
||||
.from(projects)
|
||||
.where(eq(projects.id, projectId))
|
||||
.get();
|
||||
if (!project) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const entries = this.database
|
||||
.select()
|
||||
.from(projectHistoryStackEntries)
|
||||
.where(eq(projectHistoryStackEntries.projectId, projectId))
|
||||
.orderBy(
|
||||
asc(projectHistoryStackEntries.stack),
|
||||
asc(projectHistoryStackEntries.position)
|
||||
)
|
||||
.all();
|
||||
const undoEntries = entries.filter((entry) => entry.stack === "undo");
|
||||
const redoEntries = entries.filter((entry) => entry.stack === "redo");
|
||||
|
||||
return {
|
||||
projectId,
|
||||
currentRevision: project.currentRevision,
|
||||
undoDepth: undoEntries.length,
|
||||
redoDepth: redoEntries.length,
|
||||
undoChangeSetId: undoEntries.at(-1)?.changeSetId ?? null,
|
||||
redoChangeSetId: redoEntries.at(-1)?.changeSetId ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user