Align UI storage event runtime contracts

This commit is contained in:
liu weikai
2026-06-15 01:50:49 +08:00
parent f055d36fd5
commit b2d19334af
11 changed files with 2291 additions and 61 deletions
@@ -0,0 +1,261 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <cstring>
namespace sys
{
namespace runtime
{
enum class NoticeCategory : uint8_t
{
General,
ChatDelivery,
Storage,
Tracker,
Protocol,
};
enum class NoticeSeverity : uint8_t
{
Info,
Success,
Warning,
Error,
};
enum class FeedbackEventKind : uint8_t
{
Posted,
Presented,
Dropped,
Deduped,
};
struct NoticeIntent
{
static constexpr std::size_t kMaxMessageBytes = 192;
uint32_t notice_id = 0;
NoticeCategory category = NoticeCategory::General;
NoticeSeverity severity = NoticeSeverity::Info;
char message[kMaxMessageBytes]{};
uint32_t duration_ms = 0;
uint32_t dedupe_key = 0;
uint32_t created_at_ms = 0;
};
inline void setNoticeMessage(NoticeIntent& intent, const char* message)
{
std::strncpy(intent.message, message ? message : "", NoticeIntent::kMaxMessageBytes - 1);
intent.message[NoticeIntent::kMaxMessageBytes - 1] = '\0';
}
struct FeedbackEvent
{
FeedbackEventKind kind = FeedbackEventKind::Dropped;
uint32_t notice_id = 0;
uint32_t timestamp_ms = 0;
int32_t error = 0;
};
class FeedbackPolicy
{
public:
virtual ~FeedbackPolicy() = default;
virtual bool shouldShow(const NoticeIntent& intent) const = 0;
virtual bool dedupe(const NoticeIntent& previous, const NoticeIntent& next) const = 0;
virtual uint32_t duration(const NoticeIntent& intent) const = 0;
};
class DefaultFeedbackPolicy : public FeedbackPolicy
{
public:
bool shouldShow(const NoticeIntent& intent) const override
{
return intent.message[0] != '\0';
}
bool dedupe(const NoticeIntent& previous, const NoticeIntent& next) const override
{
if (next.dedupe_key != 0 && previous.dedupe_key == next.dedupe_key)
{
return true;
}
return previous.category == next.category && previous.severity == next.severity &&
std::strcmp(previous.message, next.message) == 0;
}
uint32_t duration(const NoticeIntent& intent) const override
{
if (intent.duration_ms != 0)
{
return intent.duration_ms;
}
return intent.severity == NoticeSeverity::Error ? 2200 : 1400;
}
};
class IFeedbackPresenter
{
public:
virtual ~IFeedbackPresenter() = default;
virtual bool present(const NoticeIntent& intent) = 0;
};
class IFeedbackEventSink
{
public:
virtual ~IFeedbackEventSink() = default;
virtual bool publish(const FeedbackEvent& event) = 0;
};
template <std::size_t N>
class FeedbackQueue
{
public:
bool enqueue(const NoticeIntent& intent)
{
if (count_ >= N)
{
return false;
}
notices_[(head_ + count_) % N] = intent;
++count_;
return true;
}
bool popReady(uint32_t now_ms, NoticeIntent& out)
{
(void)now_ms;
if (count_ == 0)
{
return false;
}
out = notices_[head_];
head_ = (head_ + 1) % N;
--count_;
return true;
}
bool dedupe(const NoticeIntent& intent, const FeedbackPolicy& policy)
{
for (std::size_t i = 0; i < count_; ++i)
{
NoticeIntent& queued = notices_[(head_ + i) % N];
if (policy.dedupe(queued, intent))
{
queued = intent;
return true;
}
}
return false;
}
std::size_t size() const
{
return count_;
}
private:
NoticeIntent notices_[N]{};
std::size_t head_ = 0;
std::size_t count_ = 0;
};
template <std::size_t N>
class FeedbackRuntime
{
public:
FeedbackRuntime(FeedbackQueue<N>& queue,
FeedbackPolicy& policy,
IFeedbackPresenter& presenter,
IFeedbackEventSink& events)
: queue_(queue), policy_(policy), presenter_(presenter), events_(events)
{
}
bool post(NoticeIntent intent)
{
if (!policy_.shouldShow(intent))
{
publish(FeedbackEventKind::Dropped, intent.notice_id, intent.created_at_ms, -1);
return false;
}
if (intent.notice_id == 0)
{
intent.notice_id = next_notice_id_++;
}
intent.duration_ms = policy_.duration(intent);
if (queue_.dedupe(intent, policy_))
{
publish(FeedbackEventKind::Deduped, intent.notice_id, intent.created_at_ms, 0);
return true;
}
const bool queued = queue_.enqueue(intent);
publish(queued ? FeedbackEventKind::Posted : FeedbackEventKind::Dropped,
intent.notice_id,
intent.created_at_ms,
queued ? 0 : -2);
return queued;
}
void handle(const FeedbackEvent& event)
{
last_event_ = event;
}
std::size_t drainToUi(uint32_t now_ms)
{
std::size_t presented = 0;
NoticeIntent intent{};
while (queue_.popReady(now_ms, intent))
{
if (presenter_.present(intent))
{
++presented;
publish(FeedbackEventKind::Presented, intent.notice_id, now_ms, 0);
}
else
{
publish(FeedbackEventKind::Dropped, intent.notice_id, now_ms, -3);
}
}
return presented;
}
FeedbackEvent lastEvent() const
{
return last_event_;
}
private:
void publish(FeedbackEventKind kind, uint32_t notice_id, uint32_t timestamp_ms, int32_t error)
{
FeedbackEvent event{};
event.kind = kind;
event.notice_id = notice_id;
event.timestamp_ms = timestamp_ms;
event.error = error;
last_event_ = event;
(void)events_.publish(event);
}
FeedbackQueue<N>& queue_;
FeedbackPolicy& policy_;
IFeedbackPresenter& presenter_;
IFeedbackEventSink& events_;
FeedbackEvent last_event_{};
uint32_t next_notice_id_ = 1;
};
} // namespace runtime
} // namespace sys
@@ -0,0 +1,406 @@
#pragma once
#include "sys/runtime_async.h"
#include <cstddef>
#include <cstdint>
#include <cstring>
namespace sys
{
namespace runtime
{
enum class PersistencePolicyMode : uint8_t
{
DebouncedSave,
BatchSave,
ImmediateCriticalSave,
DropDuplicateSave,
};
enum class PersistenceEventKind : uint8_t
{
SaveQueued,
SaveStarted,
SaveSucceeded,
SaveFailed,
SaveCoalesced,
SaveDropped,
};
struct StoreSnapshot
{
const uint8_t* data = nullptr;
std::size_t len = 0;
bool valid = false;
};
struct StoreStorageResult
{
bool ok = false;
std::size_t bytes = 0;
int32_t error = 0;
};
struct PersistenceCommand
{
uint32_t command_id = 0;
const char* store_key = nullptr;
PersistencePolicyMode policy = PersistencePolicyMode::DebouncedSave;
uint32_t deadline_ms = 0;
uint32_t created_at_ms = 0;
};
struct PersistenceEvent
{
PersistenceEventKind kind = PersistenceEventKind::SaveFailed;
const char* store_key = nullptr;
uint32_t command_id = 0;
uint32_t timestamp_ms = 0;
int32_t error = 0;
};
class PersistencePolicy
{
public:
virtual ~PersistencePolicy() = default;
virtual PersistencePolicyMode modeFor(const char* store_key) const = 0;
virtual uint32_t delayFor(PersistencePolicyMode policy) const = 0;
virtual BusAccessPolicy busPolicyFor(PersistencePolicyMode policy) const = 0;
};
class DefaultPersistencePolicy : public PersistencePolicy
{
public:
PersistencePolicyMode modeFor(const char* store_key) const override
{
(void)store_key;
return PersistencePolicyMode::DebouncedSave;
}
uint32_t delayFor(PersistencePolicyMode policy) const override
{
switch (policy)
{
case PersistencePolicyMode::ImmediateCriticalSave:
return 0;
case PersistencePolicyMode::BatchSave:
return 500;
case PersistencePolicyMode::DropDuplicateSave:
case PersistencePolicyMode::DebouncedSave:
default:
return 150;
}
}
BusAccessPolicy busPolicyFor(PersistencePolicyMode policy) const override
{
return policy == PersistencePolicyMode::ImmediateCriticalSave
? BusAccessPolicy::DurableCommit
: BusAccessPolicy::BackgroundWorkerBounded;
}
};
class IStoreSnapshotProvider
{
public:
virtual ~IStoreSnapshotProvider() = default;
virtual StoreSnapshot snapshot(const char* store_key) = 0;
};
class IStoreStorageAdapter
{
public:
virtual ~IStoreStorageAdapter() = default;
virtual StoreStorageResult write(const char* store_key,
const uint8_t* bytes,
std::size_t len) = 0;
virtual StoreStorageResult read(const char* store_key,
uint8_t* bytes,
std::size_t capacity,
std::size_t& out_len) = 0;
};
class IPersistenceEventSink
{
public:
virtual ~IPersistenceEventSink() = default;
virtual bool publish(const PersistenceEvent& event) = 0;
};
template <std::size_t N>
class DirtyStoreRegistry
{
public:
bool markDirty(const char* store_key,
PersistencePolicyMode policy,
uint32_t due_ms)
{
if (store_key == nullptr || store_key[0] == '\0')
{
return false;
}
for (std::size_t i = 0; i < count_; ++i)
{
if (sameKey(records_[i].store_key, store_key))
{
records_[i].policy = policy;
records_[i].due_ms = due_ms;
records_[i].dirty = true;
return true;
}
}
if (count_ >= N)
{
return false;
}
records_[count_++] = DirtyStoreRecord{store_key, due_ms, policy, true};
return true;
}
bool takeDue(uint32_t now_ms, PersistenceCommand& out)
{
for (std::size_t i = 0; i < count_; ++i)
{
DirtyStoreRecord& record = records_[i];
if (!record.dirty || static_cast<int32_t>(record.due_ms - now_ms) > 0)
{
continue;
}
out.store_key = record.store_key;
out.policy = record.policy;
out.created_at_ms = now_ms;
record.dirty = false;
compact();
return true;
}
return false;
}
bool hasPending(const char* store_key) const
{
for (std::size_t i = 0; i < count_; ++i)
{
if (records_[i].dirty && sameKey(records_[i].store_key, store_key))
{
return true;
}
}
return false;
}
std::size_t size() const
{
return count_;
}
private:
struct DirtyStoreRecord
{
const char* store_key = nullptr;
uint32_t due_ms = 0;
PersistencePolicyMode policy = PersistencePolicyMode::DebouncedSave;
bool dirty = false;
};
static bool sameKey(const char* lhs, const char* rhs)
{
return lhs != nullptr && rhs != nullptr && std::strcmp(lhs, rhs) == 0;
}
void compact()
{
DirtyStoreRecord kept[N]{};
std::size_t kept_count = 0;
for (std::size_t i = 0; i < count_; ++i)
{
if (records_[i].dirty)
{
kept[kept_count++] = records_[i];
}
}
for (std::size_t i = 0; i < kept_count; ++i)
{
records_[i] = kept[i];
}
count_ = kept_count;
}
DirtyStoreRecord records_[N]{};
std::size_t count_ = 0;
};
class PersistenceWorker
{
public:
PersistenceWorker(IStoreSnapshotProvider& snapshots,
IStoreStorageAdapter& storage,
IBusArbiter& bus,
IPersistenceEventSink& events,
PersistencePolicy& policy)
: snapshots_(snapshots), storage_(storage), bus_(bus), events_(events), policy_(policy)
{
}
bool submit(const PersistenceCommand& command)
{
if (pending_)
{
return false;
}
pending_command_ = command;
pending_ = true;
return true;
}
void tick(uint32_t now_ms)
{
if (!pending_)
{
return;
}
PersistenceEvent started{};
started.kind = PersistenceEventKind::SaveStarted;
started.store_key = pending_command_.store_key;
started.command_id = pending_command_.command_id;
started.timestamp_ms = now_ms;
(void)events_.publish(started);
BusAcquireRequest request{};
request.policy = policy_.busPolicyFor(pending_command_.policy);
request.command_id = pending_command_.command_id;
request.deadline_ms = pending_command_.deadline_ms;
const BusAcquireResult acquire = bus_.acquire(request);
if (acquire.status != BusAcquireStatus::Acquired)
{
publishResult(PersistenceEventKind::SaveFailed, now_ms, -11);
pending_ = false;
return;
}
StoreSnapshot snapshot = snapshots_.snapshot(pending_command_.store_key);
StoreStorageResult result{};
if (snapshot.valid)
{
result = storage_.write(pending_command_.store_key, snapshot.data, snapshot.len);
}
else
{
result.error = -12;
}
bus_.release(acquire.token);
publishResult(result.ok ? PersistenceEventKind::SaveSucceeded
: PersistenceEventKind::SaveFailed,
now_ms,
result.ok ? 0 : result.error);
pending_ = false;
}
bool busy() const
{
return pending_;
}
private:
void publishResult(PersistenceEventKind kind, uint32_t now_ms, int32_t error)
{
PersistenceEvent event{};
event.kind = kind;
event.store_key = pending_command_.store_key;
event.command_id = pending_command_.command_id;
event.timestamp_ms = now_ms;
event.error = error;
(void)events_.publish(event);
}
IStoreSnapshotProvider& snapshots_;
IStoreStorageAdapter& storage_;
IBusArbiter& bus_;
IPersistenceEventSink& events_;
PersistencePolicy& policy_;
PersistenceCommand pending_command_{};
bool pending_ = false;
};
template <std::size_t N>
class PersistenceRuntime
{
public:
PersistenceRuntime(DirtyStoreRegistry<N>& registry,
PersistenceWorker& worker,
IPersistenceEventSink& events,
PersistencePolicy& policy)
: registry_(registry), worker_(worker), events_(events), policy_(policy)
{
}
bool markDirty(const char* store_key, uint32_t now_ms)
{
const PersistencePolicyMode mode = policy_.modeFor(store_key);
const bool marked = registry_.markDirty(store_key, mode, now_ms + policy_.delayFor(mode));
PersistenceEvent event{};
event.kind = marked ? PersistenceEventKind::SaveQueued
: PersistenceEventKind::SaveDropped;
event.store_key = store_key;
event.timestamp_ms = now_ms;
(void)events_.publish(event);
return marked;
}
bool requestSave(const char* store_key,
PersistencePolicyMode policy,
uint32_t now_ms)
{
return registry_.markDirty(store_key, policy, now_ms + policy_.delayFor(policy));
}
void tick(uint32_t now_ms)
{
if (!worker_.busy())
{
PersistenceCommand command{};
if (registry_.takeDue(now_ms, command))
{
command.command_id = next_command_id_++;
if (!worker_.submit(command))
{
(void)registry_.markDirty(command.store_key,
command.policy,
now_ms + policy_.delayFor(command.policy));
}
}
}
worker_.tick(now_ms);
}
void handle(const PersistenceEvent& event)
{
last_event_ = event;
}
PersistenceEvent lastEvent() const
{
return last_event_;
}
private:
DirtyStoreRegistry<N>& registry_;
PersistenceWorker& worker_;
IPersistenceEventSink& events_;
PersistencePolicy& policy_;
PersistenceEvent last_event_{};
uint32_t next_command_id_ = 1;
};
} // namespace runtime
} // namespace sys
@@ -270,6 +270,164 @@ class IBusArbiter
virtual StorageHealthState health() const = 0;
};
class IBusAdapter
{
public:
virtual ~IBusAdapter() = default;
virtual bool tryAcquire(uint32_t timeout_ms) = 0;
virtual void release() = 0;
virtual uint32_t nowMs() const = 0;
virtual uint32_t owner() const = 0;
};
class BusPolicyStrategy
{
public:
virtual ~BusPolicyStrategy() = default;
virtual BusAccessPolicy select(const RuntimeCommand& command) const = 0;
virtual uint32_t timeoutFor(BusAccessPolicy policy) const = 0;
};
class DefaultBusPolicyStrategy : public BusPolicyStrategy
{
public:
BusAccessPolicy select(const RuntimeCommand& command) const override
{
if (command.priority == RuntimePriority::Realtime ||
command.priority == RuntimePriority::Interactive)
{
return BusAccessPolicy::InteractiveWorkerBounded;
}
if (command.kind == RuntimeCommandKind::TrackStop ||
command.kind == RuntimeCommandKind::TrackFlush)
{
return BusAccessPolicy::DurableCommit;
}
return BusAccessPolicy::BackgroundWorkerBounded;
}
uint32_t timeoutFor(BusAccessPolicy policy) const override
{
switch (policy)
{
case BusAccessPolicy::UiNeverBlock:
return 0;
case BusAccessPolicy::InteractiveWorkerBounded:
return 2;
case BusAccessPolicy::BackgroundWorkerBounded:
return 25;
case BusAccessPolicy::DurableCommit:
return 150;
case BusAccessPolicy::RecoveryExclusive:
return 500;
default:
return 0;
}
}
};
class StorageBusArbiter : public IBusArbiter
{
public:
StorageBusArbiter(IBusAdapter& adapter, BusPolicyStrategy& policy)
: adapter_(adapter), policy_(policy)
{
}
BusAcquireResult acquire(const BusAcquireRequest& request) override
{
const uint32_t start_ms = adapter_.nowMs();
uint32_t timeout_ms = policy_.timeoutFor(request.policy);
if (request.deadline_ms != 0)
{
const uint32_t remaining =
static_cast<int32_t>(request.deadline_ms - start_ms) > 0
? request.deadline_ms - start_ms
: 0;
if (remaining < timeout_ms)
{
timeout_ms = remaining;
}
}
const bool acquired = adapter_.tryAcquire(timeout_ms);
const uint32_t end_ms = adapter_.nowMs();
BusAcquireResult result{};
result.status = acquired ? BusAcquireStatus::Acquired
: (timeout_ms == 0 ? BusAcquireStatus::Busy
: BusAcquireStatus::TimedOut);
result.token.resource = request.resource;
result.token.owner = request.command_id;
result.token.acquired_ms = acquired ? end_ms : 0;
result.token.valid = acquired;
result.diagnostics.resource = request.resource;
result.diagnostics.owner = adapter_.owner();
result.diagnostics.command_id = request.command_id;
result.diagnostics.wait_ms = end_ms - start_ms;
result.diagnostics.policy = request.policy;
updateHealth(result.status, end_ms);
return result;
}
void release(const BusAccessToken& token) override
{
if (!token.valid)
{
return;
}
adapter_.release();
consecutive_timeouts_ = 0;
if (health_.status == StorageHealthStatus::Slow ||
health_.status == StorageHealthStatus::Recovering)
{
health_.status = StorageHealthStatus::Healthy;
health_.last_error = 0;
health_.last_transition_ms = adapter_.nowMs();
}
}
StorageHealthState health() const override
{
return health_;
}
BusAccessPolicy selectPolicy(const RuntimeCommand& command) const
{
return policy_.select(command);
}
private:
void updateHealth(BusAcquireStatus status, uint32_t now_ms)
{
if (status == BusAcquireStatus::Acquired)
{
return;
}
health_.last_transition_ms = now_ms;
if (status == BusAcquireStatus::Unavailable)
{
health_.status = StorageHealthStatus::Unavailable;
health_.last_error = -3;
return;
}
++consecutive_timeouts_;
health_.last_error = status == BusAcquireStatus::TimedOut ? -2 : -1;
health_.status = consecutive_timeouts_ >= 3 ? StorageHealthStatus::Degraded
: StorageHealthStatus::Slow;
}
IBusAdapter& adapter_;
BusPolicyStrategy& policy_;
StorageHealthState health_{};
uint8_t consecutive_timeouts_ = 0;
};
class IPlatformStorageAdapter
{
public:
@@ -0,0 +1,461 @@
#pragma once
#include "sys/feedback_runtime.h"
#include "sys/persistence_runtime.h"
#include "sys/runtime_async.h"
#include <cstddef>
#include <cstdint>
#include <cassert>
namespace sys
{
namespace runtime
{
class FakeClock
{
public:
uint32_t now() const
{
return now_ms_;
}
void advance(uint32_t ms)
{
now_ms_ += ms;
}
private:
uint32_t now_ms_ = 0;
};
class FakeCommandQueue : public ICommandQueue
{
public:
bool enqueue(const RuntimeCommand& command) override
{
return queue_.enqueue(command);
}
std::size_t cancel(uint32_t dedupe_key) override
{
return queue_.cancel(dedupe_key);
}
bool popReady(uint32_t now_ms, RuntimeCommand& out) override
{
return queue_.popReady(now_ms, out);
}
std::size_t size() const
{
return queue_.size();
}
private:
FixedCommandQueue<32> queue_{};
};
class FakeEventBus : public IEventSink,
public IPersistenceEventSink,
public IFeedbackEventSink
{
public:
bool publish(const RuntimeEvent& event) override
{
return runtime_events_.publish(event);
}
bool publish(const PersistenceEvent& event) override
{
if (persistence_count_ >= kMaxEvents)
{
return false;
}
persistence_events_[persistence_count_++] = event;
return true;
}
bool publish(const FeedbackEvent& event) override
{
if (feedback_count_ >= kMaxEvents)
{
return false;
}
feedback_events_[feedback_count_++] = event;
return true;
}
std::size_t drain()
{
std::size_t count = 0;
RuntimeEvent event{};
while (runtime_events_.pop(event))
{
++count;
}
count += persistence_count_;
count += feedback_count_;
persistence_count_ = 0;
feedback_count_ = 0;
return count;
}
std::size_t persistenceCount() const
{
return persistence_count_;
}
std::size_t feedbackCount() const
{
return feedback_count_;
}
const PersistenceEvent& persistenceEvent(std::size_t index) const
{
return persistence_events_[index];
}
const FeedbackEvent& feedbackEvent(std::size_t index) const
{
return feedback_events_[index];
}
private:
static constexpr std::size_t kMaxEvents = 32;
FixedEventSink<32> runtime_events_{};
PersistenceEvent persistence_events_[kMaxEvents]{};
FeedbackEvent feedback_events_[kMaxEvents]{};
std::size_t persistence_count_ = 0;
std::size_t feedback_count_ = 0;
};
class FakeStorageBackend : public IPlatformStorageAdapter,
public IStoreSnapshotProvider,
public IStoreStorageAdapter
{
public:
void scriptDelay(const char* operation, uint32_t ms)
{
(void)operation;
delay_ms_ = ms;
}
void scriptFailure(const char* operation, int32_t error)
{
(void)operation;
fail_ = true;
error_ = error;
}
PlatformStorageResult read(const PlatformStorageReadRequest& request) override
{
last_command_id_ = request.command_id;
return platformResult(request.capacity);
}
PlatformStorageResult write(const PlatformStorageWriteRequest& request) override
{
last_command_id_ = request.command_id;
return platformResult(request.len);
}
PlatformStorageResult list(const PlatformStorageListRequest& request) override
{
last_command_id_ = request.command_id;
return platformResult(0);
}
PlatformStorageResult flush(const PlatformStorageFlushRequest& request) override
{
last_command_id_ = request.command_id;
return platformResult(0);
}
StoreSnapshot snapshot(const char* store_key) override
{
(void)store_key;
StoreSnapshot snapshot{};
snapshot.data = snapshot_;
snapshot.len = snapshot_len_;
snapshot.valid = !fail_;
return snapshot;
}
StoreStorageResult write(const char* store_key,
const uint8_t* bytes,
std::size_t len) override
{
(void)store_key;
(void)bytes;
StoreStorageResult result{};
result.ok = !fail_;
result.bytes = result.ok ? len : 0;
result.error = result.ok ? 0 : error_;
++write_count_;
return result;
}
StoreStorageResult read(const char* store_key,
uint8_t* bytes,
std::size_t capacity,
std::size_t& out_len) override
{
(void)store_key;
(void)bytes;
out_len = fail_ ? 0 : capacity;
StoreStorageResult result{};
result.ok = !fail_;
result.bytes = out_len;
result.error = result.ok ? 0 : error_;
return result;
}
uint32_t delayMs() const
{
return delay_ms_;
}
uint32_t lastCommandId() const
{
return last_command_id_;
}
std::size_t writeCount() const
{
return write_count_;
}
private:
PlatformStorageResult platformResult(std::size_t bytes)
{
PlatformStorageResult result{};
result.ok = !fail_;
result.bytes = result.ok ? bytes : 0;
result.error = result.ok ? 0 : error_;
return result;
}
uint8_t snapshot_[4] = {1, 2, 3, 4};
std::size_t snapshot_len_ = sizeof(snapshot_);
uint32_t delay_ms_ = 0;
uint32_t last_command_id_ = 0;
std::size_t write_count_ = 0;
bool fail_ = false;
int32_t error_ = -1;
};
class FakeBusArbiter : public IBusArbiter
{
public:
void scriptAcquire(BusAcquireStatus status)
{
scripted_status_ = status;
}
BusAcquireResult acquire(const BusAcquireRequest& request) override
{
++acquire_count_;
last_request_ = request;
BusAcquireResult result{};
result.status = scripted_status_;
result.token.valid = scripted_status_ == BusAcquireStatus::Acquired;
result.token.resource = request.resource;
result.token.owner = request.command_id;
result.token.acquired_ms = now_ms_;
result.diagnostics.resource = request.resource;
result.diagnostics.command_id = request.command_id;
result.diagnostics.policy = request.policy;
if (scripted_status_ != BusAcquireStatus::Acquired)
{
health_.status = StorageHealthStatus::Slow;
health_.last_error = -1;
health_.last_transition_ms = now_ms_;
}
return result;
}
void release(const BusAccessToken& token) override
{
if (token.valid)
{
++release_count_;
}
}
StorageHealthState health() const override
{
return health_;
}
BusAcquireRequest diagnostics() const
{
return last_request_;
}
void setNow(uint32_t now_ms)
{
now_ms_ = now_ms;
}
std::size_t acquireCount() const
{
return acquire_count_;
}
std::size_t releaseCount() const
{
return release_count_;
}
private:
BusAcquireStatus scripted_status_ = BusAcquireStatus::Acquired;
BusAcquireRequest last_request_{};
StorageHealthState health_{};
uint32_t now_ms_ = 0;
std::size_t acquire_count_ = 0;
std::size_t release_count_ = 0;
};
class FakeUiOwner : public IUiEffectSink
{
public:
void assertNoBlockingCalls() const
{
assert(blocking_calls_ == 0);
}
bool apply(const RuntimeUiEffect& effect) override
{
last_effect_ = effect;
++effect_count_;
return true;
}
void tick()
{
++tick_count_;
}
void recordBlockingCall()
{
++blocking_calls_;
}
std::size_t effectCount() const
{
return effect_count_;
}
private:
RuntimeUiEffect last_effect_{};
std::size_t effect_count_ = 0;
std::size_t tick_count_ = 0;
std::size_t blocking_calls_ = 0;
};
class FakeFeedbackPresenter : public IFeedbackPresenter
{
public:
bool present(const NoticeIntent& intent) override
{
if (count_ >= kMaxNotices)
{
return false;
}
captured_[count_++] = intent;
return true;
}
std::size_t captured() const
{
return count_;
}
const NoticeIntent& capturedNotice(std::size_t index) const
{
return captured_[index];
}
private:
static constexpr std::size_t kMaxNotices = 16;
NoticeIntent captured_[kMaxNotices]{};
std::size_t count_ = 0;
};
class RuntimeHarness
{
public:
FakeClock& clock()
{
return clock_;
}
FakeCommandQueue& commands()
{
return commands_;
}
FakeEventBus& events()
{
return events_;
}
FakeStorageBackend& storage()
{
return storage_;
}
FakeBusArbiter& bus()
{
return bus_;
}
FakeUiOwner& ui()
{
return ui_;
}
FakeFeedbackPresenter& feedbackPresenter()
{
return feedback_;
}
void advance(uint32_t ms)
{
clock_.advance(ms);
bus_.setNow(clock_.now());
}
std::size_t drainUi()
{
ui_.tick();
return events_.drain();
}
void runUntilIdle(uint32_t step_ms = 10, uint32_t max_steps = 64)
{
for (uint32_t i = 0; i < max_steps; ++i)
{
advance(step_ms);
if (drainUi() == 0)
{
return;
}
}
}
private:
FakeClock clock_{};
FakeCommandQueue commands_{};
FakeEventBus events_{};
FakeStorageBackend storage_{};
FakeBusArbiter bus_{};
FakeUiOwner ui_{};
FakeFeedbackPresenter feedback_{};
};
} // namespace runtime
} // namespace sys