Files
EVEngine/src/modules/editor/EditorDocumentService.h
T

161 lines
6.3 KiB
C++

#pragma once
#include "editor/EditorProtocol.h"
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
namespace eve::editor {
/** @brief Authoring document category used to choose controllers and views. */
enum class DocumentKind {
Scene,
Prefab,
Material,
MaterialFunction,
MaterialInstance,
AnimationClip,
AnimationGraph,
ParticleGraph,
UiDocument,
Timeline,
ProjectSettings,
ImportSettings,
Generic
};
/** @brief Stable document identity; paths are deliberately excluded. */
struct DocumentKey {
DocumentKind kind = DocumentKind::Generic;
AssetGuid asset;
auto operator<=>(const DocumentKey&) const = default;
};
/** @brief Lifecycle state visible to workspaces and close coordination. */
enum class DocumentState { Loading, Ready, Saving, Conflict, Failed, Closed };
/** @brief Separate edit, saved and external-store revision clocks. */
struct DocumentRevision {
Revision edit = 0;
Revision saved = 0;
Revision disk = 0;
Revision preview = 0;
};
/** @brief Immutable document metadata snapshot. */
struct DocumentSnapshot {
DocumentId id;
DocumentKey key;
std::string title;
std::string resourceUri;
DocumentState state = DocumentState::Loading;
DocumentRevision revision;
std::string diskContentHash;
std::vector<StableId> viewIds;
std::vector<EditorDiagnostic> diagnostics;
/** @brief True when the latest edit revision has not been saved. */
bool dirty() const { return revision.edit != revision.saved; }
};
/** @brief Immutable save request capturing one exact edit revision. */
struct SaveTicket {
StableId id;
DocumentId document;
Revision capturedEditRevision = 0;
Revision expectedDiskRevision = 0;
std::string expectedContentHash;
};
/** @brief Value snapshot returned by an atomic document store. */
struct StoredDocument {
EditorValue content;
Revision revision = 0;
std::string contentHash;
};
/** @brief Compare-and-swap persistence boundary used by DocumentService. */
class IAtomicDocumentStore {
public:
virtual ~IAtomicDocumentStore() = default;
/** @brief Read the latest value for a resource URI. */
virtual EditorResult<StoredDocument> read(const std::string& resourceUri) const = 0;
/** @brief Replace a value only when the expected disk revision still matches. */
virtual EditorResult<StoredDocument> compareAndSwap(const std::string& resourceUri, Revision expectedRevision,
const std::string& expectedContentHash,
const EditorValue& content) = 0;
};
/** @brief Deterministic in-memory CAS store suitable for tests and transient tools. */
class MemoryAtomicDocumentStore final : public IAtomicDocumentStore {
public:
EditorResult<StoredDocument> read(const std::string& resourceUri) const override;
EditorResult<StoredDocument> compareAndSwap(const std::string& resourceUri, Revision expectedRevision,
const std::string& expectedContentHash,
const EditorValue& content) override;
private:
std::unordered_map<std::string, StoredDocument> documents_;
};
/**
* @brief Document coordinator with revision-safe requestSave/executeSave sequencing.
*
* The store is non-owning and must outlive this service. A successful old save
* ticket advances `saved` only to its captured edit revision, preserving dirty
* state for edits made while the save was pending.
*/
class DocumentService {
public:
explicit DocumentService(IAtomicDocumentStore* store) : store_(store) {}
/** @brief Open an asset-backed document, loading existing persisted content when present. */
EditorResult<DocumentSnapshot> open(DocumentKey key, std::string title, std::string resourceUri,
EditorValue initialContent = {});
/** @brief Replace the working content using an optional expected edit revision. */
EditorResult<DocumentSnapshot> edit(const DocumentId& document, EditorValue content,
std::optional<Revision> expectedRevision = std::nullopt);
/** @brief Capture an exact content/revision pair for a later CAS save. */
EditorResult<SaveTicket> requestSave(const DocumentId& document);
/** @brief Execute a captured save ticket without reading newer working content. */
EditorResult<DocumentSnapshot> executeSave(const SaveTicket& ticket);
/** @brief Return the current working content snapshot. */
EditorResult<EditorValue> content(const DocumentId& document) const;
/** @brief Return one metadata snapshot. */
EditorResult<DocumentSnapshot> snapshot(const DocumentId& document) const;
/** @brief Return all open documents in stable id order. */
std::vector<DocumentSnapshot> documents() const;
/**
* @brief Reconcile one open document with the latest store value.
* Clean documents adopt external content; dirty documents enter Conflict
* without losing their working content.
* @param document Stable identity of the open document to inspect.
* @return Updated snapshot or a structured conflict/failure.
*/
EditorResult<DocumentSnapshot> reconcileExternal(const DocumentId& document);
/** @brief Close and discard in-memory state; persisted content is unchanged. */
EditorResult<void> close(const DocumentId& document);
private:
struct OpenDocument {
DocumentSnapshot snapshot;
EditorValue content;
};
struct PendingSave {
SaveTicket ticket;
EditorValue content;
};
static EditorResult<DocumentSnapshot> error(EditorStatus status, const char* rule, std::string message);
IAtomicDocumentStore* store_ = nullptr;
std::unordered_map<DocumentId, OpenDocument, StrongEditorIdHash<DocumentId>> open_;
std::unordered_map<StableId, PendingSave, StrongEditorIdHash<StableId>> pendingSaves_;
std::uint64_t ticketSequence_ = 0;
};
} // namespace eve::editor