7f3b00a646
Rewords Cleared/Unanswered/ForeignRequest to name only observed facts and every live hypothesis instead of picking a winner; adds a guarded answeredOutcome accessor against a future unhandled BakeAnswerKind; folds an unreachable BakeScanKey/Context test state; fixes generation-stamp timing.
181 lines
7.2 KiB
C++
181 lines
7.2 KiB
C++
// bake_wire.cpp — see bake_wire.h. Pure: standard library only.
|
|
|
|
#include "core/wire/bake_wire.h"
|
|
|
|
#include <utility>
|
|
|
|
#include "core/version/app_version.h"
|
|
#include "core/wire/wire.h"
|
|
|
|
namespace reasampler::wire {
|
|
|
|
namespace {
|
|
|
|
constexpr const char* kRequestMagic = "rsbakereq1";
|
|
constexpr const char* kOutcomeMagic = "rsbakeout1";
|
|
|
|
using wire::putField;
|
|
using Cursor = wire::Cursor;
|
|
|
|
// An unrecognized integer is `Failed`, not a parse error: the two artifacts ship
|
|
// independently, and a newer extension naming a failure this build has no word for must
|
|
// still read as a failure rather than as Ok (which is what the numeric default would be).
|
|
BakeStatus statusFromWire(int raw) {
|
|
switch (static_cast<BakeStatus>(raw)) {
|
|
case BakeStatus::Ok:
|
|
case BakeStatus::Failed:
|
|
case BakeStatus::NoProject:
|
|
case BakeStatus::StagedMissing:
|
|
case BakeStatus::NoSource:
|
|
case BakeStatus::IndexRejected:
|
|
case BakeStatus::WrongProject:
|
|
return static_cast<BakeStatus>(raw);
|
|
}
|
|
return BakeStatus::Failed;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::string bakeActionLookupName() {
|
|
return "_" + version::channelCommandId(kBakeActionSuffix);
|
|
}
|
|
|
|
bool BakeRequest::operator==(const BakeRequest& o) const {
|
|
return instanceGuid == o.instanceGuid && stagedFilePath == o.stagedFilePath &&
|
|
sourceSampleId == o.sourceSampleId &&
|
|
sourceRelativePath == o.sourceRelativePath &&
|
|
sourceDisplayName == o.sourceDisplayName && ownUsageKey == o.ownUsageKey &&
|
|
rootNote == o.rootNote && generation == o.generation;
|
|
}
|
|
|
|
bool BakeOutcome::operator==(const BakeOutcome& o) const {
|
|
return status == o.status && sampleId == o.sampleId &&
|
|
relativePath == o.relativePath && displayName == o.displayName &&
|
|
rootNote == o.rootNote && channelCount == o.channelCount &&
|
|
replaced == o.replaced && message == o.message && generation == o.generation;
|
|
}
|
|
|
|
std::string encodeBakeRequest(const BakeRequest& req) {
|
|
std::string out = kRequestMagic;
|
|
putField(out, req.instanceGuid);
|
|
putField(out, req.stagedFilePath);
|
|
putField(out, req.sourceSampleId);
|
|
putField(out, req.sourceRelativePath);
|
|
putField(out, req.sourceDisplayName);
|
|
putField(out, req.ownUsageKey);
|
|
putField(out, std::to_string(req.rootNote));
|
|
putField(out, std::to_string(req.generation));
|
|
return out;
|
|
}
|
|
|
|
std::optional<BakeRequest> decodeBakeRequest(const std::string& wire) {
|
|
Cursor cur(wire);
|
|
if (!cur.literal(kRequestMagic)) return std::nullopt;
|
|
|
|
BakeRequest req;
|
|
if (!cur.field(req.instanceGuid)) return std::nullopt;
|
|
if (!cur.field(req.stagedFilePath)) return std::nullopt;
|
|
if (!cur.field(req.sourceSampleId)) return std::nullopt;
|
|
if (!cur.field(req.sourceRelativePath)) return std::nullopt;
|
|
if (!cur.field(req.sourceDisplayName)) return std::nullopt;
|
|
if (!cur.field(req.ownUsageKey)) return std::nullopt;
|
|
if (!cur.fieldInt(req.rootNote)) return std::nullopt;
|
|
if (!cur.fieldInt64(req.generation)) return std::nullopt;
|
|
|
|
if (!cur.ok() || !cur.atEnd()) return std::nullopt;
|
|
return req;
|
|
}
|
|
|
|
std::string encodeBakeOutcome(const BakeOutcome& outcome) {
|
|
std::string out = kOutcomeMagic;
|
|
putField(out, std::to_string(static_cast<int>(outcome.status)));
|
|
putField(out, outcome.sampleId);
|
|
putField(out, outcome.relativePath);
|
|
putField(out, outcome.displayName);
|
|
putField(out, std::to_string(outcome.rootNote));
|
|
putField(out, std::to_string(outcome.channelCount));
|
|
putField(out, outcome.replaced ? "1" : "0");
|
|
putField(out, outcome.message);
|
|
putField(out, std::to_string(outcome.generation));
|
|
return out;
|
|
}
|
|
|
|
std::optional<BakeOutcome> decodeBakeOutcome(const std::string& wire) {
|
|
Cursor cur(wire);
|
|
if (!cur.literal(kOutcomeMagic)) return std::nullopt;
|
|
|
|
BakeOutcome outcome;
|
|
int rawStatus = 0;
|
|
std::string replaced;
|
|
if (!cur.fieldInt(rawStatus)) return std::nullopt;
|
|
if (!cur.field(outcome.sampleId)) return std::nullopt;
|
|
if (!cur.field(outcome.relativePath)) return std::nullopt;
|
|
if (!cur.field(outcome.displayName)) return std::nullopt;
|
|
if (!cur.fieldInt(outcome.rootNote)) return std::nullopt;
|
|
if (!cur.fieldInt(outcome.channelCount)) return std::nullopt;
|
|
if (!cur.field(replaced)) return std::nullopt;
|
|
if (!cur.field(outcome.message)) return std::nullopt;
|
|
if (!cur.fieldInt64(outcome.generation)) return std::nullopt;
|
|
|
|
if (!cur.ok() || !cur.atEnd()) return std::nullopt;
|
|
if (replaced != "0" && replaced != "1") return std::nullopt;
|
|
outcome.status = statusFromWire(rawStatus);
|
|
outcome.replaced = (replaced == "1");
|
|
return outcome;
|
|
}
|
|
|
|
BakeAnswer classifyBakeAnswer(const std::optional<std::string>& raw,
|
|
const BakeRequest& sent) {
|
|
BakeAnswer answer;
|
|
// `raw` folds three distinct bridge outcomes into one — absent, explicitly
|
|
// cleared, and an unreadable/oversized read — because the bridge cannot label
|
|
// which occurred. A caller's message for `Cleared` must not claim more than that.
|
|
if (!raw || raw->empty()) {
|
|
answer.kind = BakeAnswerKind::Cleared;
|
|
return answer;
|
|
}
|
|
if (std::optional<BakeOutcome> outcome = decodeBakeOutcome(*raw)) {
|
|
answer.kind = outcome->generation == sent.generation
|
|
? BakeAnswerKind::Answered
|
|
: BakeAnswerKind::ForeignOutcome;
|
|
answer.outcome = std::move(outcome);
|
|
return answer;
|
|
}
|
|
// Still a request: whether it is OURS is what separates "nothing read this key" from
|
|
// "another instance overwrote it" — a persisted instanceGuid is copyable, so two
|
|
// instances CAN name one key.
|
|
if (const std::optional<BakeRequest> req = decodeBakeRequest(*raw)) {
|
|
answer.kind =
|
|
*req == sent ? BakeAnswerKind::Unanswered : BakeAnswerKind::ForeignRequest;
|
|
return answer;
|
|
}
|
|
answer.kind = BakeAnswerKind::Undecodable;
|
|
return answer;
|
|
}
|
|
|
|
const BakeOutcome* answeredOutcome(const BakeAnswer& answer) {
|
|
if (answer.kind != BakeAnswerKind::Answered || !answer.outcome) return nullptr;
|
|
return &*answer.outcome;
|
|
}
|
|
|
|
BakeScanVerdict classifyBakeScan(const BakeScanContext& session, const BakeScanKey& key,
|
|
std::int64_t nowSec) {
|
|
// Not a request: an outcome the writing instance has not collected yet, or a value
|
|
// from a build we do not read. The writer owns clearing its own key.
|
|
if (!key.decoded) return BakeScanVerdict::Ignore;
|
|
|
|
// Either direction, so a clock moved backwards is caught too.
|
|
const std::int64_t age = nowSec - key.generation;
|
|
if (age > kMaxRequestAgeSeconds || age < -kMaxRequestAgeSeconds)
|
|
return BakeScanVerdict::ClearStale;
|
|
|
|
// Two facts have to agree before anything may land: the loaded project is the one a
|
|
// persist would write into (loadedProjectIsActive), and this key's tab IS that loaded
|
|
// project (matchesLoadedProject — false by construction whenever no project is
|
|
// loaded, which is what folds the former three-way check into two).
|
|
const bool landable = session.loadedProjectIsActive && key.matchesLoadedProject;
|
|
return landable ? BakeScanVerdict::Land : BakeScanVerdict::RefuseWrongProject;
|
|
}
|
|
|
|
} // namespace reasampler::wire
|