载入中...
搜索中...
未找到
StatePatch.cpp
浏览该文件的文档.
2
3#include "common/Capability.h"
4#include "common/Json.h"
8
9#include <simplesquirrel/simplesquirrel.hpp>
10
11#include <algorithm>
12#include <exception>
13#include <functional>
14#include <iomanip>
15#include <limits>
16#include <sstream>
17
18namespace eve::statepatch {
19namespace {
20
22struct ScriptStateStore {
23 explicit ScriptStateStore(StateStoreHandleRef value) : reference(value) {}
24 ~ScriptStateStore() noexcept {
25 StatePatch::release(reference).ignore("script state-patch store proxy destruction");
26 }
28};
29
31struct ScriptStateBatch {
32 explicit ScriptStateBatch(StateBatchHandleRef value) : reference(value) {}
33 ~ScriptStateBatch() noexcept {
34 StatePatch::releaseBatch(reference).ignore("script state-patch batch proxy destruction");
35 }
36 StateBatchHandleRef reference;
37};
38
39template <class Ref, class Proxy, class Release>
40ssq::Table makeOwnedProxy(HSQUIRRELVM vm, eve::Result<Ref>&& reference, Release&& release) {
42 const Ref ref = std::move(reference).takeValue();
43 auto object = eve::script::makeOwnedSquirrelInstance<Proxy>(vm, std::make_unique<Proxy>(ref));
44 if (!object) {
45 const eve::Status status = object.status();
46 object.ignore("failed to create owned state-patch proxy");
47 std::invoke(std::forward<Release>(release), ref).ignore("rollback failed owned state-patch allocation");
49 }
50 ssq::Object owned = std::move(object).takeValue();
52 result.set("value", owned);
53 result.set("ownership", std::string("owned"));
54 result.set("ownerEpoch", static_cast<std::int64_t>(ref.ownerEpoch));
55 result.set("handle", static_cast<std::int64_t>(ref.packed()));
56 return result;
57}
58
59std::string quote(const std::string& value) {
60 std::ostringstream out;
61 out << '"';
62 for (unsigned char c : value) {
63 switch (c) {
64 case '"': out << "\\\""; break;
65 case '\\': out << "\\\\"; break;
66 case '\b': out << "\\b"; break;
67 case '\f': out << "\\f"; break;
68 case '\n': out << "\\n"; break;
69 case '\r': out << "\\r"; break;
70 case '\t': out << "\\t"; break;
71 default:
72 if (c < 0x20)
73 out << "\\u" << std::hex << std::setw(4) << std::setfill('0') << int(c) << std::dec;
74 else
75 out << static_cast<char>(c);
76 }
77 }
78 return out.str() + '"';
79}
80
81std::string canonicalJson(const eve::json::Value& value) {
82 if (value.isNull()) return "null";
83 if (value.isBool()) return value.asBool() ? "true" : "false";
84 if (value.isNumber()) return value.asString();
85 if (value.isString()) return quote(value.asString());
86 if (value.isArray()) {
87 std::string out = "[";
88 for (size_t i = 0; i < value.size(); ++i) {
89 if (i) out += ',';
90 out += canonicalJson(value.at(i));
91 }
92 return out + ']';
93 }
94 if (value.isObject()) {
95 auto keys = value.keys();
96 std::sort(keys.begin(), keys.end());
97 std::string out = "{";
98 for (size_t i = 0; i < keys.size(); ++i) {
99 if (i) out += ',';
100 out += quote(keys[i]) + ':' + canonicalJson(value.get(keys[i].c_str()));
101 }
102 return out + '}';
103 }
104 return {};
105}
106
107bool normalize(const std::string& input, std::string& output) {
108 auto document = eve::json::Document::parse(input);
109 if (!document.valid()) return false;
110 output = canonicalJson(document.root());
111 return true;
112}
113
114bool parseU64(const eve::json::Value& value, uint64_t& output) {
115 if (!value.isString()) return false;
116 try {
117 size_t used = 0;
118 output = std::stoull(value.asString(), &used);
119 return used == value.asString().size();
120 } catch (...) {
121 return false;
122 }
123}
124
125eve::LogicalId statePatchSchema() {
126 const auto schema = eve::LogicalId::parse("statepatch:store");
127 if (!schema) std::terminate();
128 return *schema;
129}
130
131const eve::SnapshotMigrationChain& statePatchMigrations() {
132 static const eve::SnapshotMigrationChain chain = [] {
134 const auto registration =
135 result.add(statePatchSchema(), eve::SchemaVersion(0), eve::SchemaVersion(1),
137 const auto* object = payload.getIf<eve::Value::Object>();
138 if (!object)
140 eve::DiagnosticCode::ParseError, "state patch payload must be an object"));
141 eve::Value::Object migrated = *object;
142 if (!migrated.contains("version")) migrated.emplace("version", eve::Value(std::int64_t(1)));
143 return eve::Result<eve::Value>::success(eve::Value(std::move(migrated)));
144 });
145 if (!registration.ok()) std::terminate();
146 return result;
147 }();
148 return chain;
149}
150
151} // namespace
152
153Store::Store(eve::PersistentId instanceId) : instanceId_(instanceId) {}
154
155bool PatchBatch::set(const std::string& subject, const std::string& key, const std::string& jsonValue) {
156 std::string value;
157 const bool valid = normalize(jsonValue, value);
158 operations_.push_back({false, subject, key, value, std::nullopt, valid ? "" : "value must be valid JSON"});
159 return valid;
160}
161
162bool PatchBatch::setExpected(const std::string& subject, const std::string& key, const std::string& jsonValue,
163 const std::string& expectedJson) {
164 std::string value;
165 std::string expected;
166 const bool valueValid = normalize(jsonValue, value);
167 const bool expectedValid = normalize(expectedJson, expected);
168 std::string error;
169 if (!valueValid) error = "value must be valid JSON";
170 if (!expectedValid)
171 error = error.empty() ? "expected value must be valid JSON" : error + "; expected value must be valid JSON";
172 operations_.push_back({false, subject, key, value, expected, error});
173 return valueValid && expectedValid;
174}
175
176bool PatchBatch::remove(const std::string& subject, const std::string& key) {
177 operations_.push_back({true, subject, key, {}, std::nullopt, {}});
178 return true;
179}
180
181bool PatchBatch::removeExpected(const std::string& subject, const std::string& key, const std::string& expectedJson) {
182 std::string expected;
183 const bool valid = normalize(expectedJson, expected);
184 operations_.push_back({true, subject, key, {}, expected, valid ? "" : "expected value must be valid JSON"});
185 return valid;
186}
187
189 operations_.clear();
190 result_ = {};
191}
192
193int PatchBatch::size() const { return static_cast<int>(operations_.size()); }
194
195const PatchResult& PatchBatch::result() const { return result_; }
196
197eve::Result<PatchBatchHandleRef> Store::newBatch() { return batches_.emplace(std::make_unique<PatchBatch>()); }
198
202
204
206 if (!reference.isValid()) return false;
207 return batches_.isStale(reference);
208}
209
211 if (!batch) return false;
212 batch->result_ = {};
213 batch->result_.revisionBefore = revision_;
214 batch->result_.revisionAfter = revision_;
215 Values staged = values_;
216 for (size_t i = 0; i < batch->operations_.size(); ++i) {
217 const auto& operation = batch->operations_[i];
218 auto fail = [&](const std::string& code, const std::string& message) {
219 batch->result_.errors.push_back({static_cast<int>(i), operation.subject, operation.key, code, message});
220 };
221 if (operation.subject.empty()) fail("invalid_subject", "subject must not be empty");
222 if (operation.key.empty()) fail("invalid_key", "key must not be empty");
223 if (!operation.inputError.empty()) fail("invalid_json", operation.inputError);
224 if (!batch->result_.errors.empty() && batch->result_.errors.back().operationIndex == static_cast<int>(i))
225 continue;
226 const auto subjectIt = staged.find(operation.subject);
227 const auto valueIt = subjectIt == staged.end() ? std::map<std::string, Value>::const_iterator{}
228 : subjectIt->second.find(operation.key);
229 const bool exists = subjectIt != staged.end() && valueIt != subjectIt->second.end();
230 if (operation.expected && (!exists || valueIt->second.json != *operation.expected)) {
231 fail("conflict", "current value does not match expected JSON");
232 continue;
233 }
234 if (operation.remove) {
235 if (exists) {
236 staged[operation.subject].erase(operation.key);
237 if (staged[operation.subject].empty()) staged.erase(operation.subject);
238 }
239 } else {
240 staged[operation.subject][operation.key].json = operation.value;
241 }
242 }
243 if (!batch->result_.errors.empty()) return false;
244
245 struct Pending {
246 std::string subject;
247 std::string key;
248 std::string oldJson;
249 std::string newJson;
250 bool removed;
251 };
252 std::vector<Pending> changes;
253 std::set<std::pair<std::string, std::string>> keys;
254 for (const auto& [subject, entries] : values_)
255 for (const auto& [key, value] : entries) keys.emplace(subject, key);
256 for (const auto& [subject, entries] : staged)
257 for (const auto& [key, value] : entries) keys.emplace(subject, key);
258 for (const auto& [subject, key] : keys) {
259 const auto oldSubject = values_.find(subject);
260 const auto newSubject = staged.find(subject);
261 const auto oldValue =
262 oldSubject == values_.end() ? std::map<std::string, Value>::const_iterator{} : oldSubject->second.find(key);
263 const auto newValue =
264 newSubject == staged.end() ? std::map<std::string, Value>::const_iterator{} : newSubject->second.find(key);
265 const bool had = oldSubject != values_.end() && oldValue != oldSubject->second.end();
266 const bool has = newSubject != staged.end() && newValue != newSubject->second.end();
267 if (had != has || (had && oldValue->second.json != newValue->second.json))
268 changes.push_back({subject, key, had ? oldValue->second.json : "", has ? newValue->second.json : "", !has});
269 }
270 if (!changes.empty()) {
271 if (revision_ == std::numeric_limits<uint64_t>::max() ||
272 nextSequence_ > std::numeric_limits<uint64_t>::max() - changes.size()) {
273 batch->result_.errors.push_back({-1, {}, {}, "revision_exhausted", "revision or event sequence exhausted"});
274 return false;
275 }
276 ++revision_;
277 for (const auto& change : changes) {
278 if (!change.removed) staged[change.subject][change.key].revision = revision_;
279 dirty_.emplace(change.subject, change.key);
280 events_.push_back({nextSequence_++, revision_, change.subject, change.key, change.oldJson, change.newJson,
281 change.removed});
282 }
283 values_ = std::move(staged);
284 }
285 batch->result_.success = true;
286 batch->result_.changedCount = static_cast<int>(changes.size());
287 batch->result_.revisionAfter = revision_;
288 return true;
289}
290
291bool Store::has(const std::string& subject, const std::string& key) const {
292 const auto found = values_.find(subject);
293 return found != values_.end() && found->second.find(key) != found->second.end();
294}
295
296std::string Store::get(const std::string& subject, const std::string& key) const {
297 const auto found = values_.find(subject);
298 if (found == values_.end()) return {};
299 const auto value = found->second.find(key);
300 return value == found->second.end() ? std::string{} : value->second.json;
301}
302
303uint64_t Store::valueRevision(const std::string& subject, const std::string& key) const {
304 const auto found = values_.find(subject);
305 if (found == values_.end()) return 0;
306 const auto value = found->second.find(key);
307 return value == found->second.end() ? 0 : value->second.revision;
308}
309
310uint64_t Store::revision() const { return revision_; }
311
312int Store::querySubjects() {
313 query_.clear();
314 for (const auto& [subject, entries] : values_) query_.push_back(subject);
315 return static_cast<int>(query_.size());
316}
317
318int Store::queryKeys(const std::string& subject) {
319 query_.clear();
320 const auto found = values_.find(subject);
321 if (found != values_.end())
322 for (const auto& [key, value] : found->second) query_.push_back(key);
323 return static_cast<int>(query_.size());
324}
325
326std::string Store::queryAt(int index) const {
327 return index >= 0 && static_cast<size_t>(index) < query_.size() ? query_[static_cast<size_t>(index)]
328 : std::string{};
329}
330
331int Store::queryDirty() {
332 dirtyQuery_.assign(dirty_.begin(), dirty_.end());
333 return static_cast<int>(dirtyQuery_.size());
334}
335
336std::string Store::dirtySubjectAt(int index) const {
337 return index >= 0 && static_cast<size_t>(index) < dirtyQuery_.size() ? dirtyQuery_[static_cast<size_t>(index)].first
338 : std::string{};
339}
340
341std::string Store::dirtyKeyAt(int index) const {
342 return index >= 0 && static_cast<size_t>(index) < dirtyQuery_.size()
343 ? dirtyQuery_[static_cast<size_t>(index)].second
344 : std::string{};
345}
346
347void Store::clearDirty() {
348 dirty_.clear();
349 dirtyQuery_.clear();
350}
351
352int Store::eventCount() const { return static_cast<int>(events_.size()); }
353
354const ChangeEvent* Store::eventAt(int index) const {
355 return index >= 0 && static_cast<size_t>(index) < events_.size() ? &events_[static_cast<size_t>(index)] : nullptr;
356}
357
358void Store::clearEvents() { events_.clear(); }
359
360std::string Store::snapshotJson() const {
361 std::ostringstream out;
362 out << "{\"version\":1,\"revision\":" << quote(std::to_string(revision_))
363 << ",\"nextSequence\":" << quote(std::to_string(nextSequence_)) << ",\"values\":[";
364 bool first = true;
365 for (const auto& [subject, entries] : values_)
366 for (const auto& [key, value] : entries) {
367 if (!first) out << ',';
368 first = false;
369 out << "{\"subject\":" << quote(subject) << ",\"key\":" << quote(key)
370 << ",\"revision\":" << quote(std::to_string(value.revision)) << ",\"value\":" << value.json << '}';
371 }
372 out << "],\"dirty\":[";
373 first = true;
374 for (const auto& [subject, key] : dirty_) {
375 if (!first) out << ',';
376 first = false;
377 out << "[" << quote(subject) << ',' << quote(key) << ']';
378 }
379 return out.str() + "]}";
380}
381
382eve::Result<void> Store::restoreJson(const std::string& json) {
383 std::string parseError;
384 auto document = eve::json::Document::parse(json, &parseError);
385 if (!document.valid())
386 return eve::Result<void>::failure(eve::Diagnostic::error(eve::DiagnosticCode::ParseError, parseError.empty() ? "invalid state patch snapshot" : parseError,
387 "$", {}, "statepatch.store.restoreJson"));
388
389 const auto root = document.root();
390 if (!root.isObject()) return eve::Result<void>::failure(eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "snapshot root must be an object",
391 "$", {}, "statepatch.store.restoreJson"));
392
393 uint64_t restoredRevision = 0;
394 uint64_t restoredSequence = 0;
395 if (root.getInt("version") != 1) return eve::Result<void>::failure(eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "unsupported state patch snapshot version",
396 "$.version", {}, "statepatch.store.restoreJson"));
397 if (!parseU64(root.get("revision"), restoredRevision))
398 return eve::Result<void>::failure(eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "snapshot revision must be a decimal string",
399 "$.revision", {}, "statepatch.store.restoreJson"));
400 if (!parseU64(root.get("nextSequence"), restoredSequence) || restoredSequence == 0)
401 return eve::Result<void>::failure(eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "snapshot nextSequence must be a non-zero decimal string",
402 "$.nextSequence", {}, "statepatch.store.restoreJson"));
403
404 const auto values = root.get("values");
405 const auto dirty = root.get("dirty");
406 if (!values.isArray() || !dirty.isArray()) {
408 "$", {}, "statepatch.store.restoreJson"));
409 }
410 Values restoredValues;
411 std::set<std::pair<std::string, std::string>> restoredDirty;
412 for (size_t i = 0; i < values.size(); ++i) {
413 const auto item = values.at(i);
414 uint64_t valueRevision = 0;
415 if (!item.isObject() || item.getString("subject").empty() || item.getString("key").empty() ||
416 !parseU64(item.get("revision"), valueRevision) || valueRevision > restoredRevision || !item.get("value")) {
417 return eve::Result<void>::failure(eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "invalid value at index " + std::to_string(i),
418 "$.values[" + std::to_string(i) + "]", {}, "statepatch.store.restoreJson"));
419 }
420 const auto subject = item.getString("subject");
421 const auto key = item.getString("key");
422 if (restoredValues[subject].contains(key)) {
424 "$.values[" + std::to_string(i) + "]", {}, "statepatch.store.restoreJson"));
425 }
426 restoredValues[subject][key] = {canonicalJson(item.get("value")), valueRevision};
427 }
428 for (size_t i = 0; i < dirty.size(); ++i) {
429 const auto item = dirty.at(i);
430 if (!item.isArray() || item.size() != 2 || !item.at(0).isString() || !item.at(1).isString() ||
431 item.at(0).asString().empty() || item.at(1).asString().empty()) {
432 return eve::Result<void>::failure(eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "invalid dirty key at index " + std::to_string(i),
433 "$.dirty[" + std::to_string(i) + "]", {}, "statepatch.store.restoreJson"));
434 }
435 restoredDirty.emplace(item.at(0).asString(), item.at(1).asString());
436 }
437 values_ = std::move(restoredValues);
438 dirty_ = std::move(restoredDirty);
439 revision_ = restoredRevision;
440 nextSequence_ = restoredSequence;
441 events_.clear();
442 query_.clear();
443 dirtyQuery_.clear();
444 batches_.clear();
446}
447
448eve::Result<eve::SnapshotEnvelope> Store::snapshot(const eve::SnapshotHashProvider& hashProvider) const {
449 auto payload = eve::Value::fromJson(snapshotJson());
451 return eve::makeSnapshotEnvelope("statepatch.store", statePatchSchema(), eve::SchemaVersion(1), instanceId_,
452 eve::Revision(revision_), tick_, std::move(payload).takeValue(), hashProvider);
453}
454
455eve::Result<void> Store::restoreSnapshot(const eve::SnapshotEnvelope& source,
456 const eve::SnapshotHashProvider& hashProvider) {
457 if (source.type != "statepatch.store" || source.schema != statePatchSchema())
459 "snapshot does not belong to statepatch::Store"));
460 if (!instanceId_.isNil() && source.instanceId != instanceId_)
462 eve::DiagnosticCode::Conflict, "snapshot instanceId does not match statepatch::Store"));
463 auto migrated = statePatchMigrations().migrate(source, eve::SchemaVersion(1), hashProvider);
464 if (!migrated.ok()) return eve::Result<void>::failure(migrated.status());
465 const auto& candidateEnvelope = migrated.value();
466 auto metadata = eve::validateSnapshotPayloadMetadata(candidateEnvelope.payload, candidateEnvelope.revision,
467 candidateEnvelope.tick);
468 if (!metadata.ok()) return eve::Result<void>::failure(metadata.status());
469 auto payload = candidateEnvelope.payload.toJson();
470 if (!payload.ok()) return eve::Result<void>::failure(payload.status());
471
472 Store candidate(instanceId_);
473 auto restored = candidate.restoreJson(std::move(payload).takeValue());
474 if (!restored.ok()) return eve::Result<void>::failure(restored.status());
475 if (candidate.revision_ != candidateEnvelope.revision.value())
477 eve::DiagnosticCode::Conflict, "state patch payload revision disagrees with snapshot envelope"));
478 candidate.instanceId_ = candidateEnvelope.instanceId;
479 candidate.tick_ = candidateEnvelope.tick;
480 *this = std::move(candidate);
482}
483
484eve::Result<std::string> Store::snapshotEnvelopeJson(const eve::SnapshotHashProvider& hashProvider) const {
485 auto value = snapshot(hashProvider);
486 if (!value.ok()) return eve::Result<std::string>::failure(value.status());
487 return std::move(value).andThen(
488 [](eve::SnapshotEnvelope&& envelope) { return eve::serializeSnapshotEnvelope(envelope); });
489}
490
491eve::Result<void> Store::restoreSnapshotJson(std::string_view json, const eve::SnapshotHashProvider& hashProvider) {
492 auto source = eve::parseSnapshotEnvelope(json, hashProvider);
493 if (!source.ok()) return eve::Result<void>::failure(source.status());
494 return restoreSnapshot(std::move(source).takeValue(), hashProvider);
495}
496
497void Store::copyTransactionStateFrom(const Store& source) {
498 values_ = source.values_;
499 revision_ = source.revision_;
500 nextSequence_ = source.nextSequence_;
501 dirty_ = source.dirty_;
502 events_ = source.events_;
503 query_.clear();
504 dirtyQuery_.clear();
505 batches_.clear();
506}
507
508void Store::swapTransactionState(Store& other) noexcept {
509 values_.swap(other.values_);
510 std::swap(revision_, other.revision_);
511 std::swap(nextSequence_, other.nextSequence_);
512 dirty_.swap(other.dirty_);
513 events_.swap(other.events_);
514 query_.clear();
515 dirtyQuery_.clear();
516 other.query_.clear();
517 other.dirtyQuery_.clear();
518}
519
520bool Store::transactionStateEquals(const Store& other) const {
521 if (snapshotJson() != other.snapshotJson() || events_.size() != other.events_.size()) return false;
522 for (size_t i = 0; i < events_.size(); ++i) {
523 const auto& left = events_[i];
524 const auto& right = other.events_[i];
525 if (left.sequence != right.sequence || left.revision != right.revision || left.subject != right.subject ||
526 left.key != right.key || left.oldJson != right.oldJson || left.newJson != right.newJson ||
527 left.removed != right.removed)
528 return false;
529 }
530 return true;
531}
532
533namespace {
534
535eve::Result<void> patchParticipantFailure(const PatchResult& result) {
538 std::vector<eve::Diagnostic> diagnostics;
539 for (const auto& error : result.errors) {
540 if (error.code == "conflict") {
541 statusCode = eve::StatusCode::Conflict;
542 diagnosticCode = eve::DiagnosticCode::Conflict;
543 } else if (statusCode == eve::StatusCode::Failed && error.code.starts_with("invalid_")) {
544 statusCode = eve::StatusCode::Rejected;
546 }
547 diagnostics.push_back(eve::Diagnostic::error(
548 diagnosticCode, error.message.empty() ? "state patch batch was rejected" : error.message,
549 "statepatch[" + std::to_string(error.operationIndex) + "]"));
550 }
551 if (diagnostics.empty())
553 "state patch batch could not be prepared", "statepatch"));
554 return eve::Result<void>::failure(eve::Status(statusCode, std::move(diagnostics)));
555}
556
557} // namespace
558
559bool StoreTransactionParticipant::contextMatches(const transaction::TransactionContext& context) const noexcept {
560 return context.transactionId() == transactionId_ && context.correlationId() == correlationId_ &&
561 context.causationId() == causationId_;
562}
563
564eve::Result<void> StoreTransactionParticipant::lifecycleFailure(std::string_view operation) const {
567 "statepatch participant cannot " + std::string(operation) + " in its current lifecycle phase",
568 "statepatch.lifecycle"));
569}
570
571eve::Result<void> StoreTransactionParticipant::contextFailure(const transaction::TransactionContext& context) const {
572 (void)context;
574 eve::DiagnosticCode::Conflict, "transaction context changed between participant lifecycle calls",
575 "statepatch.context"));
576}
577
578eve::Result<void> StoreTransactionParticipant::prepare(const transaction::TransactionContext& context) {
579 if (phase_ != Phase::Idle) return lifecycleFailure("prepare");
580 if (context.transactionId().empty())
582 "transaction id must not be empty", "transactionId"));
583
584 transactionId_ = context.transactionId();
585 correlationId_ = context.correlationId();
586 causationId_ = context.causationId();
587 before_ = std::make_unique<Store>();
588 before_->copyTransactionStateFrom(store_);
589 prepared_ = std::make_unique<Store>();
590 prepared_->copyTransactionStateFrom(*before_);
591
592 if (!prepared_->commit(&batch_)) {
593 auto result = patchParticipantFailure(batch_.result());
594 before_.reset();
595 prepared_.reset();
596 phase_ = Phase::Failed;
597 return result;
598 }
599
600 expectedAfter_ = std::make_unique<Store>();
601 expectedAfter_->copyTransactionStateFrom(*prepared_);
602 phase_ = Phase::Prepared;
604}
605
606eve::Result<void> StoreTransactionParticipant::commit(const transaction::TransactionContext& context) {
607 if (phase_ != Phase::Prepared) return lifecycleFailure("commit");
608 if (!contextMatches(context)) return contextFailure(context);
609 if (!before_ || !prepared_ || !expectedAfter_ || !store_.transactionStateEquals(*before_))
611 eve::DiagnosticCode::Conflict, "state patch store changed after prepare", "statepatch.store"));
612
613 store_.swapTransactionState(*prepared_);
614 phase_ = Phase::Committed;
616}
617
618eve::Result<void> StoreTransactionParticipant::rollback(const transaction::TransactionContext& context) {
619 if (phase_ != Phase::Prepared) return lifecycleFailure("rollback");
620 if (!contextMatches(context)) return contextFailure(context);
621 before_.reset();
622 prepared_.reset();
623 expectedAfter_.reset();
624 phase_ = Phase::RolledBack;
626}
627
628eve::Result<void> StoreTransactionParticipant::compensate(const transaction::TransactionContext& context) {
629 if (phase_ != Phase::Committed) return lifecycleFailure("compensate");
630 if (!contextMatches(context)) return contextFailure(context);
631 if (!prepared_ || !expectedAfter_ || !store_.transactionStateEquals(*expectedAfter_))
633 eve::DiagnosticCode::Conflict, "state patch store changed before compensation", "statepatch.store"));
634
635 store_.swapTransactionState(*prepared_);
636 expectedAfter_.reset();
637 phase_ = Phase::Compensated;
639}
640
641StatePatch::~StatePatch() {
642 for (auto& [store, adapter] : worldAdapters_) {
643 (void)store;
644 if (!adapter) continue;
645 eve::cap::removeListener<eve::IStateQuery>(static_cast<eve::IStateQuery*>(adapter.get()));
646 eve::cap::removeListener<eve::IStateMutation>(static_cast<eve::IStateMutation*>(adapter.get()));
647 }
648 worldAdapters_.clear();
649}
650
651void StatePatch::attachWorldAdapter(Store& store) {
652 if (worldAdapters_.contains(&store)) return;
653 auto adapter = std::make_unique<StatePatchStateAdapter>(store);
654 eve::cap::addListener<eve::IStateQuery>(static_cast<eve::IStateQuery*>(adapter.get()));
655 eve::cap::addListener<eve::IStateMutation>(static_cast<eve::IStateMutation*>(adapter.get()));
656 worldAdapters_.emplace(&store, std::move(adapter));
657}
658
659void StatePatch::detachWorldAdapter(Store& store) {
660 const auto found = worldAdapters_.find(&store);
661 if (found == worldAdapters_.end()) return;
662 eve::cap::removeListener<eve::IStateQuery>(static_cast<eve::IStateQuery*>(found->second.get()));
663 eve::cap::removeListener<eve::IStateMutation>(static_cast<eve::IStateMutation*>(found->second.get()));
664 worldAdapters_.erase(found);
665}
666
667eve::Result<StateStoreHandleRef> StatePatch::newStore() {
668 StatePatch* module = StatePatch::create();
669 auto created = module->stores_.emplace(std::make_unique<Store>());
670 if (!created) return created;
671 auto ref = std::move(created).takeValue();
672 auto store = module->stores_.resolve(ref);
673 if (store.isBound()) module->attachWorldAdapter(*store);
675}
676
678 StatePatch* module = ModuleManager::getInstance<StatePatch>("StatePatch");
679 if (!module) return {};
680 auto store = module->stores_.resolve(reference.store);
681 if (!store.isBound()) return {};
682 return store->resolveBatch(reference.batch);
683}
684
686 StatePatch* module = ModuleManager::getInstance<StatePatch>("StatePatch");
687 if (!module)
689 "StatePatch module is no longer loaded", "batch", {},
690 "statepatch.squirrel"));
691 auto store = module->stores_.resolve(reference.store);
692 if (!store.isBound())
694 eve::DiagnosticCode::StaleHandle, "owning StatePatch store is stale", "store", {}, "statepatch.squirrel"));
695 return store->releaseBatch(reference.batch);
696}
697
698bool StatePatch::isBatchStale(StateBatchHandleRef reference) noexcept {
699 if (!reference.isValid()) return false;
700 StatePatch* module = ModuleManager::getInstance<StatePatch>("StatePatch");
701 if (!module) return true;
702 auto store = module->stores_.resolve(reference.store);
703 return !store.isBound() || store->isBatchStale(reference.batch);
704}
705
707 StatePatch* module = ModuleManager::getInstance<StatePatch>("StatePatch");
708 if (!module) return {};
709 return module->stores_.resolve(reference);
710}
711
713 StatePatch* module = ModuleManager::getInstance<StatePatch>("StatePatch");
714 if (!module)
716 "StatePatch module is no longer loaded", "store", {},
717 "statepatch.squirrel"));
718 auto store = module->stores_.resolve(reference);
719 if (store.isBound()) module->detachWorldAdapter(*store);
720 return module->stores_.erase(reference);
721}
722
723bool StatePatch::isStale(StateStoreHandleRef reference) noexcept {
724 if (!reference.isValid()) return false;
725 StatePatch* module = ModuleManager::getInstance<StatePatch>("StatePatch");
726 return !module || module->stores_.isStale(reference);
727}
728
730
731void StatePatch::expose(ssq::Table& table) {
732 const HSQUIRRELVM vm = table.getHandle();
733 auto error = table.addClass<PatchError>("PatchError", std::function<PatchError*()>([] { return nullptr; }), false);
734 error.addFunc("getOperationIndex", [](PatchError* e) { return e ? e->operationIndex : -1; });
735 error.addFunc("getSubject", [](PatchError* e) { return e ? e->subject : std::string{}; });
736 error.addFunc("getKey", [](PatchError* e) { return e ? e->key : std::string{}; });
737 error.addFunc("getCode", [](PatchError* e) { return e ? e->code : std::string{}; });
738 error.addFunc("getMessage", [](PatchError* e) { return e ? e->message : std::string{}; });
739
740 auto result =
741 table.addClass<PatchResult>("PatchResult", std::function<PatchResult*()>([] { return nullptr; }), false);
742 result.addFunc("isSuccess", [](PatchResult* r) { return r && r->success; });
743 result.addFunc("getChangedCount", [](PatchResult* r) { return r ? r->changedCount : 0; });
744 result.addFunc("getRevisionBefore", [](PatchResult* r) { return r ? static_cast<int64_t>(r->revisionBefore) : 0; });
745 result.addFunc("getRevisionAfter", [](PatchResult* r) { return r ? static_cast<int64_t>(r->revisionAfter) : 0; });
746 result.addFunc("errorCount", [](PatchResult* r) { return r ? static_cast<int>(r->errors.size()) : 0; });
747 result.addFunc("errorAt", [](PatchResult* r, int index) -> PatchError* {
748 return r && index >= 0 && static_cast<size_t>(index) < r->errors.size() ? &r->errors[static_cast<size_t>(index)]
749 : nullptr;
750 });
751
752 auto event =
753 table.addClass<ChangeEvent>("StateChangeEvent", std::function<ChangeEvent*()>([] { return nullptr; }), false);
754 event.addFunc("getSequence", [](ChangeEvent* e) { return e ? static_cast<int64_t>(e->sequence) : 0; });
755 event.addFunc("getRevision", [](ChangeEvent* e) { return e ? static_cast<int64_t>(e->revision) : 0; });
756 event.addFunc("getSubject", [](ChangeEvent* e) { return e ? e->subject : std::string{}; });
757 event.addFunc("getKey", [](ChangeEvent* e) { return e ? e->key : std::string{}; });
758 event.addFunc("getOldJson", [](ChangeEvent* e) { return e ? e->oldJson : std::string{}; });
759 event.addFunc("getNewJson", [](ChangeEvent* e) { return e ? e->newJson : std::string{}; });
760 event.addFunc("isRemoved", [](ChangeEvent* e) { return e && e->removed; });
761
762 auto batch = table.addClass<PatchBatch>("PatchBatch", std::function<PatchBatch*()>([] { return nullptr; }), false);
763 batch.addFunc("set", &PatchBatch::set);
764 batch.addFunc("setExpected", &PatchBatch::setExpected);
765 batch.addFunc("remove", &PatchBatch::remove);
766 batch.addFunc("removeExpected", &PatchBatch::removeExpected);
767 batch.addFunc("clear", &PatchBatch::clear);
768 batch.addFunc("size", &PatchBatch::size);
769 batch.addFunc("result",
770 [](PatchBatch* b) -> PatchResult* { return b ? const_cast<PatchResult*>(&b->result()) : nullptr; });
771
772 auto ownedBatch = table.addClass<ScriptStateBatch>(
773 "StateBatchProxy", std::function<ScriptStateBatch*()>([] { return nullptr; }), true);
774 ownedBatch.addFunc("ownership", [](ScriptStateBatch*) { return std::string("owned"); });
775 ownedBatch.addFunc("ownerEpoch", [](ScriptStateBatch* value) {
776 return value ? static_cast<int64_t>(value->reference.ownerEpoch) : int64_t{0};
777 });
778 ownedBatch.addFunc("handle", [](ScriptStateBatch* value) {
779 return value ? static_cast<int64_t>(value->reference.packed()) : int64_t{0};
780 });
781 ownedBatch.addFunc("isStale",
782 [](ScriptStateBatch* value) { return !value || StatePatch::isBatchStale(value->reference); });
783 ownedBatch.addFunc("release", [vm](ScriptStateBatch* value) {
784 if (!value)
787 "owned state batch proxy must not be null",
788 "batch", {}, "statepatch.squirrel")));
789 return eve::script::projectResult(vm, StatePatch::releaseBatch(value->reference));
790 });
791 ownedBatch.addFunc("set", [vm](ScriptStateBatch* value, const std::string& subject, const std::string& key,
792 const std::string& jsonValue) {
793 auto view = value ? StatePatch::resolveBatch(value->reference) : eve::script::Borrowed<PatchBatch>();
794 if (!view.isBound())
796 vm,
798 eve::DiagnosticCode::StaleHandle, std::string("owned state-patch ") + "batch" + " handle is stale",
799 "batch", {}, "statepatch.squirrel")),
800 [](bool value) { return eve::Value(value); });
802 [](bool value) { return eve::Value(value); });
803 });
804 ownedBatch.addFunc("remove", [vm](ScriptStateBatch* value, const std::string& subject, const std::string& key) {
805 auto view = value ? StatePatch::resolveBatch(value->reference) : eve::script::Borrowed<PatchBatch>();
806 if (!view.isBound())
808 vm,
810 eve::DiagnosticCode::StaleHandle, std::string("owned state-patch ") + "batch" + " handle is stale",
811 "batch", {}, "statepatch.squirrel")),
812 [](bool value) { return eve::Value(value); });
814 [](bool value) { return eve::Value(value); });
815 });
816 ownedBatch.addFunc("setExpected", [vm](ScriptStateBatch* value, const std::string& subject, const std::string& key,
817 const std::string& jsonValue, const std::string& expectedJson) {
818 auto view = value ? StatePatch::resolveBatch(value->reference) : eve::script::Borrowed<PatchBatch>();
819 if (!view.isBound())
821 vm,
823 eve::DiagnosticCode::StaleHandle, std::string("owned state-patch ") + "batch" + " handle is stale",
824 "batch", {}, "statepatch.squirrel")),
825 [](bool value) { return eve::Value(value); });
827 vm, eve::Result<bool>::success(view->setExpected(subject, key, jsonValue, expectedJson)),
828 [](bool result) { return eve::Value(result); });
829 });
830 ownedBatch.addFunc("removeExpected", [vm](ScriptStateBatch* value, const std::string& subject,
831 const std::string& key, const std::string& expectedJson) {
832 auto view = value ? StatePatch::resolveBatch(value->reference) : eve::script::Borrowed<PatchBatch>();
833 if (!view.isBound())
835 vm,
837 eve::DiagnosticCode::StaleHandle, std::string("owned state-patch ") + "batch" + " handle is stale",
838 "batch", {}, "statepatch.squirrel")),
839 [](bool value) { return eve::Value(value); });
841 eve::Result<bool>::success(view->removeExpected(subject, key, expectedJson)),
842 [](bool result) { return eve::Value(result); });
843 });
844 ownedBatch.addFunc("clear", [](ScriptStateBatch* value) {
845 auto view = value ? StatePatch::resolveBatch(value->reference) : eve::script::Borrowed<PatchBatch>();
846 if (view.isBound()) view->clear();
847 });
848 ownedBatch.addFunc("size", [](ScriptStateBatch* value) {
849 auto view = value ? StatePatch::resolveBatch(value->reference) : eve::script::Borrowed<PatchBatch>();
850 return view.isBound() ? view->size() : 0;
851 });
852 ownedBatch.addFunc("result", [](ScriptStateBatch* value) -> PatchResult* {
853 auto view = value ? StatePatch::resolveBatch(value->reference) : eve::script::Borrowed<PatchBatch>();
854 return view.isBound() ? const_cast<PatchResult*>(&view->result()) : nullptr;
855 });
856
857 auto store = table.addClass<Store>("StateStore", std::function<Store*()>([] { return nullptr; }), false);
858 store.addFunc("commit", &Store::commit);
859 store.addFunc("has", &Store::has);
860 store.addFunc("get", &Store::get);
861 store.addFunc("valueRevision", [](Store* s, const std::string& subject, const std::string& key) {
862 return s ? static_cast<int64_t>(s->valueRevision(subject, key)) : 0;
863 });
864 store.addFunc("revision", [](Store* s) { return s ? static_cast<int64_t>(s->revision()) : 0; });
865 store.addFunc("querySubjects", &Store::querySubjects);
866 store.addFunc("queryKeys", &Store::queryKeys);
867 store.addFunc("queryAt", &Store::queryAt);
868 store.addFunc("queryDirty", &Store::queryDirty);
869 store.addFunc("dirtySubjectAt", &Store::dirtySubjectAt);
870 store.addFunc("dirtyKeyAt", &Store::dirtyKeyAt);
871 store.addFunc("clearDirty", &Store::clearDirty);
872 store.addFunc("eventCount", &Store::eventCount);
873 store.addFunc("eventAt", [](Store* s, int index) -> ChangeEvent* {
874 return s ? const_cast<ChangeEvent*>(s->eventAt(index)) : nullptr;
875 });
876 store.addFunc("clearEvents", &Store::clearEvents);
877 store.addFunc("snapshotJson", &Store::snapshotJson);
878 store.addFunc("restoreJson", [vm](Store* value, const std::string& json) {
879 if (!value)
882 "state store must not be null", "store", {},
883 "statepatch.squirrel")));
884 return eve::script::projectResult(vm, value->restoreJson(json));
885 });
886
887 auto ownedStore = table.addClass<ScriptStateStore>(
888 "StateStoreProxy", std::function<ScriptStateStore*()>([] { return nullptr; }), true);
889 ownedStore.addFunc("ownership", [](ScriptStateStore*) { return std::string("owned"); });
890 ownedStore.addFunc("ownerEpoch", [](ScriptStateStore* value) {
891 return value ? static_cast<int64_t>(value->reference.ownerEpoch) : int64_t{0};
892 });
893 ownedStore.addFunc("handle", [](ScriptStateStore* value) {
894 return value ? static_cast<int64_t>(value->reference.packed()) : int64_t{0};
895 });
896 ownedStore.addFunc("isStale",
897 [](ScriptStateStore* value) { return !value || StatePatch::isStale(value->reference); });
898 ownedStore.addFunc("release", [vm](ScriptStateStore* value) {
899 if (!value)
902 "owned state store proxy must not be null",
903 "store", {}, "statepatch.squirrel")));
904 return eve::script::projectResult(vm, StatePatch::release(value->reference));
905 });
906 ownedStore.addFunc("commit", [](ScriptStateStore* value, ScriptStateBatch* batch) {
907 if (!value || !batch || !(batch->reference.store == value->reference)) return false;
908 auto store = StatePatch::resolve(value->reference);
909 auto staged = StatePatch::resolveBatch(batch->reference);
910 return store.isBound() && staged.isBound() && store->commit(staged.get());
911 });
912 ownedStore.addFunc("revision", [](ScriptStateStore* value) {
913 auto view = value ? StatePatch::resolve(value->reference) : eve::script::Borrowed<Store>();
914 return view.isBound() ? static_cast<int64_t>(view->revision()) : int64_t{0};
915 });
916 ownedStore.addFunc("has", [](ScriptStateStore* value, const std::string& subject, const std::string& key) {
917 auto view = value ? StatePatch::resolve(value->reference) : eve::script::Borrowed<Store>();
918 return view.isBound() && view->has(subject, key);
919 });
920 ownedStore.addFunc("get", [](ScriptStateStore* value, const std::string& subject, const std::string& key) {
921 auto view = value ? StatePatch::resolve(value->reference) : eve::script::Borrowed<Store>();
922 return view.isBound() ? view->get(subject, key) : std::string{};
923 });
924 ownedStore.addFunc("valueRevision",
925 [](ScriptStateStore* value, const std::string& subject, const std::string& key) {
926 auto view = value ? StatePatch::resolve(value->reference) : eve::script::Borrowed<Store>();
927 return view.isBound() ? static_cast<int64_t>(view->valueRevision(subject, key)) : int64_t{0};
928 });
929 ownedStore.addFunc("querySubjects", [](ScriptStateStore* value) {
930 auto view = value ? StatePatch::resolve(value->reference) : eve::script::Borrowed<Store>();
931 return view.isBound() ? view->querySubjects() : 0;
932 });
933 ownedStore.addFunc("queryKeys", [](ScriptStateStore* value, const std::string& subject) {
934 auto view = value ? StatePatch::resolve(value->reference) : eve::script::Borrowed<Store>();
935 return view.isBound() ? view->queryKeys(subject) : 0;
936 });
937 ownedStore.addFunc("queryAt", [](ScriptStateStore* value, int index) {
938 auto view = value ? StatePatch::resolve(value->reference) : eve::script::Borrowed<Store>();
939 return view.isBound() ? view->queryAt(index) : std::string{};
940 });
941 ownedStore.addFunc("queryDirty", [](ScriptStateStore* value) {
942 auto view = value ? StatePatch::resolve(value->reference) : eve::script::Borrowed<Store>();
943 return view.isBound() ? view->queryDirty() : 0;
944 });
945 ownedStore.addFunc("dirtySubjectAt", [](ScriptStateStore* value, int index) {
946 auto view = value ? StatePatch::resolve(value->reference) : eve::script::Borrowed<Store>();
947 return view.isBound() ? view->dirtySubjectAt(index) : std::string{};
948 });
949 ownedStore.addFunc("dirtyKeyAt", [](ScriptStateStore* value, int index) {
950 auto view = value ? StatePatch::resolve(value->reference) : eve::script::Borrowed<Store>();
951 return view.isBound() ? view->dirtyKeyAt(index) : std::string{};
952 });
953 ownedStore.addFunc("clearDirty", [](ScriptStateStore* value) {
954 auto view = value ? StatePatch::resolve(value->reference) : eve::script::Borrowed<Store>();
955 if (view.isBound()) view->clearDirty();
956 });
957 ownedStore.addFunc("eventCount", [](ScriptStateStore* value) {
958 auto view = value ? StatePatch::resolve(value->reference) : eve::script::Borrowed<Store>();
959 return view.isBound() ? view->eventCount() : 0;
960 });
961 ownedStore.addFunc("eventAt", [](ScriptStateStore* value, int index) -> ChangeEvent* {
962 auto view = value ? StatePatch::resolve(value->reference) : eve::script::Borrowed<Store>();
963 return view.isBound() ? const_cast<ChangeEvent*>(view->eventAt(index)) : nullptr;
964 });
965 ownedStore.addFunc("clearEvents", [](ScriptStateStore* value) {
966 auto view = value ? StatePatch::resolve(value->reference) : eve::script::Borrowed<Store>();
967 if (view.isBound()) view->clearEvents();
968 });
969 ownedStore.addFunc("snapshotJson", [](ScriptStateStore* value) {
970 auto view = value ? StatePatch::resolve(value->reference) : eve::script::Borrowed<Store>();
971 return view.isBound() ? view->snapshotJson() : std::string{};
972 });
973 ownedStore.addFunc("restoreJson", [vm](ScriptStateStore* value, const std::string& json) {
974 auto view = value ? StatePatch::resolve(value->reference) : eve::script::Borrowed<Store>();
975 if (!view.isBound())
978 "owned state-patch store handle is stale",
979 "store", {}, "statepatch.squirrel")));
980 return eve::script::projectResult(vm, view->restoreJson(json));
981 });
982 ownedStore.addFunc("newBatch", [vm](ScriptStateStore* value) -> ssq::Table {
983 if (!value)
986 "owned state store proxy must not be null",
987 "store", {}, "statepatch.squirrel"))
988 .status());
989 auto store = StatePatch::resolve(value->reference);
990 if (!store.isBound())
993 "owned state-patch store handle is stale",
994 "store", {}, "statepatch.squirrel"))
995 .status());
996 auto batch = store->newBatch();
997 if (!batch) return eve::script::projectStatusResult(vm, batch.status());
998 const auto batchRef = std::move(batch).takeValue();
999 StateBatchHandleRef reference{value->reference, batchRef, batchRef.ownerEpoch};
1000 return makeOwnedProxy<StateBatchHandleRef, ScriptStateBatch>(
1002 [](StateBatchHandleRef ref) { return StatePatch::releaseBatch(ref); });
1003 });
1004
1005 auto cls = table.addClass(name, StatePatch::create, false);
1006 expose(cls);
1007}
1008
1009void StatePatch::expose(ssq::Class& cls) {
1010 cls.addFunc("getName", &StatePatch::getName);
1011 cls.addFunc("newStore", [vm = cls.getHandle()](StatePatch*) -> ssq::Table {
1012 return makeOwnedProxy<StateStoreHandleRef, ScriptStateStore>(
1013 vm, StatePatch::newStore(), [](StateStoreHandleRef ref) { return StatePatch::release(ref); });
1014 });
1015}
1016
1017} // namespace eve::statepatch
ActionParameterOperation operation
double value
Value::Object payload
int root
Definition AnimSmr.cpp:119
int subject
Definition AnimSmr.cpp:163
std::string output
ActiveSource owned
AuthorityStoreHandleRef reference
Definition Authority.cpp:24
const std::string & s
std::unordered_map< std::string, QuestRuntime > entries
struct SQVM * HSQUIRRELVM
HSQUIRRELVM vm
Definition ECS.cpp:20
HSQOBJECT cls
Definition ECS.cpp:21
std::map< std::string, Var > values
EvpackChunkInput input
Definition Evpack.cpp:170
std::string message
DiagnosticCode code
std::uint32_t key
wgpu::PopErrorScopeStatus status
double r
HexVec3 left
HexVec3 right
std::int32_t second
std::int32_t c
std::int32_t first
char quote
bool exists
std::string name
bool valid
MeleePoint3 b
Definition MeleeHit.cpp:41
#define Module_IMPL(ModuleName, newExpr)
Definition Module.h:26
std::string error
Definition Package.cpp:60
bool dirty
std::string script
Definition PlayHost.cpp:116
std::function< eve::Result< void >()> release
Definition Procgen.cpp:86
glm::mat4 view
bool found
int created
int removed
The single Squirrel projection for common Result, Status and Value.
std::string_view adapter
Consumer-owned query and mutation contracts for authoritative world state.
Json object
uint32_t index
const UnitySourceAsset & source
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
Human-readable, scoped identifier in the form namespace:name.
Definition Identity.h:411
static std::optional< LogicalId > parse(std::string_view text)
Parses a scoped logical name.
Definition Identity.cpp:36
void ignore(std::string_view reason={}) const noexcept
Explicitly discard this result after documenting the reason.
Definition Result.h:537
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
One directed payload migration step.
Definition Snapshot.h:145
Result< void > add(LogicalId schema, SchemaVersion from, SchemaVersion to, Migration migration)
Register one schema-local migration edge.
Definition Snapshot.cpp:196
Structured status and zero or more diagnostics for an operation.
Definition Status.h:68
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
static Result< Value > fromJson(std::string_view json)
Parse one strict JSON value into an owning Value.
Definition Value.cpp:57
std::map< std::string, Value > Object
Definition Value.h:34
Id128 public API.
Definition Identity.h:112
static Document parse(const std::string &text, std::string *error=nullptr)
Parse.
Definition Json.cpp:581
EVENGINE_API_FOUNDATION public API.
Definition Json.h:38
Non-owning C++ observation with no implicit lifetime extension.
All-or-nothing mutation boundary owned by the consumer.
Read-only world-state boundary owned by consumers such as Dialogue.
Definition StateAccess.h:79
Ordered collection of set and remove operations committed atomically.
Definition StatePatch.h:86
void clear()
Removes every operation and resets the latest result.
bool setExpected(const std::string &subject, const std::string &key, const std::string &jsonValue, const std::string &expectedJson)
Appends a set operation guarded by an expected current JSON value.
bool set(const std::string &subject, const std::string &key, const std::string &jsonValue)
Appends an unconditional set operation.
bool remove(const std::string &subject, const std::string &key)
Appends an unconditional remove operation.
const PatchResult & result() const
Returns the result of the most recent commit attempt.
int size() const
Returns the number of queued operations.
bool removeExpected(const std::string &subject, const std::string &key, const std::string &expectedJson)
Appends a remove operation guarded by an expected current JSON value.
Script module factory for generic state patch stores.
Definition StatePatch.h:272
static eve::Result< void > releaseBatch(StateBatchHandleRef reference)
Releases a store-qualified patch batch.
static eve::script::Borrowed< PatchBatch > resolveBatch(StateBatchHandleRef reference) noexcept
Resolves a store-qualified batch as a non-owning observation.
static bool isBatchStale(StateBatchHandleRef reference) noexcept
Reports whether a store-qualified batch reference is stale.
static eve::Result< void > release(StateStoreHandleRef reference)
Releases a module-owned state store.
Deterministic subject-and-key JSON value store with atomic patching.
Definition StatePatch.h:119
bool has(const std::string &subject, const std::string &key) const
Returns whether a subject and key currently exist.
bool commit(PatchBatch *batch)
Validates and atomically commits a batch.
eve::Result< void > releaseBatch(PatchBatchHandleRef reference)
Releases a store-owned patch batch.
eve::Result< void > restoreJson(const std::string &json)
Transactionally restores a snapshot produced by snapshotJson().
eve::script::Borrowed< PatchBatch > resolveBatch(PatchBatchHandleRef reference) noexcept
Resolves a live batch as a non-owning observation.
Store(eve::PersistentId instanceId={})
Creates an empty store with an optional persistent identity.
bool isBatchStale(PatchBatchHandleRef reference) const noexcept
Reports whether a batch reference is stale for this Store.
eve::Result< PatchBatchHandleRef > newBatch()
Allocates a store-owned empty patch batch and returns its handle.
Immutable context shared by every participant of one transaction.
Definition Transaction.h:30
Vec2 normalize(const Vec2 &a)
Normalize.
Definition UrbanTypes.h:44
std::unordered_map< std::string, SkillDefinition > & table()
Definition Skill.cpp:65
ssq::Table projectStatusResult(HSQUIRRELVM vm, const Status &status)
Project a checked native status that carries no payload.
ssq::Table projectResult(HSQUIRRELVM vm, Result< void > &&result)
Consume and project a void native Result using the common schema.
eve::script::RuntimeHandleRef< StateStoreHandleTag > StateStoreHandleRef
Generation- and module-epoch-qualified state store reference.
Definition StatePatch.h:28
Build metadata (engine git commit, build time, third-party version).
Definition Build.cpp:16
DiagnosticCode
Stable machine-readable diagnostic codes.
Definition Diagnostic.h:47
Result< void > validateSnapshotPayloadMetadata(const Value &payload, Revision revision, SimulationTick tick)
Validate optional payload copies of envelope revision and tick.
Definition Snapshot.cpp:80
StatusCode
Stable outcome category for an operation.
Definition Status.h:27
Result< SnapshotEnvelope > parseSnapshotEnvelope(std::string_view json, const SnapshotHashProvider &hashProvider)
Parse and verify an envelope from canonical or compatible JSON text.
Definition Snapshot.cpp:190
Result< std::string > serializeSnapshotEnvelope(const SnapshotEnvelope &snapshot)
Serialize an envelope as deterministic compact JSON.
Definition Snapshot.cpp:184
Result< SnapshotEnvelope > makeSnapshotEnvelope(std::string type, LogicalId schema, SchemaVersion schemaVersion, PersistentId instanceId, Revision revision, SimulationTick tick, Value payload, const SnapshotHashProvider &hashProvider)
Construct and seal a snapshot envelope.
Definition Snapshot.cpp:103
std::function< Result< ContentId >(std::string_view canonicalInput)> SnapshotHashProvider
Injected content-digest implementation used by snapshots.
Definition Snapshot.h:36
StatePatch implementation of the common world-state contracts.
Stable outer format shared by persistence and cross-process snapshots.
Definition Snapshot.h:46
Immutable description of one committed value change.
Definition StatePatch.h:75
One validation error produced while applying a patch batch.
Definition StatePatch.h:57
Summary of the most recent batch commit attempt.
Definition StatePatch.h:66
std::vector< PatchError > errors
Definition StatePatch.h:71
A patch batch reference qualified by both its owning Store and batch slot.
Definition StatePatch.h:41