diff --git a/CMakeLists.txt b/CMakeLists.txt index 2add65a..0ac36b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -125,6 +125,7 @@ set(APP_SOURCES "${APP_DIR}/runtime/live/RuntimeStateLayerModel.h" "${APP_DIR}/runtime/live/RuntimeLiveState.cpp" "${APP_DIR}/runtime/live/RuntimeLiveState.h" + "${APP_DIR}/runtime/persistence/PersistenceRequest.h" "${APP_DIR}/runtime/presentation/RuntimeStateJson.cpp" "${APP_DIR}/runtime/presentation/RuntimeStateJson.h" "${APP_DIR}/runtime/presentation/RuntimeStatePresenter.cpp" @@ -184,6 +185,7 @@ target_include_directories(LoopThroughWithOpenGLCompositing PRIVATE "${APP_DIR}/runtime/coordination" "${APP_DIR}/runtime/events" "${APP_DIR}/runtime/live" + "${APP_DIR}/runtime/persistence" "${APP_DIR}/runtime/presentation" "${APP_DIR}/runtime/snapshot" "${APP_DIR}/runtime/store" @@ -293,6 +295,7 @@ target_include_directories(RuntimeEventTypeTests PRIVATE "${APP_DIR}" "${APP_DIR}/runtime" "${APP_DIR}/runtime/events" + "${APP_DIR}/runtime/persistence" ) if(MSVC) @@ -366,6 +369,7 @@ target_include_directories(RuntimeSubsystemTests PRIVATE "${APP_DIR}/runtime/coordination" "${APP_DIR}/runtime/events" "${APP_DIR}/runtime/live" + "${APP_DIR}/runtime/persistence" "${APP_DIR}/runtime/presentation" "${APP_DIR}/runtime/snapshot" "${APP_DIR}/runtime/store" diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.cpp index 89fe1a6..0ce8ad3 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.cpp @@ -580,8 +580,7 @@ void RuntimeCoordinator::PublishCoordinatorFollowUpEvents(const std::string& act if (result.persistenceRequested) { RuntimePersistenceRequestedEvent persistenceRequested; - persistenceRequested.reason = action; - persistenceRequested.debounceAllowed = true; + persistenceRequested.request = PersistenceRequest::RuntimeStateRequest(action); mRuntimeEventDispatcher.PublishPayload(persistenceRequested, "RuntimeCoordinator"); } diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/events/RuntimeEventPayloads.h b/apps/LoopThroughWithOpenGLCompositing/runtime/events/RuntimeEventPayloads.h index 95489b9..9e7d86e 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/events/RuntimeEventPayloads.h +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/events/RuntimeEventPayloads.h @@ -4,6 +4,8 @@ #include #include +#include "PersistenceRequest.h" + #include enum class RuntimeEventSeverity @@ -109,8 +111,7 @@ struct RuntimeStateChangedEvent struct RuntimePersistenceRequestedEvent { - std::string reason; - bool debounceAllowed = true; + PersistenceRequest request; }; struct RuntimeReloadRequestedEvent diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/persistence/PersistenceRequest.h b/apps/LoopThroughWithOpenGLCompositing/runtime/persistence/PersistenceRequest.h new file mode 100644 index 0000000..91838e4 --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/persistence/PersistenceRequest.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include + +enum class PersistenceTargetKind +{ + RuntimeState, + StackPreset, + RuntimeConfig +}; + +struct PersistenceRequest +{ + PersistenceTargetKind targetKind = PersistenceTargetKind::RuntimeState; + std::string reason; + std::string debounceKey = "runtime-state"; + bool debounceAllowed = true; + bool flushRequested = false; + uint64_t sequence = 0; + + static PersistenceRequest RuntimeStateRequest(const std::string& reason) + { + PersistenceRequest request; + request.targetKind = PersistenceTargetKind::RuntimeState; + request.reason = reason; + request.debounceKey = "runtime-state"; + request.debounceAllowed = true; + return request; + } +}; + +struct PersistenceSnapshot +{ + PersistenceTargetKind targetKind = PersistenceTargetKind::RuntimeState; + std::filesystem::path targetPath; + std::string contents; + std::string reason; + uint64_t generation = 0; +}; diff --git a/docs/PHASE_6_BACKGROUND_PERSISTENCE_DESIGN.md b/docs/PHASE_6_BACKGROUND_PERSISTENCE_DESIGN.md index df30cb6..efcfd11 100644 --- a/docs/PHASE_6_BACKGROUND_PERSISTENCE_DESIGN.md +++ b/docs/PHASE_6_BACKGROUND_PERSISTENCE_DESIGN.md @@ -7,15 +7,16 @@ Phases 1-5 separate durable state, coordination policy, render-facing snapshots, ## Status - Phase 6 design package: proposed. -- Phase 6 implementation: not started. -- Current alignment: `RuntimeStore` owns durable serialization, config, package metadata, preset IO, and persistence requests; `CommittedLiveState` owns the current committed/session layer state; and `RuntimeCoordinator` already publishes explicit persistence-request outcomes for persisted mutations. The remaining issue is that actual disk writes are still synchronous store work rather than queued, debounced, atomic background writes. +- Phase 6 implementation: Step 1 complete. +- Current alignment: `RuntimeStore` owns durable serialization, config, package metadata, preset IO, and persistence requests; `CommittedLiveState` owns the current committed/session layer state; and `RuntimeCoordinator` publishes typed persistence requests for persisted mutations. The remaining issue is that actual disk writes are still synchronous store work rather than queued, debounced, atomic background writes. Current persistence footholds: - `RuntimeStore` owns persistent runtime-state serialization, stack preset serialization, and durable file IO. - `CommittedLiveState` owns current committed/session layer and parameter state. - `RuntimeCoordinatorResult::persistenceRequested` exists as an explicit mutation outcome. -- `RuntimeEventType::RuntimePersistenceRequested` exists as the event-level persistence request. +- `RuntimeEventType::RuntimePersistenceRequested` now carries a `PersistenceRequest`. +- `PersistenceRequest` and `PersistenceSnapshot` name the request/snapshot contract that later steps will hand to the writer. - Phase 5 clarified which live-state mutations are durable, committed-live, transient automation, or render-local. Settled OSC commits are session-only by default and do not request persistence. ## Why Phase 6 Exists @@ -183,9 +184,16 @@ Make request types and event payloads explicit enough that callers stop thinking Initial target: -- keep existing coordinator persistence decisions -- introduce a `PersistenceRequest`/`PersistenceSnapshot` shape -- document which requests are debounceable +- [x] keep existing coordinator persistence decisions +- [x] introduce a `PersistenceRequest`/`PersistenceSnapshot` shape +- [x] document which requests are debounceable + +Current implementation: + +- `runtime/persistence/PersistenceRequest.h` defines `PersistenceTargetKind`, `PersistenceRequest`, and `PersistenceSnapshot`. +- `RuntimePersistenceRequestedEvent` carries a typed `PersistenceRequest`. +- `RuntimeCoordinator` emits runtime-state persistence requests with reason, debounce key, and debounce policy. +- Existing synchronous save behavior is intentionally unchanged until Step 2/3. ### Step 2. Extract Snapshot Writing From `RuntimeStore` diff --git a/tests/RuntimeEventTypeTests.cpp b/tests/RuntimeEventTypeTests.cpp index c9651e9..d447813 100644 --- a/tests/RuntimeEventTypeTests.cpp +++ b/tests/RuntimeEventTypeTests.cpp @@ -62,10 +62,19 @@ void TestRuntimeEventPayloadTypes() Expect(rejectedMutation.errorMessage == "Unknown layer.", "mutation payload carries rejection error"); RuntimePersistenceRequestedEvent persistence; - persistence.reason = "UpdateLayerParameter"; - persistence.debounceAllowed = true; + persistence.request = PersistenceRequest::RuntimeStateRequest("UpdateLayerParameter"); Expect(RuntimeEventPayloadType(persistence) == RuntimeEventType::RuntimePersistenceRequested, "runtime persistence payload maps to persistence event type"); - Expect(persistence.debounceAllowed, "runtime persistence payload carries debounce policy"); + Expect(persistence.request.targetKind == PersistenceTargetKind::RuntimeState, "runtime persistence payload carries target kind"); + Expect(persistence.request.reason == "UpdateLayerParameter", "runtime persistence payload carries request reason"); + Expect(persistence.request.debounceAllowed, "runtime persistence payload carries debounce policy"); + Expect(persistence.request.debounceKey == "runtime-state", "runtime persistence payload carries debounce key"); + + PersistenceSnapshot persistenceSnapshot; + persistenceSnapshot.targetKind = PersistenceTargetKind::RuntimeState; + persistenceSnapshot.reason = persistence.request.reason; + persistenceSnapshot.contents = "{}"; + Expect(persistenceSnapshot.reason == "UpdateLayerParameter", "persistence snapshot carries capture reason"); + Expect(persistenceSnapshot.contents == "{}", "persistence snapshot carries serialized content"); FileChangeDetectedEvent fileChange; fileChange.path = "PollRuntimeStoreChanges"; @@ -459,8 +468,7 @@ void TestAcceptedMutationFollowUps() stateChanged.persistenceRequested = true; RuntimePersistenceRequestedEvent persistence; - persistence.reason = mutation.action; - persistence.debounceAllowed = true; + persistence.request = PersistenceRequest::RuntimeStateRequest(mutation.action); RuntimeReloadRequestedEvent reload; reload.reason = mutation.action; @@ -487,7 +495,8 @@ void TestAcceptedMutationFollowUps() const RuntimeEvent* persistenceEvent = harness.LastSeen(RuntimeEventType::RuntimePersistenceRequested); const auto* persistencePayload = persistenceEvent ? std::get_if(&persistenceEvent->payload) : nullptr; - Expect(persistencePayload && persistencePayload->reason == "SetLayerShader", "persistence follow-up preserves mutation action reason"); + Expect(persistencePayload && persistencePayload->request.reason == "SetLayerShader", "persistence follow-up preserves mutation action reason"); + Expect(persistencePayload && persistencePayload->request.debounceKey == "runtime-state", "persistence follow-up preserves debounce key"); } void TestAppLevelBroadcastAndBuildCoalescing() diff --git a/tests/RuntimeSubsystemTests.cpp b/tests/RuntimeSubsystemTests.cpp index d03061b..3cc95bd 100644 --- a/tests/RuntimeSubsystemTests.cpp +++ b/tests/RuntimeSubsystemTests.cpp @@ -259,7 +259,7 @@ void TestRuntimeCoordinatorPersistenceEvents() if (event.type != RuntimeEventType::RuntimePersistenceRequested) continue; const auto* payload = std::get_if(&event.payload); - return payload ? payload->reason : std::string(); + return payload ? payload->request.reason : std::string(); } return std::string(); };