merge: hotfix — FSM snapshot restore (XState v5)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { createActor, type Snapshot } from "xstate";
|
||||
import { createActor } from "xstate";
|
||||
import { ulid } from "ulid";
|
||||
import { pipelineMachine } from "./machine.js";
|
||||
import { createInitialContext, type PipelineContext } from "./context.js";
|
||||
@@ -17,6 +17,13 @@ export function getPrisma(): PrismaClient {
|
||||
return _prisma;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pipeline persistence layout:
|
||||
* - pipelines.currentState — XState state value (for quick queries)
|
||||
* - pipelines.contextJson — FULL persisted snapshot JSON from XState v5
|
||||
* (includes value, context, status, children, etc.)
|
||||
*/
|
||||
|
||||
export async function createPipeline(
|
||||
projectName: string,
|
||||
requirements: string,
|
||||
@@ -25,20 +32,27 @@ export async function createPipeline(
|
||||
const pipelineId = ulid();
|
||||
const ctx = createInitialContext(pipelineId, projectName, requirements);
|
||||
|
||||
const actor = createActor(pipelineMachine, {
|
||||
input: ctx,
|
||||
});
|
||||
const actor = createActor(pipelineMachine, { input: ctx });
|
||||
actor.start();
|
||||
// Manually set pipelineId into context via assign on fresh start is awkward;
|
||||
// just store the ctx alongside the persisted snapshot for later restoration.
|
||||
const persistedSnapshot = actor.getPersistedSnapshot();
|
||||
const snapshot = actor.getSnapshot();
|
||||
actor.stop();
|
||||
|
||||
// Merge our pipelineId into the persisted context for recovery
|
||||
const persistedWithId = mergeContextIntoSnapshot(
|
||||
persistedSnapshot,
|
||||
ctx,
|
||||
);
|
||||
|
||||
await prisma.pipeline.create({
|
||||
data: {
|
||||
id: pipelineId,
|
||||
projectName,
|
||||
requirements,
|
||||
currentState: String(snapshot.value),
|
||||
contextJson: JSON.stringify(ctx),
|
||||
contextJson: JSON.stringify(persistedWithId),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -56,21 +70,22 @@ export async function sendEvent(
|
||||
where: { id: pipelineId },
|
||||
});
|
||||
|
||||
const ctx = JSON.parse(pipeline.contextJson) as PipelineContext;
|
||||
const fromState = pipeline.currentState;
|
||||
const persistedSnapshot = JSON.parse(pipeline.contextJson) as unknown;
|
||||
|
||||
// XState v5 accepts a persisted snapshot via the options object.
|
||||
// We bypass the strict generic typing because the snapshot is produced
|
||||
// by the same machine and serialised through JSON.
|
||||
const actor = createActor(pipelineMachine, {
|
||||
snapshot: {
|
||||
value: fromState,
|
||||
context: ctx,
|
||||
} as unknown as Snapshot<unknown>,
|
||||
});
|
||||
snapshot: persistedSnapshot,
|
||||
} as Parameters<typeof createActor>[1]);
|
||||
actor.start();
|
||||
actor.send(event);
|
||||
|
||||
const snapshot = actor.getSnapshot();
|
||||
const toState = String(snapshot.value);
|
||||
const newContext = snapshot.context as PipelineContext;
|
||||
const newPersistedSnapshot = actor.getPersistedSnapshot();
|
||||
actor.stop();
|
||||
|
||||
await prisma.$transaction([
|
||||
@@ -78,7 +93,7 @@ export async function sendEvent(
|
||||
where: { id: pipelineId },
|
||||
data: {
|
||||
currentState: toState,
|
||||
contextJson: JSON.stringify(newContext),
|
||||
contextJson: JSON.stringify(newPersistedSnapshot),
|
||||
},
|
||||
}),
|
||||
prisma.stateTransition.create({
|
||||
@@ -131,13 +146,32 @@ export async function getPipelineState(
|
||||
|
||||
if (!pipeline) return null;
|
||||
|
||||
const snap = JSON.parse(pipeline.contextJson) as { context?: PipelineContext };
|
||||
const context =
|
||||
(snap.context as PipelineContext | undefined) ??
|
||||
(snap as unknown as PipelineContext);
|
||||
|
||||
return {
|
||||
state: pipeline.currentState as PipelineState,
|
||||
context: JSON.parse(pipeline.contextJson) as PipelineContext,
|
||||
context,
|
||||
transitions: pipeline.transitions,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge our canonical PipelineContext into the XState persisted snapshot.
|
||||
* XState v5 snapshots include `.context`, so we overlay our values.
|
||||
*/
|
||||
function mergeContextIntoSnapshot(
|
||||
snapshot: unknown,
|
||||
ctx: PipelineContext,
|
||||
): unknown {
|
||||
if (snapshot && typeof snapshot === "object") {
|
||||
return { ...(snapshot as object), context: ctx };
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
export async function listPipelines(opts?: {
|
||||
state?: PipelineState;
|
||||
limit?: number;
|
||||
|
||||
Reference in New Issue
Block a user