namespace WebDrop.Core; public interface IClock { long NowMs { get; } } public sealed class SystemClock : IClock { public long NowMs => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); } /// 大帧的字节存储。键由服务生成,实现不得解析。 public interface IBlobStore { ValueTask PutAsync(string key, ReadOnlyMemory data, CancellationToken ct = default); ValueTask ReadAsync(string key, CancellationToken ct = default); void Delete(string key); void DeletePrefix(string prefix); IEnumerable<(string Key, DateTimeOffset LastWrite)> Enumerate(); } /// /// 创建频道前的人机/滥用校验(Cap 的 PoW 验证码)。验证码本身不属于本项目: /// 校验失败抛 CaptchaRequired,验证服务不可用抛 CaptchaUnavailable(两者对用户的含义不同)。 /// public interface ICaptchaVerifier { bool Enabled { get; } Task VerifyAsync(string? token, CancellationToken ct); } /// 不启用验证码(开发和小型自托管)。 public sealed class NullCaptchaVerifier : ICaptchaVerifier { public bool Enabled => false; public Task VerifyAsync(string? token, CancellationToken ct) => Task.CompletedTask; } /// 服务提交后发出的通知,传输层据此唤醒订阅者。 public interface IChannelEvents { void Advanced(Id16 channelId, long seq); void SettingsChanged(Id16 channelId); void SeatsChanged(Id16 channelId); void ChannelDeleted(Id16 channelId); } public sealed class NullChannelEvents : IChannelEvents { public void Advanced(Id16 channelId, long seq) { } public void SettingsChanged(Id16 channelId) { } public void SeatsChanged(Id16 channelId) { } public void ChannelDeleted(Id16 channelId) { } } /// 频道和分享资源共用的读写事务入口。Write 串行执行,抛出异常即回滚。 public interface IResourceStore { T Read(Func action); T Write(Func action); } /// 单个数据库事务内的频道、分享与帧操作。 public interface IResourceTx { // 便捷分享 Share? GetShare(Id16 id); void InsertShare(Share share); void UpdateShare(Share share); IReadOnlyList ListExpiredShares(long now, int limit); bool HasResourceId(Id16 id); int CountShareVisitors(Id16 id); bool HasShareVisitor(Id16 id, ReadOnlySpan endpointHash); void InsertShareVisitor(Id16 id, ReadOnlySpan endpointHash, long joinedAt); void DeleteShareRows(Id16 id); IReadOnlyList ListShareItems(Id16 id); int CountShareItems(Id16 id); void AddShareBytes(Id16 id, long delta); IReadOnlyList ListExpiredShareItems(long now, int limit); ShareItem? GetShareItem(Id16 shareId, Id16 itemId); void InsertShareItem(ShareItem item); void UpdateShareItem(ShareItem item); void DeleteShareItemRows(Id16 shareId, Id16 itemId); bool HasDownloadGrant(Id16 resourceId, Id16 itemId, ReadOnlySpan endpointHash); void AddDownloadGrant(Id16 resourceId, Id16 itemId, ReadOnlySpan endpointHash); ShareFrame? GetShareFrame(Id16 shareId, Id16 itemId, uint index); void InsertShareFrame(ShareFrame frame); IReadOnlyList ListShareFrames(Id16 shareId, Id16 itemId, uint fromIndex, int limit); // 频道 Channel? GetChannel(Id16 id); void InsertChannel(Channel channel); void UpdateChannel(Id16 id, ChannelSettings settings, long expiresAt); void TouchChannel(Id16 id, long expiresAt); void AddChannelBytes(Id16 id, long delta); void SetCompactedThrough(Id16 id, long seq); long NextSeq(Id16 id); void DeleteChannelRows(Id16 id); IReadOnlyList ListExpiredChannels(long now, int limit); // 席位 Seat? GetSeat(Id16 channelId, ReadOnlySpan endpointHash); int CountSeats(Id16 channelId); void InsertSeat(Id16 channelId, Seat seat); bool DeleteSeat(Id16 channelId, ReadOnlySpan endpointHash); void SetSeatLabel(Id16 channelId, ReadOnlySpan endpointHash, byte[] label); IReadOnlyList ListSeats(Id16 channelId); // 条目 Item? GetItem(Id16 channelId, Id16 itemId); void InsertItem(Item item); void UpdateItem(Item item); void DeleteItemRows(Id16 channelId, Id16 itemId); int CountItems(Id16 channelId); IReadOnlyList ListOldestItems(Id16 channelId, int limit); Item? FindSnapshotItem(Id16 channelId, Id16 excludeItemId); IReadOnlyList ListExpiredItems(long now, int limit); // 帧 StoredFrame? GetFrame(Id16 channelId, Id16 itemId, uint index); void InsertFrame(StoredFrame frame); IReadOnlyList ListFrames(Id16 channelId, Id16 itemId, uint fromIndex, int limit); bool BlobKeyExists(string blobKey); // 日志 void InsertLog(Id16 channelId, LogEntry entry); IReadOnlyList ReadLog(Id16 channelId, long fromSeq, int limit); long PruneTombstones(Id16 channelId, long olderThanMs); }