载入中...
搜索中...
未找到
EditorTransactionConsumer.cpp
浏览该文件的文档.
2
3#include <charconv>
4#include <exception>
5#include <limits>
6#include <optional>
7#include <utility>
8#include <vector>
9
10namespace eve::editor {
11namespace {
12
13eve::DiagnosticCode commonDiagnostic(EditorStatus status) noexcept {
14 switch (status) {
15 case EditorStatus::Rejected: return eve::DiagnosticCode::InvalidArgument;
16 case EditorStatus::Conflict: return eve::DiagnosticCode::Conflict;
17 case EditorStatus::NotFound: return eve::DiagnosticCode::NotFound;
18 case EditorStatus::Unsupported: return eve::DiagnosticCode::Unsupported;
19 case EditorStatus::Cancelled: return eve::DiagnosticCode::Cancelled;
20 case EditorStatus::Ok:
21 case EditorStatus::Applied:
22 case EditorStatus::Pending:
23 case EditorStatus::NoOp: return eve::DiagnosticCode::None;
24 case EditorStatus::Failed: return eve::DiagnosticCode::Failed;
25 }
27}
28
29template <class Output, class Input>
30eve::Result<Output> convertEditorFailure(const Result<Input>& source, std::string_view context) {
31 if (!source.diagnostics().empty()) return eve::Result<Output>::failure(source.status());
33 eve::Status::failure(source.code(), eve::Diagnostic::error(commonDiagnostic(source.code()),
34 std::string(context))));
35}
36
37bool hasCoordinatorCleanupFailure(const eve::Status& status) {
38 for (const auto& diagnostic : status.diagnostics()) {
39 if (diagnostic.path().starts_with("transaction.rollback") ||
40 diagnostic.path().starts_with("transaction.compensation"))
41 return true;
42 }
43 return false;
44}
45
46std::optional<std::size_t> coordinatorFailedCommitIndex(const eve::Status& status) noexcept {
47 constexpr std::string_view prefix = "transaction.commit[";
48 for (const auto& diagnostic : status.diagnostics()) {
49 const std::string& path = diagnostic.path();
50 if (!path.starts_with(prefix) || path.back() != ']') continue;
51 std::uint64_t index = 0;
52 const char* first = path.data() + prefix.size();
53 const char* last = path.data() + path.size() - 1;
54 const auto [end, error] = std::from_chars(first, last, index);
55 if (error == std::errc{} && end == last &&
56 index <= static_cast<std::uint64_t>(std::numeric_limits<std::size_t>::max()))
57 return static_cast<std::size_t>(index);
58 }
59 return std::nullopt;
60}
61
62bool hasPreviouslyCommittedAdditionalParticipant(const eve::Status& status,
63 std::size_t editorParticipantCount) noexcept {
64 const auto failedIndex = coordinatorFailedCommitIndex(status);
65 // Editor-owned participants are rebuilt for every attempt. A borrowed
66 // additional participant that precedes the failing commit is different:
67 // its effect may already have escaped, and retaining only its name is not
68 // enough to prove that a retry is idempotent. Require explicit discard or
69 // reconciliation instead of invoking that participant twice.
70 return failedIndex.has_value() && *failedIndex > editorParticipantCount;
71}
72
73struct CommandBatch {
74 std::vector<std::unique_ptr<IEditCommand>> commands;
75 bool previewApplied = false;
76};
77
78class CommandParticipant final : public transaction::ITransactionParticipant {
79public:
80 enum class InitialPhase { Idle, Committed };
81
82 explicit CommandParticipant(CommandBatch& batch, InitialPhase initial = InitialPhase::Idle)
83 : batch_(batch), phase_(initial == InitialPhase::Committed ? Phase::Committed : Phase::Idle) {}
84
85 std::string_view name() const noexcept override { return "editor.commands"; }
86
87 [[nodiscard]] eve::Result<void> prepare(const transaction::TransactionContext&) override {
88 if (phase_ != Phase::Idle)
90 "editor command participant is not idle",
91 "editor.commands.prepare"));
92 if (batch_.commands.empty())
95 "editor transaction must contain at least one command", "editor.commands"));
96 for (const auto& command : batch_.commands) {
97 if (!command)
99 eve::DiagnosticCode::InvalidArgument, "editor command must not be null", "editor.commands"));
100 }
101 phase_ = Phase::Prepared;
103 }
104
105 [[nodiscard]] eve::Result<void> commit(const transaction::TransactionContext&) override {
106 if (phase_ != Phase::Prepared)
108 "editor command participant is not prepared",
109 "editor.commands.commit"));
110 if (!batch_.previewApplied) {
111 std::size_t applied = 0;
112 try {
113 for (const auto& command : batch_.commands) {
114 if (!command->apply()) {
115 const bool reverted = revertPrefix(applied);
116 if (!reverted) batch_.previewApplied = true;
119 reverted ? "editor command rejected during commit"
120 : "editor command commit failed and compensation was incomplete",
121 "editor.commands.commit"));
122 }
123 ++applied;
124 }
125 } catch (const std::exception& exception) {
126 const bool reverted = revertPrefix(applied);
127 if (!reverted) batch_.previewApplied = true;
129 eve::DiagnosticCode::Failed, std::string("editor command threw during commit: ") + exception.what(),
130 "editor.commands.commit"));
131 } catch (...) {
132 const bool reverted = revertPrefix(applied);
133 if (!reverted) batch_.previewApplied = true;
135 eve::DiagnosticCode::Failed, "editor command threw during commit", "editor.commands.commit"));
136 }
137 batch_.previewApplied = true;
138 }
139 phase_ = Phase::Committed;
141 }
142
143 [[nodiscard]] eve::Result<void> rollback(const transaction::TransactionContext&) override {
144 if (phase_ != Phase::Prepared)
146 "editor command participant is not prepared",
147 "editor.commands.rollback"));
148 if (batch_.previewApplied && !revertAll())
150 "editor command preview rollback was incomplete",
151 "editor.commands.rollback"));
152 batch_.previewApplied = false;
153 phase_ = Phase::RolledBack;
155 }
156
157 [[nodiscard]] eve::Result<void> compensate(const transaction::TransactionContext&) override {
158 if (phase_ != Phase::Committed)
160 "editor command participant is not committed",
161 "editor.commands.compensate"));
162 if (!batch_.previewApplied || !revertAll())
164 "editor command compensation was incomplete",
165 "editor.commands.compensate"));
166 batch_.previewApplied = false;
167 phase_ = Phase::Compensated;
169 }
170
171private:
172 enum class Phase { Idle, Prepared, Committed, RolledBack, Compensated };
173
174 bool revertPrefix(std::size_t count) noexcept {
175 try {
176 while (count > 0) batch_.commands[--count]->revert();
177 return true;
178 } catch (...) {
179 return false;
180 }
181 }
182
183 bool revertAll() noexcept { return revertPrefix(batch_.commands.size()); }
184
185 CommandBatch& batch_;
186 Phase phase_ = Phase::Idle;
187};
188
189class AuthorityParticipant final : public transaction::ITransactionParticipant {
190public:
191 AuthorityParticipant(IEditAuthority& authority, TransactionSpec specification,
192 std::vector<DomainOperation> operations)
193 : authority_(&authority), specification_(std::move(specification)), operations_(std::move(operations)) {}
194
195 AuthorityParticipant(IEditAuthority& authority, TransactionSpec specification,
196 std::vector<DomainOperation> operations, TransactionReceipt receipt)
197 : authority_(&authority),
198 specification_(std::move(specification)),
199 operations_(std::move(operations)),
200 receipt_(std::move(receipt)),
201 phase_(Phase::Committed) {}
202
203 std::string_view name() const noexcept override { return "editor.authority"; }
204
205 [[nodiscard]] eve::Result<void> prepare(const transaction::TransactionContext&) override {
206 if (phase_ != Phase::Idle)
208 "editor authority participant is not idle",
209 "editor.authority.prepare"));
210 if (!authority_)
212 eve::DiagnosticCode::Failed, "editor authority participant has no authority", "editor.authority"));
213 auto plan = authority_->preflight(specification_, operations_);
214 if (!plan.ok()) return convertEditorFailure<void>(plan, "editor authority preflight");
215 plan_ = std::move(plan.value());
216 phase_ = Phase::Prepared;
218 }
219
220 [[nodiscard]] eve::Result<void> commit(const transaction::TransactionContext&) override {
221 if (phase_ != Phase::Prepared || !plan_)
223 "editor authority participant is not prepared",
224 "editor.authority.commit"));
225 auto result = authority_->commit(*plan_);
226 if (!result.ok()) return convertEditorFailure<void>(result, "editor authority commit");
227 receipt_ = std::move(result.value());
228 phase_ = Phase::Committed;
230 }
231
232 [[nodiscard]] eve::Result<void> rollback(const transaction::TransactionContext&) override {
233 if (phase_ != Phase::Prepared)
235 "editor authority participant is not prepared",
236 "editor.authority.rollback"));
237 plan_.reset();
238 phase_ = Phase::RolledBack;
240 }
241
242 [[nodiscard]] eve::Result<void> compensate(const transaction::TransactionContext&) override {
243 if (phase_ != Phase::Committed || !receipt_)
245 eve::DiagnosticCode::PreconditionViolation, "editor authority participant has no committed receipt",
246 "editor.authority.compensate"));
247 auto result = authority_->compensate(*receipt_);
248 if (!result.ok())
249 return convertEditorFailure<void>(result, "editor authority compensation");
250 // History keeps the original commit receipt. This participant's
251 // latest effect is the compensation receipt, whose afterRevision is
252 // also the base revision required by a later redo.
253 receipt_ = std::move(result.value());
254 phase_ = Phase::Compensated;
256 }
257
258 [[nodiscard]] const std::optional<TransactionReceipt>& receipt() const noexcept { return receipt_; }
259
260private:
261 enum class Phase { Idle, Prepared, Committed, RolledBack, Compensated };
262
263 IEditAuthority* authority_ = nullptr;
264 TransactionSpec specification_;
265 std::vector<DomainOperation> operations_;
266 std::optional<AuthorityPlan> plan_;
267 std::optional<TransactionReceipt> receipt_;
268 Phase phase_ = Phase::Idle;
269};
270
271} // namespace
272
274 struct Pending {
276 std::unique_ptr<CommandBatch> commands;
277 std::vector<DomainOperation> operations;
278 bool commandModeSet = false;
279 bool previewMode = false;
280 std::vector<std::string> additionalNames;
281 std::uint64_t attemptSequence = 0;
282 };
283
286 std::unique_ptr<CommandBatch> commands;
287 std::vector<DomainOperation> operations;
288 std::optional<TransactionReceipt> authorityReceipt;
289 };
290
291 explicit Impl(IEditAuthority* authorityValue) : authority(authorityValue) {}
292
294 std::optional<Pending> pending;
295 std::vector<HistoryEntry> undo;
296 std::vector<HistoryEntry> redo;
298 std::uint64_t nextLegacyId = 1;
299 std::uint64_t redoSequence = 0;
301 std::vector<eve::Diagnostic> diagnostics;
302 bool retryAllowed = false;
304};
305
307 : impl_(std::make_unique<Impl>(authority)) {}
308
310
312 if (impl_->pending)
315 "editor authority cannot change during an active transaction", "editor.authority"));
316 impl_->authority = authority;
318}
319
321 if (impl_->pending)
323 "an editor transaction is already active",
324 "editor.transaction"));
325 if (specification.id.empty())
327 eve::DiagnosticCode::InvalidArgument, "editor transaction id is required", "editor.transaction.id"));
328 const TransactionId id = specification.id;
330 pending.specification = std::move(specification);
331 impl_->pending = std::move(pending);
332 impl_->state = EditorCommitState::Pending;
333 impl_->diagnostics.clear();
334 impl_->retryAllowed = false;
335 impl_->retryBlockedByCommittedParticipant = false;
337}
338
340 if (impl_->nextLegacyId == std::numeric_limits<std::uint64_t>::max())
342 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "legacy editor transaction id allocator is exhausted",
343 "editor.transaction.id"));
344 TransactionSpec specification;
345 specification.id = TransactionId("editor.legacy." + std::to_string(impl_->nextLegacyId++));
346 specification.label = std::move(label);
347 return begin(std::move(specification));
348}
349
350eve::Result<void> EditorTransactionConsumer::append(std::unique_ptr<IEditCommand> command) {
351 if (!impl_->pending)
353 eve::DiagnosticCode::PreconditionViolation, "no editor transaction is active", "editor.transaction"));
354 if (!command)
356 "editor command must not be null", "editor.command"));
357 if (!impl_->pending->operations.empty())
359 eve::DiagnosticCode::Conflict, "commands and authority operations cannot share one editor transaction",
360 "editor.transaction"));
361 if (impl_->pending->commandModeSet && impl_->pending->previewMode)
363 eve::DiagnosticCode::Conflict, "a strict command cannot follow a preview command", "editor.transaction"));
364 impl_->pending->commandModeSet = true;
365 impl_->pending->previewMode = false;
366 if (!impl_->pending->commands) impl_->pending->commands = std::make_unique<CommandBatch>();
367 if (!impl_->pending->commands->commands.empty()) {
368 try {
369 if (impl_->pending->commands->commands.back()->mergeWith(*command)) return eve::Result<void>::success();
370 } catch (const std::exception& exception) {
372 eve::DiagnosticCode::Failed, std::string("editor command merge threw: ") + exception.what(),
373 "editor.command.merge"));
374 } catch (...) {
376 eve::DiagnosticCode::Failed, "editor command merge threw", "editor.command.merge"));
377 }
378 }
379 impl_->pending->commands->commands.push_back(std::move(command));
381}
382
383eve::Result<void> EditorTransactionConsumer::appendPreview(std::unique_ptr<IEditCommand> command) {
384 if (!impl_->pending)
386 eve::DiagnosticCode::PreconditionViolation, "no editor transaction is active", "editor.transaction"));
387 if (!command)
389 "editor command must not be null", "editor.command"));
390 if (!impl_->pending->operations.empty())
392 eve::DiagnosticCode::Conflict, "commands and authority operations cannot share one editor transaction",
393 "editor.transaction"));
394 if (impl_->pending->commandModeSet && !impl_->pending->previewMode)
396 eve::DiagnosticCode::Conflict, "a preview command cannot follow a strict command", "editor.transaction"));
397 if (!impl_->pending->commands) impl_->pending->commands = std::make_unique<CommandBatch>();
398
399 // A command is applied only after merge evaluation has completed on an
400 // owning candidate copy. The candidate carries the aggregate history but
401 // never publishes to the real target; the incoming command is the only
402 // object that applies the next preview delta.
403 std::unique_ptr<IEditCommand> mergedCandidate;
404 if (!impl_->pending->commands->commands.empty()) {
405 try {
406 mergedCandidate = impl_->pending->commands->commands.back()->clone();
407 } catch (const std::exception& exception) {
409 eve::DiagnosticCode::Failed, std::string("editor command staging threw: ") + exception.what(),
410 "editor.command.stage"));
411 } catch (...) {
413 eve::DiagnosticCode::Failed, "editor command staging threw", "editor.command.stage"));
414 }
415 if (mergedCandidate) {
416 try {
417 if (mergedCandidate->mergeWith(*command)) {
418 // The merge has changed only mergedCandidate. Applying the
419 // incoming delta keeps the live target at the same state
420 // that the command recorded as its `before` value.
421 bool applied = false;
422 try {
423 applied = command->apply();
424 } catch (const std::exception& exception) {
425 try {
426 command->revert();
427 } catch (...) {
429 eve::DiagnosticCode::Failed, "editor command preview failed and could not be reverted",
430 "editor.command.preview"));
431 }
434 std::string("editor command preview threw: ") + exception.what(),
435 "editor.command.preview"));
436 } catch (...) {
437 try {
438 command->revert();
439 } catch (...) {
441 eve::DiagnosticCode::Failed, "editor command preview failed and could not be reverted",
442 "editor.command.preview"));
443 }
445 eve::DiagnosticCode::Failed, "editor command preview threw", "editor.command.preview"));
446 }
447 if (!applied) {
448 try {
449 command->revert();
450 } catch (...) {
453 "editor command preview was rejected and could not be reverted",
454 "editor.command.preview"));
455 }
457 "editor command preview was rejected",
458 "editor.command.preview"));
459 }
460 impl_->pending->commands->commands.back() = std::move(mergedCandidate);
461 impl_->pending->commands->previewApplied = true;
462 impl_->pending->commandModeSet = true;
463 impl_->pending->previewMode = true;
465 }
466 } catch (const std::exception& exception) {
468 eve::DiagnosticCode::Failed, std::string("editor command merge threw: ") + exception.what(),
469 "editor.command.merge"));
470 } catch (...) {
472 eve::DiagnosticCode::Failed, "editor command merge threw", "editor.command.merge"));
473 }
474 }
475 }
476
477 try {
478 // Reserve before applying so vector growth cannot be the first
479 // observable failure after the target has been changed.
480 impl_->pending->commands->commands.reserve(impl_->pending->commands->commands.size() + 1);
481 } catch (const std::exception& exception) {
483 eve::DiagnosticCode::Failed, std::string("editor command staging allocation threw: ") + exception.what(),
484 "editor.command.stage"));
485 } catch (...) {
487 eve::DiagnosticCode::Failed, "editor command staging allocation threw", "editor.command.stage"));
488 }
489
490 bool applied = false;
491 try {
492 applied = command->apply();
493 } catch (const std::exception& exception) {
494 try {
495 command->revert();
496 } catch (...) {
498 eve::DiagnosticCode::Failed, "editor command preview failed and could not be reverted",
499 "editor.command.preview"));
500 }
502 eve::DiagnosticCode::Failed, std::string("editor command preview threw: ") + exception.what(),
503 "editor.command.preview"));
504 } catch (...) {
505 try {
506 command->revert();
507 } catch (...) {
509 eve::DiagnosticCode::Failed, "editor command preview failed and could not be reverted",
510 "editor.command.preview"));
511 }
513 eve::DiagnosticCode::Failed, "editor command preview threw", "editor.command.preview"));
514 }
515 if (!applied) {
516 try {
517 command->revert();
518 } catch (...) {
520 eve::DiagnosticCode::Failed, "editor command preview was rejected and could not be reverted",
521 "editor.command.preview"));
522 }
524 eve::DiagnosticCode::InvalidArgument, "editor command preview was rejected", "editor.command.preview"));
525 }
526 impl_->pending->commands->commands.push_back(std::move(command));
527 impl_->pending->commands->previewApplied = true;
528 impl_->pending->commandModeSet = true;
529 impl_->pending->previewMode = true;
531}
532
534 if (!impl_->pending)
536 eve::DiagnosticCode::PreconditionViolation, "no editor transaction is active", "editor.transaction"));
537 if (operation.type.empty() || operation.target.empty())
539 "authority operation type and target are required",
540 "editor.operation"));
541 if (impl_->pending->commands)
543 eve::DiagnosticCode::Conflict, "commands and authority operations cannot share one editor transaction",
544 "editor.transaction"));
545 if (impl_->pending->specification.target.empty())
547 "authority transaction target is required",
548 "editor.transaction.target"));
549 if (operation.target != impl_->pending->specification.target)
551 eve::DiagnosticCode::Conflict, "authority operation target does not match the transaction target",
552 "editor.operation.target"));
553 impl_->pending->operations.push_back(std::move(operation));
555}
556
558 if (!impl_->pending)
560 eve::DiagnosticCode::PreconditionViolation, "no editor transaction is active", "editor.transaction"));
561 const auto& pending = *impl_->pending;
562 const std::size_t commandCount = pending.commands ? pending.commands->commands.size() : 0;
563 if (commandCount == 0 && pending.operations.empty())
565 eve::DiagnosticCode::InvalidArgument, "editor transaction has no work", "editor.transaction"));
566 EditorDryRunReport report;
567 report.specification = pending.specification;
568 report.commandCount = commandCount;
569 report.operationCount = pending.operations.size();
570 if (!pending.operations.empty()) {
571 if (!impl_->authority)
574 "authority preflight requires an injected authority", "editor.authority"));
575 auto plan = impl_->authority->preflight(pending.specification, pending.operations);
576 if (!plan.ok())
577 return convertEditorFailure<EditorDryRunReport>(plan, "editor authority preflight");
578 report.authorityPlan = std::move(plan.value());
579 }
580 return eve::Result<EditorDryRunReport>::success(std::move(report));
581}
582
584 return commitAttempt(std::span<transaction::ITransactionParticipant*>{}, false);
585}
586
588 std::span<transaction::ITransactionParticipant*> additional) {
589 return commitAttempt(additional, false);
590}
591
593 std::span<transaction::ITransactionParticipant*> additional) {
594 if (!impl_->pending)
597 "no failed editor transaction is pending", "editor.transaction.retry"));
598 if (impl_->state != EditorCommitState::FailedRetryable)
601 "editor transaction is not awaiting retry", "editor.transaction.retry"));
602 if (!impl_->retryAllowed)
605 impl_->retryBlockedByCommittedParticipant
606 ? "editor transaction cannot be retried after an additional participant committed; reconcile or "
607 "discard it"
608 : "editor transaction cannot be retried after incomplete cleanup; discard it",
609 "editor.transaction.retry"));
610 return commitAttempt(additional, true);
611}
612
613eve::Result<EditorTransactionRecord> EditorTransactionConsumer::commitAttempt(
614 std::span<transaction::ITransactionParticipant*> additional, bool retryAttempt) {
615 if (!impl_->pending)
617 eve::DiagnosticCode::PreconditionViolation, "no editor transaction is active", "editor.transaction"));
618
619 auto retainFailure = [&](const eve::Status& status, bool retryAllowed) {
621 impl_->retryAllowed = retryAllowed;
622 impl_->retryBlockedByCommittedParticipant = false;
623 impl_->diagnostics = status.diagnostics();
625 };
626 auto reject = [&](eve::DiagnosticCode code, std::string message, std::string path) {
629 const bool preserveRetryBlock = impl_->state == EditorCommitState::FailedRetryable;
631 if (!preserveRetryBlock) {
632 impl_->retryAllowed = true;
633 impl_->retryBlockedByCommittedParticipant = false;
634 }
635 impl_->diagnostics = result.diagnostics();
636 return result;
637 };
638
639 if (!retryAttempt && impl_->state == EditorCommitState::FailedRetryable)
640 return reject(eve::DiagnosticCode::Conflict,
641 "editor commit failed; call retry() or discard() before another commit attempt",
642 "editor.transaction.commit");
643 if (retryAttempt && impl_->state != EditorCommitState::FailedRetryable)
644 return reject(eve::DiagnosticCode::PreconditionViolation, "editor transaction is not awaiting retry",
645 "editor.transaction.retry");
646
647 Impl::Pending& pending = *impl_->pending;
648 const std::size_t commandCount = pending.commands ? pending.commands->commands.size() : 0;
649 if (commandCount == 0 && pending.operations.empty())
650 return reject(eve::DiagnosticCode::InvalidArgument, "editor transaction has no work", "editor.transaction");
651 if (!pending.operations.empty() && !impl_->authority)
652 return reject(eve::DiagnosticCode::Unsupported, "authority commit requires an injected authority",
653 "editor.authority");
654
655 for (std::size_t i = 0; i < additional.size(); ++i) {
656 if (!additional[i])
657 return reject(eve::DiagnosticCode::InvalidArgument, "additional participant must not be null",
658 "editor.transaction.participants[" + std::to_string(i) + "]");
659 for (std::size_t previous = 0; previous < i; ++previous) {
660 if (additional[previous] == additional[i])
661 return reject(eve::DiagnosticCode::Conflict,
662 "an additional participant may occur only once in an editor transaction",
663 "editor.transaction.participants[" + std::to_string(i) + "]");
664 }
665 }
666
667 if (!retryAttempt) {
668 pending.additionalNames.clear();
669 pending.additionalNames.reserve(additional.size());
670 for (auto* participant : additional) pending.additionalNames.emplace_back(participant->name());
671 } else {
672 if (additional.size() != pending.additionalNames.size())
673 return reject(eve::DiagnosticCode::Conflict, "retry must provide the same additional participant set",
674 "editor.transaction.retry");
675 for (std::size_t i = 0; i < additional.size(); ++i) {
676 if (additional[i]->name() != pending.additionalNames[i])
677 return reject(eve::DiagnosticCode::Conflict,
678 "retry participant order or identity does not match the failed attempt",
679 "editor.transaction.retry");
680 }
681 }
682
683 std::unique_ptr<AuthorityParticipant> authorityParticipant;
684 std::unique_ptr<CommandParticipant> commandParticipant;
685 std::vector<transaction::ITransactionParticipant*> participants;
686 if (!pending.operations.empty()) {
687 authorityParticipant =
688 std::make_unique<AuthorityParticipant>(*impl_->authority, pending.specification, pending.operations);
689 participants.push_back(authorityParticipant.get());
690 }
691 if (pending.commands) {
692 commandParticipant = std::make_unique<CommandParticipant>(*pending.commands);
693 participants.push_back(commandParticipant.get());
694 }
695 participants.insert(participants.end(), additional.begin(), additional.end());
696 if (participants.empty())
697 return reject(eve::DiagnosticCode::InvalidArgument, "editor transaction has no participant",
698 "editor.transaction");
699
700 if (retryAttempt) {
701 if (pending.attemptSequence == std::numeric_limits<std::uint64_t>::max())
702 return reject(eve::DiagnosticCode::Failed, "editor transaction retry sequence is exhausted",
703 "editor.transaction.retry");
704 ++pending.attemptSequence;
705 }
706 std::string attemptId = pending.specification.id.value();
707 if (retryAttempt) attemptId += ".retry." + std::to_string(pending.attemptSequence);
708 transaction::TransactionContext context(std::move(attemptId));
709 auto coordinated = impl_->coordinator.execute(context, participants);
710 if (!coordinated.ok()) {
711 const eve::Status& status = coordinated.status();
712 const bool committedAdditional = hasPreviouslyCommittedAdditionalParticipant(
713 status, (pending.operations.empty() ? 0u : 1u) + (pending.commands ? 1u : 0u));
714 auto result = retainFailure(status, !hasCoordinatorCleanupFailure(status) && !committedAdditional);
715 impl_->retryBlockedByCommittedParticipant = committedAdditional;
716 return result;
717 }
718 transaction::TransactionReceipt coordinatorReceipt = std::move(coordinated).takeValue();
719
720 Impl::HistoryEntry history;
721 history.specification = pending.specification;
722 history.commands = std::move(pending.commands);
723 history.operations = std::move(pending.operations);
724 if (authorityParticipant) history.authorityReceipt = authorityParticipant->receipt();
725 impl_->undo.push_back(std::move(history));
726 impl_->redo.clear();
727 impl_->pending.reset();
728 impl_->state = EditorCommitState::Committed;
729 impl_->retryAllowed = false;
730 impl_->retryBlockedByCommittedParticipant = false;
731 impl_->diagnostics.clear();
732
733 EditorTransactionRecord record;
734 record.coordinator = std::move(coordinatorReceipt);
735 record.specification = impl_->undo.back().specification;
736 record.authorityReceipt = impl_->undo.back().authorityReceipt;
737 record.commandCount = commandCount;
738 record.operationCount = impl_->undo.back().operations.size();
739 return eve::Result<EditorTransactionRecord>::success(std::move(record));
740}
741
743 if (!impl_->pending)
745 eve::DiagnosticCode::PreconditionViolation, "no editor transaction is active", "editor.transaction"));
746 Impl::Pending& pending = *impl_->pending;
747 if (pending.commands && pending.commands->previewApplied) {
748 try {
749 for (auto it = pending.commands->commands.rbegin(); it != pending.commands->commands.rend(); ++it)
750 (*it)->revert();
751 } catch (const std::exception& exception) {
753 eve::DiagnosticCode::Failed, std::string("editor preview rollback threw: ") + exception.what(),
754 "editor.transaction.discard"));
756 impl_->retryAllowed = false;
757 impl_->retryBlockedByCommittedParticipant = false;
758 impl_->diagnostics = result.diagnostics();
759 return result;
760 } catch (...) {
762 eve::DiagnosticCode::Failed, "editor preview rollback threw", "editor.transaction.discard"));
764 impl_->retryAllowed = false;
765 impl_->retryBlockedByCommittedParticipant = false;
766 impl_->diagnostics = result.diagnostics();
767 return result;
768 }
769 pending.commands->previewApplied = false;
770 }
771 impl_->pending.reset();
772 impl_->state = EditorCommitState::Discarded;
773 impl_->retryAllowed = false;
774 impl_->retryBlockedByCommittedParticipant = false;
775 impl_->diagnostics.clear();
777}
778
780
782 if (impl_->pending)
785 "cannot undo while an editor transaction is active", "editor.transaction.undo"));
786 if (impl_->undo.empty())
788 eve::DiagnosticCode::NotFound, "there is no editor transaction to undo", "editor.history.undo"));
789 Impl::HistoryEntry history = std::move(impl_->undo.back());
790 impl_->undo.pop_back();
791
792 std::unique_ptr<AuthorityParticipant> authorityParticipant;
793 std::unique_ptr<CommandParticipant> commandParticipant;
794 std::vector<transaction::ITransactionParticipant*> participants;
795 if (history.authorityReceipt && impl_->authority) {
796 authorityParticipant = std::make_unique<AuthorityParticipant>(*impl_->authority, history.specification,
797 history.operations, *history.authorityReceipt);
798 participants.push_back(authorityParticipant.get());
799 }
800 if (history.commands) {
801 commandParticipant =
802 std::make_unique<CommandParticipant>(*history.commands, CommandParticipant::InitialPhase::Committed);
803 participants.push_back(commandParticipant.get());
804 }
805 if (participants.empty()) {
806 impl_->undo.push_back(std::move(history));
809 "editor history entry has no compensatable participant", "editor.history.undo"));
810 }
811 const std::string compensationId =
812 history.specification.id.value() + ".undo." + std::to_string(++impl_->redoSequence);
814 auto compensated = impl_->coordinator.compensate(context, participants);
815 if (!compensated.ok()) {
816 impl_->undo.push_back(std::move(history));
817 return eve::Result<EditorTransactionRecord>::failure(compensated.status());
818 }
819 transaction::TransactionReceipt receipt = std::move(compensated).takeValue();
820 if (authorityParticipant && authorityParticipant->receipt())
821 history.specification.baseRevision = authorityParticipant->receipt()->afterRevision;
822 impl_->redo.push_back(std::move(history));
823 impl_->state = EditorCommitState::Discarded;
824 const auto& moved = impl_->redo.back();
826 record.coordinator = std::move(receipt);
827 record.specification = moved.specification;
828 record.authorityReceipt = moved.authorityReceipt;
829 record.commandCount = moved.commands ? moved.commands->commands.size() : 0;
830 record.operationCount = moved.operations.size();
831 return eve::Result<EditorTransactionRecord>::success(std::move(record));
832}
833
835 if (impl_->pending)
838 "cannot redo while an editor transaction is active", "editor.transaction.redo"));
839 if (impl_->redo.empty())
841 eve::DiagnosticCode::NotFound, "there is no editor transaction to redo", "editor.history.redo"));
842 Impl::HistoryEntry history = std::move(impl_->redo.back());
843 impl_->redo.pop_back();
844 if (history.operations.empty() && !history.commands) {
845 impl_->redo.push_back(std::move(history));
848 "editor history entry has no replayable participant", "editor.history.redo"));
849 }
850 if (!history.operations.empty() && !impl_->authority) {
851 impl_->redo.push_back(std::move(history));
853 eve::DiagnosticCode::Unsupported, "authority redo requires an injected authority", "editor.authority"));
854 }
855
856 TransactionSpec specification = history.specification;
857 specification.id = TransactionId(specification.id.value() + ".redo." + std::to_string(++impl_->redoSequence));
858 std::unique_ptr<AuthorityParticipant> authorityParticipant;
859 std::unique_ptr<CommandParticipant> commandParticipant;
860 std::vector<transaction::ITransactionParticipant*> participants;
861 if (!history.operations.empty()) {
862 authorityParticipant =
863 std::make_unique<AuthorityParticipant>(*impl_->authority, specification, history.operations);
864 participants.push_back(authorityParticipant.get());
865 }
866 if (history.commands) {
867 commandParticipant = std::make_unique<CommandParticipant>(*history.commands);
868 participants.push_back(commandParticipant.get());
869 }
871 auto replayed = impl_->coordinator.execute(context, participants);
872 if (!replayed.ok()) {
873 impl_->redo.push_back(std::move(history));
874 return eve::Result<EditorTransactionRecord>::failure(replayed.status());
875 }
876 transaction::TransactionReceipt receipt = std::move(replayed).takeValue();
877 history.specification = specification;
878 if (authorityParticipant) history.authorityReceipt = authorityParticipant->receipt();
879 impl_->undo.push_back(std::move(history));
880 impl_->state = EditorCommitState::Committed;
881 if (!impl_->redo.empty() && authorityParticipant && authorityParticipant->receipt())
882 impl_->redo.back().specification.baseRevision = authorityParticipant->receipt()->afterRevision;
883 const auto& moved = impl_->undo.back();
885 record.coordinator = std::move(receipt);
886 record.specification = moved.specification;
887 record.authorityReceipt = moved.authorityReceipt;
888 record.commandCount = moved.commands ? moved.commands->commands.size() : 0;
889 record.operationCount = moved.operations.size();
890 return eve::Result<EditorTransactionRecord>::success(std::move(record));
891}
892
893bool EditorTransactionConsumer::active() const noexcept { return impl_->pending.has_value(); }
894bool EditorTransactionConsumer::canUndo() const noexcept { return !impl_->undo.empty(); }
895bool EditorTransactionConsumer::canRedo() const noexcept { return !impl_->redo.empty(); }
896std::size_t EditorTransactionConsumer::undoCount() const noexcept { return impl_->undo.size(); }
897std::size_t EditorTransactionConsumer::redoCount() const noexcept { return impl_->redo.size(); }
898EditorCommitState EditorTransactionConsumer::state() const noexcept { return impl_->state; }
899const std::vector<eve::Diagnostic>& EditorTransactionConsumer::diagnostics() const noexcept {
900 return impl_->diagnostics;
901}
902
904 if (impl_->pending) {
905 auto result = discard();
906 if (!result.ok()) {
907 result.ignore("EditorTransactionConsumer::clear retains pending work after a failed discard");
908 return;
909 }
910 }
911 impl_->undo.clear();
912 impl_->redo.clear();
913 impl_->state = EditorCommitState::Discarded;
914 impl_->retryAllowed = false;
915 impl_->retryBlockedByCommittedParticipant = false;
916 impl_->diagnostics.clear();
917}
918
919} // namespace eve::editor
ActionParameterOperation operation
std::vector< QuestEvent > pending
std::string label
std::string message
DiagnosticCode code
wgpu::PopErrorScopeStatus status
float u
Definition Grass.cpp:233
std::int32_t first
std::string name
graphics::Canvas * previous
std::string error
Definition Package.cpp:60
std::string path
Definition PlayHost.cpp:110
float begin
std::uint32_t count
uint32_t index
const UnitySourceAsset & source
std::vector< VegetationPresetCommand > commands
const VegetationPresetContext & context
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
Structured status and zero or more diagnostics for an operation.
Definition Status.h:68
static Status failure(StatusCode code, Diagnostic diagnostic)
Construct a failed status with one diagnostic.
Definition Status.h:84
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
UUID-backed identifier adapter for legacy textual boundaries.
Definition Identity.h:314
const std::string & value() const noexcept
Returns the exact legacy or canonical compatibility spelling.
Definition Identity.h:362
bool empty() const noexcept
True when no compatibility spelling has been assigned.
Definition Identity.h:374
Final validation and commit boundary for editor mutations.
void clear()
Clears active work and editor-owned undo/redo history.
~EditorTransactionConsumer()
Editor transaction consumer.
eve::Result< TransactionId > begin(TransactionSpec specification)
Begins a transaction with an explicit stable editor identifier.
eve::Result< EditorTransactionRecord > undo()
Compensates the latest editor-owned commit and moves it to redo history.
EditorCommitState state() const noexcept
Returns the explicit state of the active/latest commit lifecycle.
eve::Result< void > append(std::unique_ptr< IEditCommand > command)
Appends a command without publishing its target mutation.
std::size_t redoCount() const noexcept
Returns the number of redo entries.
bool canRedo() const noexcept
Returns whether compensated editor history can be replayed.
const std::vector< eve::Diagnostic > & diagnostics() const noexcept
Returns diagnostics retained from the latest failed commit attempt.
eve::Result< EditorTransactionRecord > retry(std::span< transaction::ITransactionParticipant * > additional={})
Retries a failed commit while retaining its pending work.
eve::Result< void > discard()
Explicitly discards the pending transaction and any preview.
eve::Result< EditorDryRunReport > dryRun() const
Runs structural and authority preflight without publishing state.
eve::Result< void > rollback()
Discards the active transaction and any unpublished preview.
eve::Result< EditorTransactionRecord > commit()
Commits the active editor participant set through Coordinator.
eve::Result< void > setAuthority(IEditAuthority *authority)
Changes the borrowed authority when no transaction is active.
bool active() const noexcept
Returns whether an editor transaction is active.
EditorTransactionConsumer(IEditAuthority *authority=nullptr)
Creates a consumer with an optional non-owning edit authority.
std::size_t undoCount() const noexcept
Returns the number of undo entries.
eve::Result< void > appendPreview(std::unique_ptr< IEditCommand > command)
Compatibility-only append that applies a command as an editor preview.
bool canUndo() const noexcept
Returns whether editor-owned history can be compensated.
eve::Result< TransactionId > beginLegacy(std::string label)
Begins a compatibility command transaction with an allocated identifier.
eve::Result< EditorTransactionRecord > redo()
Re-executes the latest compensated editor-owned transaction.
Coordinates participant lifecycles with deterministic ordering.
Immutable context shared by every participant of one transaction.
Definition Transaction.h:30
Result< T > applied(T value, std::vector< Diagnostic > diagnostics={})
Construct an Applied result with an owning payload and optional diagnostics.
editing::IEditAuthority IEditAuthority
eve::editing::TransactionReceipt TransactionReceipt
eve::editing::TransactionSpec TransactionSpec
eve::editing::TransactionId TransactionId
Definition EditorIds.h:34
EditorCommitState
Lifecycle state of the editor consumer's active commit attempt.
@ Pending
A transaction is open and has not yet had a commit attempt fail.
@ FailedRetryable
The last attempt failed and retained work may be retried safely.
@ Discarded
The active transaction was explicitly rolled back or discarded.
@ Committed
The active transaction was committed and moved to editor history.
eve::StatusCode EditorStatus
Definition EditorResult.h:8
Build metadata (engine git commit, build time, third-party version).
Definition Build.cpp:16
DiagnosticCode
Stable machine-readable diagnostic codes.
Definition Diagnostic.h:47
DomainOperation public API.
TransactionSpec public API.
Side-effect-free information returned by an editor preflight.
std::optional< AuthorityPlan > authorityPlan
Structured result of one editor transaction or compensation.
transaction::TransactionReceipt coordinator
std::optional< eve::editor::TransactionReceipt > authorityReceipt
Observable summary returned after an atomic participant commit.
Definition Transaction.h:97