EmboDi / src /server /scene-store.mjs
xlt99's picture
Upload 180 files
970fc3e verified
Raw History Blame Contribute Delete
3.57 kB
import { seedScene, validateScene } from "../shared/scene.mjs";
import { readFile, writeFile, rename } from "node:fs/promises";
import { validateCapturePlan } from "../shared/capture-boundary.mjs";
import { withKnownAssemblies } from "../shared/transforms.mjs";
import { reconcileBehaviors } from "../shared/behaviors.mjs";
async function createSceneStore(appRuntime) {
let state = { revision: 0, scene: seedScene(), source: "sample", updatedAt: (/* @__PURE__ */ new Date()).toISOString(), savedAt: null, saveMode: null };
try {
const saved = JSON.parse(await readFile(appRuntime.sceneLocation.file, "utf8"));
validateScene(saved.scene);
if (saved.capturePlan) validateCapturePlan(saved.capturePlan);
state = { ...saved, savedAt: saved.savedAt || saved.updatedAt, saveMode: saved.saveMode || "auto" };
} catch (error) {
if (appRuntime.sceneLocation.id) throw error;
if (error.code !== "ENOENT") console.warn("Saved scene invalid; using sample:", error.message);
}
const history = [];
const clients = /* @__PURE__ */ new Set();
const assembled = withKnownAssemblies(state.scene);
if (JSON.stringify(assembled) !== JSON.stringify(state.scene)) {
state = { ...state, scene: assembled, revision: state.revision + 1 };
await persistScene(state);
}
let mutationBusy = false;
const broadcast = () => {
for (const res of clients) res.write(`data: ${JSON.stringify(state)}
`);
};
async function persistScene(next) {
await writeFile(appRuntime.sceneLocation.temporary, JSON.stringify(next, null, 2));
await rename(appRuntime.sceneLocation.temporary, appRuntime.sceneLocation.file);
}
async function commit(scene, source, recordHistory = true, metadata = state, beforePersist, productionReady = false) {
if (mutationBusy) throw new Error("Scene is saving. Try again shortly.");
mutationBusy = true;
try {
scene = reconcileBehaviors(scene);
if ("productionReady" in scene) scene = { ...scene, productionReady };
validateScene(scene);
const now = (/* @__PURE__ */ new Date()).toISOString(), next = { ...metadata, revision: state.revision + 1, scene, source, updatedAt: now, savedAt: now, saveMode: "auto" };
if (beforePersist) await beforePersist();
await persistScene(next);
if (recordHistory) {
history.push(structuredClone(state));
if (history.length > 30) history.shift();
}
state = next;
broadcast();
return state;
} finally {
mutationBusy = false;
}
}
async function saveCurrentScene(revision) {
revisionCheck(revision);
if (mutationBusy) throw new Error("Scene is saving. Try again shortly.");
mutationBusy = true;
try {
const next = { ...state, savedAt: (/* @__PURE__ */ new Date()).toISOString(), saveMode: "manual" };
await persistScene(next);
state = next;
broadcast();
return state;
} finally {
mutationBusy = false;
}
}
function revisionCheck(revision) {
if (revision !== state.revision) throw new Error("Scene changed. Retry using the current version.");
}
function selectionCheck(ids) {
if (!Array.isArray(ids) || ids.length > 180 || ids.some((id) => typeof id !== "string" || !state.scene.objects.some((o) => o.id === id))) throw new Error("Selection contains an invalid object");
}
return { get state() {
return state;
}, get mutationBusy() {
return mutationBusy;
}, history, clients, broadcast, commit, saveCurrentScene, revisionCheck, selectionCheck };
}
export {
createSceneStore
};