From 3cffa5b15675079a0d60bceb45b08948e0f9810e Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 26 Feb 2026 16:06:50 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20port=20session=2018=20=E2=80=94=20JetSt?= =?UTF-8?q?ream=20File=20Store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FileStoreTypes: FileStoreConfig, FileStreamInfo, FileConsumerInfo, Psi, Cache, MsgId, CompressionInfo, ErrBadMsg, FileStoreDefaults constants - FileStore: JetStreamFileStore implementing IStreamStore (26 methods stubbed) with State/Type/Stop/Register* properly implemented - MessageBlock: MessageBlock type with all 40+ fields, ConsumerFileStore stub - 312 features complete (IDs 951-1262) --- .../JetStream/FileStore.cs | 428 ++++++++++++++++++ .../JetStream/FileStoreTypes.cs | 416 +++++++++++++++++ .../JetStream/MessageBlock.cs | 382 ++++++++++++++++ porting.db | Bin 2473984 -> 2473984 bytes reports/current.md | 8 +- reports/report_5a2c8a3.md | 39 ++ 6 files changed, 1269 insertions(+), 4 deletions(-) create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/JetStream/FileStore.cs create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/JetStream/FileStoreTypes.cs create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/JetStream/MessageBlock.cs create mode 100644 reports/report_5a2c8a3.md diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/FileStore.cs b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/FileStore.cs new file mode 100644 index 0000000..be1b714 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/FileStore.cs @@ -0,0 +1,428 @@ +// Copyright 2019-2026 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Adapted from server/filestore.go (fileStore struct and methods) + +using System.Threading.Channels; +using ZB.MOM.NatsNet.Server.Internal.DataStructures; + +namespace ZB.MOM.NatsNet.Server; + +/// +/// File-backed implementation of . +/// Stores JetStream messages in per-block files on disk with optional +/// encryption and compression. +/// Mirrors the fileStore struct in filestore.go. +/// +public sealed class JetStreamFileStore : IStreamStore, IDisposable +{ + // ----------------------------------------------------------------------- + // Fields — mirrors fileStore struct fields + // ----------------------------------------------------------------------- + + private readonly ReaderWriterLockSlim _mu = new(LockRecursionPolicy.NoRecursion); + + // State + private StreamState _state = new(); + private List? _tombs; + private LostStreamData? _ld; + + // Callbacks + private StorageUpdateHandler? _scb; + private StorageRemoveMsgHandler? _rmcb; + private ProcessJetStreamMsgHandler? _pmsgcb; + + // Age-check timer + private Timer? _ageChk; + private bool _ageChkRun; + private long _ageChkTime; + + // Background sync timer + private Timer? _syncTmr; + + // Configuration + private FileStreamInfo _cfg; + private FileStoreConfig _fcfg; + + // Message block list and index + private MessageBlock? _lmb; // last (active write) block + private List _blks = []; + private Dictionary _bim = []; + + // Per-subject index map + private SubjectTree? _psim; + + // Total subject-list length (sum of subject-string lengths) + private int _tsl; + + // writeFullState concurrency guard + private readonly object _wfsmu = new(); + private long _wfsrun; // Interlocked: is writeFullState running? + private int _wfsadml; // Average dmap length (protected by _wfsmu) + + // Quit / load-done channels (Channel mimics chan struct{}) + private Channel? _qch; + private Channel? _fsld; + + // Consumer list + private readonly ReaderWriterLockSlim _cmu = new(LockRecursionPolicy.NoRecursion); + private List _cfs = []; + + // Snapshot-in-progress count + private int _sips; + + // Dirty-write counter (incremented when writes are pending flush) + private int _dirty; + + // Lifecycle flags + private bool _closing; + private volatile bool _closed; + + // Flush-in-progress flag + private bool _fip; + + // Whether the store has ever received a message + private bool _receivedAny; + + // Whether the first sequence has been moved forward + private bool _firstMoved; + + // Last PurgeEx call time (for throttle logic) + private DateTime _lpex; + + // ----------------------------------------------------------------------- + // Constructor + // ----------------------------------------------------------------------- + + /// + /// Initialises a file-backed stream store using the supplied file-store + /// configuration and stream information. + /// + /// File-store configuration (block size, cipher, paths, etc.). + /// Stream metadata (created time and stream config). + /// + /// Thrown when or is null. + /// + public JetStreamFileStore(FileStoreConfig fcfg, FileStreamInfo cfg) + { + ArgumentNullException.ThrowIfNull(fcfg); + ArgumentNullException.ThrowIfNull(cfg); + + _fcfg = fcfg; + _cfg = cfg; + + // Apply defaults (mirrors newFileStoreWithCreated in filestore.go). + if (_fcfg.BlockSize == 0) + _fcfg.BlockSize = FileStoreDefaults.DefaultLargeBlockSize; + if (_fcfg.CacheExpire == TimeSpan.Zero) + _fcfg.CacheExpire = FileStoreDefaults.DefaultCacheBufferExpiration; + if (_fcfg.SubjectStateExpire == TimeSpan.Zero) + _fcfg.SubjectStateExpire = FileStoreDefaults.DefaultFssExpiration; + if (_fcfg.SyncInterval == TimeSpan.Zero) + _fcfg.SyncInterval = FileStoreDefaults.DefaultSyncInterval; + + _psim = new SubjectTree(); + _bim = new Dictionary(); + _qch = Channel.CreateUnbounded(); + _fsld = Channel.CreateUnbounded(); + } + + // ----------------------------------------------------------------------- + // IStreamStore — type / state + // ----------------------------------------------------------------------- + + /// + public StorageType Type() => StorageType.FileStorage; + + /// + public StreamState State() + { + _mu.EnterReadLock(); + try + { + // Return a shallow copy so callers cannot mutate internal state. + return new StreamState + { + Msgs = _state.Msgs, + Bytes = _state.Bytes, + FirstSeq = _state.FirstSeq, + FirstTime = _state.FirstTime, + LastSeq = _state.LastSeq, + LastTime = _state.LastTime, + NumSubjects = _state.NumSubjects, + NumDeleted = _state.NumDeleted, + Deleted = _state.Deleted, + Lost = _state.Lost, + Consumers = _state.Consumers, + }; + } + finally + { + _mu.ExitReadLock(); + } + } + + /// + public void FastState(StreamState state) + { + _mu.EnterReadLock(); + try + { + state.Msgs = _state.Msgs; + state.Bytes = _state.Bytes; + state.FirstSeq = _state.FirstSeq; + state.FirstTime = _state.FirstTime; + state.LastSeq = _state.LastSeq; + state.LastTime = _state.LastTime; + state.NumDeleted = _state.NumDeleted; + state.Consumers = _state.Consumers; + } + finally + { + _mu.ExitReadLock(); + } + } + + // ----------------------------------------------------------------------- + // IStreamStore — callback registration + // ----------------------------------------------------------------------- + + /// + public void RegisterStorageUpdates(StorageUpdateHandler cb) + { + _mu.EnterWriteLock(); + try { _scb = cb; } + finally { _mu.ExitWriteLock(); } + } + + /// + public void RegisterStorageRemoveMsg(StorageRemoveMsgHandler cb) + { + _mu.EnterWriteLock(); + try { _rmcb = cb; } + finally { _mu.ExitWriteLock(); } + } + + /// + public void RegisterProcessJetStreamMsg(ProcessJetStreamMsgHandler cb) + { + _mu.EnterWriteLock(); + try { _pmsgcb = cb; } + finally { _mu.ExitWriteLock(); } + } + + // ----------------------------------------------------------------------- + // IStreamStore — lifecycle + // ----------------------------------------------------------------------- + + /// + public void Stop() + { + _mu.EnterWriteLock(); + try + { + if (_closing) return; + _closing = true; + } + finally + { + _mu.ExitWriteLock(); + } + + _ageChk?.Dispose(); + _ageChk = null; + _syncTmr?.Dispose(); + _syncTmr = null; + + _closed = true; + } + + /// + public void Dispose() => Stop(); + + // ----------------------------------------------------------------------- + // IStreamStore — store / load (all stubs) + // ----------------------------------------------------------------------- + + /// + public (ulong Seq, long Ts) StoreMsg(string subject, byte[]? hdr, byte[]? msg, long ttl) + => throw new NotImplementedException("TODO: session 18 — filestore StoreMsg"); + + /// + public void StoreRawMsg(string subject, byte[]? hdr, byte[]? msg, ulong seq, long ts, long ttl, bool discardNewCheck) + => throw new NotImplementedException("TODO: session 18 — filestore StoreRawMsg"); + + /// + public (ulong Seq, Exception? Error) SkipMsg(ulong seq) + => throw new NotImplementedException("TODO: session 18 — filestore SkipMsg"); + + /// + public void SkipMsgs(ulong seq, ulong num) + => throw new NotImplementedException("TODO: session 18 — filestore SkipMsgs"); + + /// + public void FlushAllPending() + => throw new NotImplementedException("TODO: session 18 — filestore FlushAllPending"); + + /// + public StoreMsg? LoadMsg(ulong seq, StoreMsg? sm) + => throw new NotImplementedException("TODO: session 18 — filestore LoadMsg"); + + /// + public (StoreMsg? Sm, ulong Skip) LoadNextMsg(string filter, bool wc, ulong start, StoreMsg? smp) + => throw new NotImplementedException("TODO: session 18 — filestore LoadNextMsg"); + + /// + public (StoreMsg? Sm, ulong Skip) LoadNextMsgMulti(object? sl, ulong start, StoreMsg? smp) + => throw new NotImplementedException("TODO: session 18 — filestore LoadNextMsgMulti"); + + /// + public StoreMsg? LoadLastMsg(string subject, StoreMsg? sm) + => throw new NotImplementedException("TODO: session 18 — filestore LoadLastMsg"); + + /// + public (StoreMsg? Sm, Exception? Error) LoadPrevMsg(ulong start, StoreMsg? smp) + => throw new NotImplementedException("TODO: session 18 — filestore LoadPrevMsg"); + + /// + public (StoreMsg? Sm, ulong Skip, Exception? Error) LoadPrevMsgMulti(object? sl, ulong start, StoreMsg? smp) + => throw new NotImplementedException("TODO: session 18 — filestore LoadPrevMsgMulti"); + + /// + public (bool Removed, Exception? Error) RemoveMsg(ulong seq) + => throw new NotImplementedException("TODO: session 18 — filestore RemoveMsg"); + + /// + public (bool Removed, Exception? Error) EraseMsg(ulong seq) + => throw new NotImplementedException("TODO: session 18 — filestore EraseMsg"); + + /// + public (ulong Purged, Exception? Error) Purge() + => throw new NotImplementedException("TODO: session 18 — filestore Purge"); + + /// + public (ulong Purged, Exception? Error) PurgeEx(string subject, ulong seq, ulong keep) + => throw new NotImplementedException("TODO: session 18 — filestore PurgeEx"); + + /// + public (ulong Purged, Exception? Error) Compact(ulong seq) + => throw new NotImplementedException("TODO: session 18 — filestore Compact"); + + /// + public void Truncate(ulong seq) + => throw new NotImplementedException("TODO: session 18 — filestore Truncate"); + + // ----------------------------------------------------------------------- + // IStreamStore — query methods (all stubs) + // ----------------------------------------------------------------------- + + /// + public ulong GetSeqFromTime(DateTime t) + => throw new NotImplementedException("TODO: session 18 — filestore GetSeqFromTime"); + + /// + public SimpleState FilteredState(ulong seq, string subject) + => throw new NotImplementedException("TODO: session 18 — filestore FilteredState"); + + /// + public Dictionary SubjectsState(string filterSubject) + => throw new NotImplementedException("TODO: session 18 — filestore SubjectsState"); + + /// + public Dictionary SubjectsTotals(string filterSubject) + => throw new NotImplementedException("TODO: session 18 — filestore SubjectsTotals"); + + /// + public (ulong[] Seqs, Exception? Error) AllLastSeqs() + => throw new NotImplementedException("TODO: session 18 — filestore AllLastSeqs"); + + /// + public (ulong[] Seqs, Exception? Error) MultiLastSeqs(string[] filters, ulong maxSeq, int maxAllowed) + => throw new NotImplementedException("TODO: session 18 — filestore MultiLastSeqs"); + + /// + public (string Subject, Exception? Error) SubjectForSeq(ulong seq) + => throw new NotImplementedException("TODO: session 18 — filestore SubjectForSeq"); + + /// + public (ulong Total, ulong ValidThrough, Exception? Error) NumPending(ulong sseq, string filter, bool lastPerSubject) + => throw new NotImplementedException("TODO: session 18 — filestore NumPending"); + + /// + public (ulong Total, ulong ValidThrough, Exception? Error) NumPendingMulti(ulong sseq, object? sl, bool lastPerSubject) + => throw new NotImplementedException("TODO: session 18 — filestore NumPendingMulti"); + + // ----------------------------------------------------------------------- + // IStreamStore — stream state encoding (stubs) + // ----------------------------------------------------------------------- + + /// + public (byte[] Enc, Exception? Error) EncodedStreamState(ulong failed) + => throw new NotImplementedException("TODO: session 18 — filestore EncodedStreamState"); + + /// + public void SyncDeleted(DeleteBlocks dbs) + => throw new NotImplementedException("TODO: session 18 — filestore SyncDeleted"); + + // ----------------------------------------------------------------------- + // IStreamStore — config / admin (stubs) + // ----------------------------------------------------------------------- + + /// + public void UpdateConfig(StreamConfig cfg) + => throw new NotImplementedException("TODO: session 18 — filestore UpdateConfig"); + + /// + public void Delete(bool inline) + => throw new NotImplementedException("TODO: session 18 — filestore Delete"); + + /// + public void ResetState() + => throw new NotImplementedException("TODO: session 18 — filestore ResetState"); + + // ----------------------------------------------------------------------- + // IStreamStore — consumer management (stubs) + // ----------------------------------------------------------------------- + + /// + public IConsumerStore ConsumerStore(string name, DateTime created, ConsumerConfig cfg) + => throw new NotImplementedException("TODO: session 18 — filestore ConsumerStore"); + + /// + public void AddConsumer(IConsumerStore o) + { + _cmu.EnterWriteLock(); + try { _cfs.Add(o); } + finally { _cmu.ExitWriteLock(); } + } + + /// + public void RemoveConsumer(IConsumerStore o) + { + _cmu.EnterWriteLock(); + try { _cfs.Remove(o); } + finally { _cmu.ExitWriteLock(); } + } + + // ----------------------------------------------------------------------- + // IStreamStore — snapshot / utilization (stubs) + // ----------------------------------------------------------------------- + + /// + public (SnapshotResult? Result, Exception? Error) Snapshot(TimeSpan deadline, bool includeConsumers, bool checkMsgs) + => throw new NotImplementedException("TODO: session 18 — filestore Snapshot"); + + /// + public (ulong Total, ulong Reported, Exception? Error) Utilization() + => throw new NotImplementedException("TODO: session 18 — filestore Utilization"); +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/FileStoreTypes.cs b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/FileStoreTypes.cs new file mode 100644 index 0000000..39e55e3 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/FileStoreTypes.cs @@ -0,0 +1,416 @@ +// Copyright 2019-2026 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Adapted from server/filestore.go + +using ZB.MOM.NatsNet.Server.Internal.DataStructures; + +namespace ZB.MOM.NatsNet.Server; + +// --------------------------------------------------------------------------- +// FileStoreConfig +// --------------------------------------------------------------------------- + +/// +/// Configuration for a file-backed JetStream stream store. +/// Mirrors FileStoreConfig in filestore.go. +/// +public sealed class FileStoreConfig +{ + /// Parent directory for all storage. + public string StoreDir { get; set; } = string.Empty; + + /// + /// File block size. Also represents the maximum per-block overhead. + /// Defaults to . + /// + public ulong BlockSize { get; set; } + + /// How long with no activity until the in-memory cache is expired. + public TimeSpan CacheExpire { get; set; } + + /// How long with no activity until a message block's subject state is expired. + public TimeSpan SubjectStateExpire { get; set; } + + /// How often the store syncs data to disk in the background. + public TimeSpan SyncInterval { get; set; } + + /// When true, every write is immediately synced to disk. + public bool SyncAlways { get; set; } + + /// When true, write operations may be batched and flushed asynchronously. + public bool AsyncFlush { get; set; } + + /// Encryption cipher used when encrypting blocks. + public StoreCipher Cipher { get; set; } + + /// Compression algorithm applied to stored blocks. + public StoreCompression Compression { get; set; } + + // Internal reference to the owning server — not serialised. + // Equivalent to srv *Server in Go; kept as object to avoid circular project deps. + internal object? Server { get; set; } +} + +// --------------------------------------------------------------------------- +// FileStreamInfo +// --------------------------------------------------------------------------- + +/// +/// Remembers the creation time alongside the stream configuration. +/// Mirrors FileStreamInfo in filestore.go. +/// +public sealed class FileStreamInfo +{ + /// UTC time at which the stream was created. + public DateTime Created { get; set; } + + /// Stream configuration. + public StreamConfig Config { get; set; } = new(); +} + +// --------------------------------------------------------------------------- +// FileConsumerInfo +// --------------------------------------------------------------------------- + +/// +/// Used for creating and restoring consumer stores from disk. +/// Mirrors FileConsumerInfo in filestore.go. +/// +public sealed class FileConsumerInfo +{ + /// UTC time at which the consumer was created. + public DateTime Created { get; set; } + + /// Durable consumer name. + public string Name { get; set; } = string.Empty; + + /// Consumer configuration. + public ConsumerConfig Config { get; set; } = new(); +} + +// --------------------------------------------------------------------------- +// Psi — per-subject index entry (internal) +// --------------------------------------------------------------------------- + +/// +/// Per-subject index entry stored in the subject tree. +/// Mirrors the psi struct in filestore.go. +/// +internal sealed class Psi +{ + /// Total messages for this subject across all blocks. + public ulong Total { get; set; } + + /// Index of the first block that holds messages for this subject. + public uint Fblk { get; set; } + + /// Index of the last block that holds messages for this subject. + public uint Lblk { get; set; } +} + +// --------------------------------------------------------------------------- +// Cache — write-through and load cache (internal) +// --------------------------------------------------------------------------- + +/// +/// Write-through caching layer also used when loading messages from disk. +/// Mirrors the cache struct in filestore.go. +/// +internal sealed class Cache +{ + /// Raw message data buffer. + public byte[] Buf { get; set; } = Array.Empty(); + + /// Write position into . + public int Wp { get; set; } + + /// Per-sequence byte offsets into . + public uint[] Idx { get; set; } = Array.Empty(); + + /// First sequence number this cache covers. + public ulong Fseq { get; set; } + + /// No-random-access flag: when true sequential access is assumed. + public bool Nra { get; set; } +} + +// --------------------------------------------------------------------------- +// MsgId — sequence + timestamp pair (internal) +// --------------------------------------------------------------------------- + +/// +/// Pairs a message sequence number with its nanosecond timestamp. +/// Mirrors the msgId struct in filestore.go. +/// +internal struct MsgId +{ + /// Sequence number. + public ulong Seq; + + /// Nanosecond Unix timestamp. + public long Ts; +} + +// --------------------------------------------------------------------------- +// CompressionInfo — compression metadata +// --------------------------------------------------------------------------- + +/// +/// Compression metadata attached to a message block. +/// Mirrors CompressionInfo in filestore.go. +/// +public sealed class CompressionInfo +{ + /// Compression algorithm in use. + public StoreCompression Type { get; set; } + + /// Original (uncompressed) size in bytes. + public ulong Original { get; set; } + + /// Compressed size in bytes. + public ulong Compressed { get; set; } + + /// + /// Serialises compression metadata as a compact binary prefix. + /// Format: 'c' 'm' 'p' <algorithmByte> <uvarint originalSize> + /// + public byte[] MarshalMetadata() + { + // TODO: session 18 — implement varint encoding + throw new NotImplementedException("TODO: session 18 — filestore CompressionInfo.MarshalMetadata"); + } + + /// + /// Deserialises compression metadata from a binary buffer. + /// Returns the number of bytes consumed, or 0 if the buffer does not start with the expected prefix. + /// + public int UnmarshalMetadata(byte[] b) + { + // TODO: session 18 — implement varint decoding + throw new NotImplementedException("TODO: session 18 — filestore CompressionInfo.UnmarshalMetadata"); + } +} + +// --------------------------------------------------------------------------- +// ErrBadMsg — corrupt/malformed message error (internal) +// --------------------------------------------------------------------------- + +/// +/// Indicates a malformed or corrupt message was detected in a block file. +/// Mirrors the errBadMsg type in filestore.go. +/// +internal sealed class ErrBadMsg : Exception +{ + /// Path to the block file that contained the bad message. + public string FileName { get; } + + /// Optional additional detail about the corruption. + public string Detail { get; } + + public ErrBadMsg(string fileName, string detail = "") + : base(BuildMessage(fileName, detail)) + { + FileName = fileName; + Detail = detail; + } + + private static string BuildMessage(string fileName, string detail) + { + var baseName = Path.GetFileName(fileName); + return string.IsNullOrEmpty(detail) + ? $"malformed or corrupt message in {baseName}" + : $"malformed or corrupt message in {baseName}: {detail}"; + } +} + +// --------------------------------------------------------------------------- +// FileStoreDefaults — well-known constants +// --------------------------------------------------------------------------- + +/// +/// Well-known constants from filestore.go, exposed for cross-assembly use. +/// +public static class FileStoreDefaults +{ + // Magic / version markers written into block files. + + /// Magic byte used to identify file-store block files. + public const byte FileStoreMagic = 22; + + /// Current block file version. + public const byte FileStoreVersion = 1; + + /// New-format index version. + internal const byte NewVersion = 2; + + /// Header length in bytes for block records. + internal const int HdrLen = 2; + + // Directory names + + /// Top-level directory that holds per-stream subdirectories. + public const string StreamsDir = "streams"; + + /// Directory that holds in-flight batch data for a stream. + public const string BatchesDir = "batches"; + + /// Directory that holds message block files. + public const string MsgDir = "msgs"; + + /// Temporary directory name used during a full purge. + public const string PurgeDir = "__msgs__"; + + /// Temporary directory name for the new message block during purge. + public const string NewMsgDir = "__new_msgs__"; + + /// Directory name that holds per-consumer state. + public const string ConsumerDir = "obs"; + + // File name patterns + + /// Format string for block file names ({index}.blk). + public const string BlkScan = "{0}.blk"; + + /// Suffix for active block files. + public const string BlkSuffix = ".blk"; + + /// Format string for compacted-block staging files ({index}.new). + public const string NewScan = "{0}.new"; + + /// Format string for index files ({index}.idx). + public const string IndexScan = "{0}.idx"; + + /// Format string for per-block encryption-key files ({index}.key). + public const string KeyScan = "{0}.key"; + + /// Glob pattern used to find orphaned key files. + public const string KeyScanAll = "*.key"; + + /// Suffix for temporary rewrite/compression staging files. + public const string BlkTmpSuffix = ".tmp"; + + // Meta files + + /// Stream / consumer metadata file name. + public const string JetStreamMetaFile = "meta.inf"; + + /// Checksum file for the metadata file. + public const string JetStreamMetaFileSum = "meta.sum"; + + /// Encrypted metadata key file name. + public const string JetStreamMetaFileKey = "meta.key"; + + /// Full stream-state snapshot file name. + public const string StreamStateFile = "index.db"; + + /// Encoded TTL hash-wheel persistence file name. + public const string TtlStreamStateFile = "thw.db"; + + /// Encoded message-scheduling persistence file name. + public const string MsgSchedulingStreamStateFile = "sched.db"; + + /// Consumer state file name inside a consumer directory. + public const string ConsumerState = "o.dat"; + + // Block size defaults (bytes) + + /// Default block size for large (limits-based) streams: 8 MB. + public const ulong DefaultLargeBlockSize = 8 * 1024 * 1024; + + /// Default block size for work-queue / interest streams: 4 MB. + public const ulong DefaultMediumBlockSize = 4 * 1024 * 1024; + + /// Default block size used by mirrors/sources: 1 MB. + public const ulong DefaultSmallBlockSize = 1 * 1024 * 1024; + + /// Tiny pool block size (256 KB) — avoids large allocations at low write rates. + public const ulong DefaultTinyBlockSize = 256 * 1024; + + /// Maximum encrypted-head block size: 2 MB. + public const ulong MaximumEncryptedBlockSize = 2 * 1024 * 1024; + + /// Default block size for KV-based streams (same as medium). + public const ulong DefaultKvBlockSize = DefaultMediumBlockSize; + + /// Hard upper limit on block size. + public const ulong MaxBlockSize = DefaultLargeBlockSize; + + /// Minimum allowed block size: 32 KiB. + public const ulong FileStoreMinBlkSize = 32 * 1000; + + /// Maximum allowed block size (same as ). + public const ulong FileStoreMaxBlkSize = MaxBlockSize; + + /// + /// Default block size exposed publicly; resolves to (1 MB) + /// to match the spec note in the porting plan. + /// + public const ulong DefaultBlockSize = DefaultSmallBlockSize; + + // Timing defaults + + /// Default duration before an idle cache buffer is expired: 10 seconds. + public static readonly TimeSpan DefaultCacheBufferExpiration = TimeSpan.FromSeconds(10); + + /// Default interval for background disk sync: 2 minutes. + public static readonly TimeSpan DefaultSyncInterval = TimeSpan.FromMinutes(2); + + /// Default idle timeout before file descriptors are closed: 30 seconds. + public static readonly TimeSpan CloseFdsIdle = TimeSpan.FromSeconds(30); + + /// Default expiration time for idle per-block subject state: 2 minutes. + public static readonly TimeSpan DefaultFssExpiration = TimeSpan.FromMinutes(2); + + // Thresholds + + /// Minimum coalesce size for write batching: 16 KiB. + public const int CoalesceMinimum = 16 * 1024; + + /// Maximum wait time when gathering messages to flush: 8 ms. + public static readonly TimeSpan MaxFlushWait = TimeSpan.FromMilliseconds(8); + + /// Minimum block size before compaction is attempted: 2 MB. + public const int CompactMinimum = 2 * 1024 * 1024; + + /// Threshold above which a record length is considered corrupt: 32 MB. + public const int RlBadThresh = 32 * 1024 * 1024; + + /// Size of the per-record hash checksum in bytes. + public const int RecordHashSize = 8; + + // Encryption key size minimums + + /// Minimum size of a metadata encryption key: 64 bytes. + internal const int MinMetaKeySize = 64; + + /// Minimum size of a block encryption key: 64 bytes. + internal const int MinBlkKeySize = 64; + + // Cache-index bit flags + + /// Bit set in a cache index slot to mark that the checksum has been validated. + internal const uint Cbit = 1u << 31; + + /// Bit set in a cache index slot to mark the message as deleted. + internal const uint Dbit = 1u << 30; + + /// Bit set in a record length field to indicate the record has headers. + internal const uint Hbit = 1u << 31; + + /// Bit set in a sequence number to mark an erased message. + internal const ulong Ebit = 1UL << 63; + + /// Bit set in a sequence number to mark a tombstone. + internal const ulong Tbit = 1UL << 62; +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/MessageBlock.cs b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/MessageBlock.cs new file mode 100644 index 0000000..077ccd5 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/MessageBlock.cs @@ -0,0 +1,382 @@ +// Copyright 2019-2026 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Adapted from server/filestore.go (msgBlock struct and consumerFileStore struct) + +using System.Threading.Channels; +using ZB.MOM.NatsNet.Server.Internal.DataStructures; + +namespace ZB.MOM.NatsNet.Server; + +// --------------------------------------------------------------------------- +// MessageBlock +// --------------------------------------------------------------------------- + +/// +/// Represents a single on-disk message block file together with its +/// in-memory cache and index state. +/// Mirrors the msgBlock struct in filestore.go. +/// +internal sealed class MessageBlock +{ + // ------------------------------------------------------------------ + // Identity fields — first/last use volatile-style access in Go via + // atomic.LoadUint64 on the embedded msgId structs. + // We replicate those as plain fields; callers must acquire _mu before + // reading/writing unless using the Interlocked helpers on Seq/Ts. + // ------------------------------------------------------------------ + + /// First message in this block (sequence + nanosecond timestamp). + public MsgId First; + + /// Last message in this block (sequence + nanosecond timestamp). + public MsgId Last; + + // ------------------------------------------------------------------ + // Lock + // ------------------------------------------------------------------ + + /// Guards all mutable fields in this block. + public readonly ReaderWriterLockSlim Mu = new(LockRecursionPolicy.NoRecursion); + + // ------------------------------------------------------------------ + // Back-reference + // ------------------------------------------------------------------ + + /// Owning file store. + public JetStreamFileStore? Fs { get; set; } + + // ------------------------------------------------------------------ + // File I/O + // ------------------------------------------------------------------ + + /// Path to the .blk message data file. + public string Mfn { get; set; } = string.Empty; + + /// Open file stream for the block data file (null when closed/idle). + public FileStream? Mfd { get; set; } + + /// Path to the per-block encryption key file. + public string Kfn { get; set; } = string.Empty; + + // ------------------------------------------------------------------ + // Compression + // ------------------------------------------------------------------ + + /// Effective compression algorithm when the block was last loaded. + public StoreCompression Cmp { get; set; } + + /// + /// Last index write size in bytes; used to detect whether re-compressing + /// the block would save meaningful space. + /// + public long Liwsz { get; set; } + + // ------------------------------------------------------------------ + // Block identity + // ------------------------------------------------------------------ + + /// Monotonically increasing block index number (used in file names). + public uint Index { get; set; } + + // ------------------------------------------------------------------ + // Counters + // ------------------------------------------------------------------ + + /// User-visible byte count (excludes deleted-message bytes). + public ulong Bytes { get; set; } + + /// + /// Total raw byte count including deleted messages. + /// Used to decide when to roll to a new block. + /// + public ulong RBytes { get; set; } + + /// Byte count captured at the last compaction (0 if never compacted). + public ulong CBytes { get; set; } + + /// User-visible message count (excludes deleted messages). + public ulong Msgs { get; set; } + + // ------------------------------------------------------------------ + // Per-subject state + // ------------------------------------------------------------------ + + /// + /// Optional per-subject state tree for this block. + /// Lazily populated and expired when idle. + /// Mirrors mb.fss in filestore.go. + /// + public SubjectTree? Fss { get; set; } + + // ------------------------------------------------------------------ + // Deleted-sequence tracking + // ------------------------------------------------------------------ + + /// + /// Set of deleted sequence numbers within this block. + /// Uses the AVL-backed to match Go's avl.SequenceSet. + /// + public SequenceSet Dmap { get; set; } = new(); + + // ------------------------------------------------------------------ + // Timestamps (nanosecond Unix times, matches Go int64) + // ------------------------------------------------------------------ + + /// Nanosecond timestamp of the last write to this block. + public long Lwts { get; set; } + + /// Nanosecond timestamp of the last load (cache fill) of this block. + public long Llts { get; set; } + + /// Nanosecond timestamp of the last read from this block. + public long Lrts { get; set; } + + /// Nanosecond timestamp of the last subject-state (fss) access. + public long Lsts { get; set; } + + /// Last sequence that was looked up; used to detect linear scans. + public ulong Llseq { get; set; } + + // ------------------------------------------------------------------ + // Cache + // ------------------------------------------------------------------ + + /// + /// Active in-memory cache. May be null when evicted. + /// Mirrors mb.cache; the elastic-pointer field (mb.ecache) is + /// not ported — a plain nullable field is sufficient here. + /// + public Cache? CacheData { get; set; } + + /// Number of times the cache has been (re)loaded from disk. + public ulong Cloads { get; set; } + + /// Cache buffer expiration duration for this block. + public TimeSpan Cexp { get; set; } + + /// Per-block subject-state (fss) expiration duration. + public TimeSpan Fexp { get; set; } + + /// Timer used to expire the cache buffer when idle. + public Timer? Ctmr { get; set; } + + // ------------------------------------------------------------------ + // State flags + // ------------------------------------------------------------------ + + /// Whether the in-memory cache is currently populated. + public bool HaveCache => CacheData != null; + + /// Whether this block has been closed and must not be written to. + public bool Closed { get; set; } + + /// Whether this block has unflushed data that must be synced to disk. + public bool NeedSync { get; set; } + + /// When true every write is immediately synced (SyncAlways mode). + public bool SyncAlways { get; set; } + + /// When true compaction is suppressed for this block. + public bool NoCompact { get; set; } + + /// + /// When true the block's messages are not tracked in the per-subject index. + /// Used for blocks that only contain tombstone or deleted markers. + /// + public bool NoTrack { get; set; } + + /// Whether a background flusher goroutine equivalent is running. + public bool Flusher { get; set; } + + /// Whether a cache-load is currently in progress. + public bool Loading { get; set; } + + // ------------------------------------------------------------------ + // Write error + // ------------------------------------------------------------------ + + /// Captured write error; non-null means the block is in a bad state. + public Exception? Werr { get; set; } + + // ------------------------------------------------------------------ + // TTL / scheduling counters + // ------------------------------------------------------------------ + + /// Number of messages in this block that have a TTL set. + public ulong Ttls { get; set; } + + /// Number of messages in this block that have a schedule set. + public ulong Schedules { get; set; } + + // ------------------------------------------------------------------ + // Channels (equivalent to Go chan struct{}) + // ------------------------------------------------------------------ + + /// Flush-request channel: signals the flusher to run. + public Channel? Fch { get; set; } + + /// Quit channel: closing signals background goroutine equivalents to stop. + public Channel? Qch { get; set; } + + // ------------------------------------------------------------------ + // Checksum + // ------------------------------------------------------------------ + + /// + /// Last-check hash: 8-byte rolling checksum of the last validated record. + /// Mirrors mb.lchk [8]byte. + /// + public byte[] Lchk { get; set; } = new byte[8]; + + // ------------------------------------------------------------------ + // Test hook + // ------------------------------------------------------------------ + + /// When true, simulates a write failure. Used by unit tests only. + public bool MockWriteErr { get; set; } +} + +// --------------------------------------------------------------------------- +// ConsumerFileStore +// --------------------------------------------------------------------------- + +/// +/// File-backed implementation of . +/// Persists consumer delivery and ack state to a directory under the stream's +/// obs/ subdirectory. +/// Mirrors the consumerFileStore struct in filestore.go. +/// +public sealed class ConsumerFileStore : IConsumerStore +{ + // ------------------------------------------------------------------ + // Fields — mirrors consumerFileStore struct + // ------------------------------------------------------------------ + + private readonly object _mu = new(); + + /// Back-reference to the owning file store. + private readonly JetStreamFileStore _fs; + + /// Consumer metadata (name, created time, config). + private FileConsumerInfo _cfg; + + /// Durable consumer name. + private readonly string _name; + + /// Path to the consumer's state directory. + private readonly string _odir; + + /// Path to the consumer state file (o.dat). + private readonly string _ifn; + + /// Consumer delivery/ack state. + private ConsumerState _state = new(); + + /// Flush-request channel. + private Channel? _fch; + + /// Quit channel. + private Channel? _qch; + + /// Whether a background flusher is running. + private bool _flusher; + + /// Whether a write is currently in progress. + private bool _writing; + + /// Whether the state is dirty (pending flush). + private bool _dirty; + + /// Whether this consumer store is closed. + private bool _closed; + + // ------------------------------------------------------------------ + // Constructor + // ------------------------------------------------------------------ + + /// + /// Creates a new file-backed consumer store. + /// + public ConsumerFileStore(JetStreamFileStore fs, FileConsumerInfo cfg, string name, string odir) + { + _fs = fs; + _cfg = cfg; + _name = name; + _odir = odir; + _ifn = Path.Combine(odir, FileStoreDefaults.ConsumerState); + } + + // ------------------------------------------------------------------ + // IConsumerStore — all methods stubbed + // ------------------------------------------------------------------ + + /// + public void SetStarting(ulong sseq) + => throw new NotImplementedException("TODO: session 18 — filestore ConsumerFileStore.SetStarting"); + + /// + public void UpdateStarting(ulong sseq) + => throw new NotImplementedException("TODO: session 18 — filestore ConsumerFileStore.UpdateStarting"); + + /// + public void Reset(ulong sseq) + => throw new NotImplementedException("TODO: session 18 — filestore ConsumerFileStore.Reset"); + + /// + public bool HasState() + => throw new NotImplementedException("TODO: session 18 — filestore ConsumerFileStore.HasState"); + + /// + public void UpdateDelivered(ulong dseq, ulong sseq, ulong dc, long ts) + => throw new NotImplementedException("TODO: session 18 — filestore ConsumerFileStore.UpdateDelivered"); + + /// + public void UpdateAcks(ulong dseq, ulong sseq) + => throw new NotImplementedException("TODO: session 18 — filestore ConsumerFileStore.UpdateAcks"); + + /// + public void UpdateConfig(ConsumerConfig cfg) + => throw new NotImplementedException("TODO: session 18 — filestore ConsumerFileStore.UpdateConfig"); + + /// + public void Update(ConsumerState state) + => throw new NotImplementedException("TODO: session 18 — filestore ConsumerFileStore.Update"); + + /// + public (ConsumerState? State, Exception? Error) State() + => throw new NotImplementedException("TODO: session 18 — filestore ConsumerFileStore.State"); + + /// + public (ConsumerState? State, Exception? Error) BorrowState() + => throw new NotImplementedException("TODO: session 18 — filestore ConsumerFileStore.BorrowState"); + + /// + public byte[] EncodedState() + => throw new NotImplementedException("TODO: session 18 — filestore ConsumerFileStore.EncodedState"); + + /// + public StorageType Type() => StorageType.FileStorage; + + /// + public void Stop() + => throw new NotImplementedException("TODO: session 18 — filestore ConsumerFileStore.Stop"); + + /// + public void Delete() + => throw new NotImplementedException("TODO: session 18 — filestore ConsumerFileStore.Delete"); + + /// + public void StreamDelete() + => throw new NotImplementedException("TODO: session 18 — filestore ConsumerFileStore.StreamDelete"); +} diff --git a/porting.db b/porting.db index 28f40dd5b39eb3d9b6c57495ba424b43b606f67c..dd7db8a2db7152ce26f4bd7f27420ce40773aa6c 100644 GIT binary patch delta 22757 zcmcJ1d3aOB`hSv>IceH#rL@qdNxA}UDQO8Mlv3JK+On@j5D?jyvTrIVmZaF6BZ}oaz1N_s{R%$LH~5<~{SyJ2US- zUA!3U7sEfxo%+NTsg?S~H%IT-VBch4VNbSgw4b-%u=TR<+>!D8LYsd2s`YQQG6)A& z?SEsae&Z_Fw$dcSNQaPVm@#wS{d4DynKN&~c=Vh7kPfGpr?p%$z|v!kRd5-`&73}K z+Jt!%OyBEtx-#v5F}@7{rB{w#Y`@vA z+RoaJ*mm2t+i%&ww|{B>%>JGs)nGHaj27V!;o6Fd)U~Y)7K5-m+{qwx5)8se_%9@+ zxD5{Nf`yMcvOddxQ@;x(_(? zK8N1p(7POZheKgJ(Js{DZR6vrcR2+8adV7OchO+e32X84a$Azc)ZUb3N;1WWcf{-X z#sHL(WSndqZhFDA+SF)z$W&(3~{KDLyvK235Oau6y#8VLyI|7U(dluIkbpFk8tQ=4lU%+ z0uGJj&<~nD%3}HqH#I*<1!9C$)P7W zbe|RpS2^?phrZy@NkH|gN)}FVKZ2TwvL+nk&`}P3&Y>e5I?SO%96Cr)eZ(RdU()_) ztl&^Nhj_cumHVxfLtQvj!lBL-)hh!4b>vVHhdOY`$D#HdDrBgh*+b)W z?#F2yn#!R%4o%_EWDapw!8nooZGwX8sW~=|=YAZ=p|Knq!=cd}x}QU%v`F}xLtjNu zJ@E#FGZ9G9mmE6Hp;H{X$)R63bb~`bbLhH?>dEv!sgR&+9Qsj=+lV7trxCvWB$pZ?>TgtL*H@eTMk{~&_xbi;86WH96ZmVa~wLW zMaJ$Ns^m~N4yAF(!y$J)2VERW<&cv@DI7}XP)iQA;7}5W5;;R)Y|j0dz@cUwisz7n zLv{|?IArCJg*OCo+>fywGIJ<~LnaQ196}s2a!3FaH3UWj_oJReIxQ0Z=Fndpy33(I zIrImI?hvFJ0^z^hkH2&1KOFjvL;vQ`Z4Uj5L$|cZ*oUGq5kt_M`>_{?dUB`-hiW)f z&7mr5cU|g=ik9@E8(VX~wc=1Vhq5@7$)OAmrE};h4n5DI`VAa>jzjA?^el&-;m|q` z{eweKa|m;2Eq{O3Sl`x-B~iyH8Z#L;hc`}*X{l#|D&Q!+9BRj*d=9ncP#%YJIh4bp zHXN$ImxIGNG?YX4aA*jJYB@BRLxVUpkV6A_PWy8|_Ty0BT{nfr8`~K?|F8eo-__p@ z>(9shpvSXB(T>YEW_JyXgW?zI!}qT-zOXRIWYIZvOLgJ|v=6m1jux=tNBz&brPjQd z&th7bhFZqQeH8n<`EQ#@wr)&#sRIQ>gSif;U5e>x4|E9?$LsV}R$qRmUhl$vJZLa( z-(3$8j|X+ad-j`+NgX>E_yhT&PB5+n#wEize||FV??H*!*8&ydIj_6Y@%v6>!K+)K zEJ~C>iQFMS5EWW|nS=-*Yk|ttk0acnCCZ_LW5{6J8r>Sg;R<_Tey9}4DjxKC@(nSB|A1rzqY`G1l=?!jYsFix7`p6MW4vIC6mKB197m^aDiX+bzyk&Y(eLEpndN8d$p zxNQc^pl%m9EP)=O5imYyg3psk#*fcHvMiA1*O|&5(}O`~>JShz z7WhgQD%I@U#T!V(BeI)PP7VzR%7H~bR~gytXS30KJbSgpjUA68D=upVJ4s~Fg6sxx z7EBM_+f1jg3=H(S=u}U%LT~c~vC;LUM5qRah5?m7IK<~73-V`Y;|*z*?qTrMKQxExfgj zw19E9I0v?LdM+y9mJ_H84FSfA1A3o}p2nTID2NSC!L|07(~4*0g3Rjjl--HQATTF1 zkgO))@#Q2Ft~TePr|`;p__rZ#QG1@Ds?Y#n$PZ^AlMH>XEgFu4scw2Tn&I4h55KVJx9ENlnIC3<5Q zcSW&yXFHV1Ej^eV>I%YIQdKYKvit z?IZna>%q7amXqRy*q-K%F%p_(Tq4Xi$%Z^#2S|E45>Ddf^M!xd0|P^&345^o2zz6B z&P=$Zy|RhyQjQOe0t(+0cAN1-?a^$U@kQjmIDN_uj#IcYId!fT70HgEJyZwFsR;Od zE-zWmcpuutwto3KxMLkUpmuaQY?jHPDKJa%BC_pp>=$%E0nIr{?g(f9)Ec6*BZo0C zJ2V+6Drb|EWq=c0h#%>IQt)F%XfjX9P8ge}eP}!o`C&IR$uy62Mm7BMWAWdek%vx0 zbvB&&8ANB-IRX!drow_s11)?mD!UN{XocqNKUt0Jc*|o>#$$6Lj$mSF5+$kdd2)yh z^8B!+^dO9W*=@lKbK{wLV9aHOCIWM2bFO~Fw9&li`cT!XZJ@yU8&;U!d)}H z^cMVki82x6YF20*a8=97#KeoPFNW%*pKHq_svMn#iRR!LA_asOt`o!{2O;An3aj#>0sy3{lH{JU=cBquvN#pqJxA<#p=Nw zGT{LaW}5NYGDSx@)qC-hE>3sWj?Nkum~QLNqFCVyZeEUt;_?CdnT`f$XeNAlU_-pm zL+)Al;c}Fr$9rQP%94&%C>M?GVVlr&7+w;X?(@)XadkuQ;gw(6mC@!(lovfNCY`Q= z9GB9BU|MK4&{YM>m<{k37UI0VC=GvAi5^#taaQ!iFlqM*%>wCxG4>RZ19PrB7~_mX zkyS5$3H0C8Dn;aszl_i{n5So;!sqf6l5JJ!EzJdhW9g&(QTvUT=Qq3UXow5V0GgTx zJ2^Zc0!IyMp~tuCQo#Y~fjUReN<&QO0T@}?km&P}TO3~16D8?28+Wc3^3vgvje~14 zA1H`2W*Ro5Hz;H3+fZ8x1abIAZ)ML^szsrBKve>li%B`F4^iPYv5J)Kr%??4xla?7 z+29Dx1*##znLbYi;jT?zl&IxyWWtP9q@11 z1s*K8yUlRBsfuh=f_h1$?6+2q=(MS5mFAWK`*FC-H06k@K2uA{2vmauD5}|pzUubi z)zi=;Y^w{Vqn_MlXy_stfTm)Jl^MbCis@*8USl+@GnMP3`Wr=(o~T@shX1BLFi}c^ z@A~HYJRWi)YG;FA8P&hwjGFQKJLX)rW9d>NVLQ-QiBG!CLEG`5qd-R`N?q67xu^|Y zEK^)C-Q`QNiA^w186k9TmviF;}%=u77uFqYM3lJV3NU0 zU=reehlf0fW@z<^VWF;gGic1-;dl;oLra)1|;oXlY28c~yl3G%B5)^j6ScKN# zm3hwS)|qjA_xKq6+M`W$aIlTk0!XXKt`nDv#78@uo{0IMEOg;W6CL2ySW@Mrt5Z3!6mVUNL= zBe9bO16>0C>@h{i>ixb~@&a+ubf1gHA9o)^^E5=TCgu^U<2y_3V7w1RkxrQVrV#>Y zq6{G(vf(kIrmF1`DWC2k#7}hf8$xIq{XQK(LTajRMo4Yp^F877)Opw_p=sE#62i?r zGeOsOmYar8Bcwd-_y!rq2jiGC5shwI1G*@Va8`gPk9aW#K^Tv2E0 z`0i307@rUEA~jq^OVK#S4~>ELEp4j%CrGVfz1{%qHjU5pPk?($<7G;)?FmId>I5C6 zRxp7V7^35^KY<1@Z;ozu-IGlPI$z3$2?_&VpNlHaEl+~iADv*rGVQ@75psJe3nqXx zl8Y*wP0LU(d~1-NZJ6_^rtyATDHDc6L_z~`Jn|`2tvL@zBjfvU*R7}^#%`(u+o(PV zj~78d3Chsu&5068I`sv;SxnB|;1y^V%L+v)?yP8fn#M~c?^H2FF&pJ8;c(O4rbJg( zDj}b`8P!r}7_X*D@>W4MfnHTQ{>5|V4(iZ}63ODJ{QSNO_=%=%)~!YzDD&ziEtg0( zuL2xtl40`~=HowB!Ce&B0MAix6zCjy@;BxTb)EqdN$3>~^m*ug+qwo#)aXD6r($u} z3+9Y;Wk100>j>i`exJWEn7(YnqlRVtISqOMevbG zQo>)DiGOY5PQdqGG^evKl;^mSxnk0U%E?n(KGjXb(1pmi)CCku9muy};Z(u6pN0b! zjn~&ct<0^eKaR2&Mr55atC zD15pv#FtbP_zR2ivCSwIKlc)ZWXE2FFfE!509($kA(dxfibRrs{XvJ6&^uVdD4*O6 z&Ze-L=*3`)qJn_ZhT#Y1x~V6?E0J_yX<)wMz8%>NzC&gANcMqJ9eMeGw}PZB)UD8O zfvt$D?5DOU!cb??OC%vu7^qQn{OK)dJ$~zS#7aH)N)xsMb5zPb#w}R>-7n!C`%eNvUPJUh4TJ^jaOC(qCEmO?lh}R)a67_vRR5AF<>rLfb zE|EMvs4Hf(oNqu5l{yXd{M;V`?l!;C#8L-aNh3gJy~$;!dgk&Q;9W*{1&*i#KeY|= z8)vsE^JPbD9%`kF)6dfhhhyT!Md-3&f9zNGRQ)`g-I{9tFznurTIoq{r@#^HE0G*s zQLv|?zH8q^-?3XmEz0D#l$eDbZG$wKj1H7CXUw0Si7mUq(fs5s$Ua6F1mb2+8S}7* zO5=gb8(g5s?9CnMwr2E+w-pycB)s z{RW3L5x(wgxSu(E;V*WgJP3b>M6PmfSXlyPrXfZmX~2>OKRYSmx-jzSHQelcNBKI< zX2#vtrQyQSR+qhnL^41=$Z9da^7VHiYqaB0i=r@Q9)J|Yn)j5AVC!xvO#zaMK$Viv z{dG6ms+kF-6o*fJq!px}>9G7VB?j;p=Ha^^pcdHmLBza{-UTI_ zBOk(f+x>y^4K}S=BKa*JTuv6b-uVD+z~i2P5(VAT!yhWwfQ6x>r8=0Wc&1W5Fz@E- z4V1@scZ0J|^2n{p23gJ3kVRkK%(PPsphxUx>tE(cmapRW#>E>}9_TMULcR}yD^2q(_!#`ts5@cXt6XHtdSI6HFtF|qm=#+*U@uythO3F#{E2c> z*g_Me1ysHe=TRMA^9g!^S$bL&Ox>qVlLTX>hv>IUm2Z#Pho0j{$Ko#gxee4yBqLcu zGIKPOIcGnlJ)(I}$g!Dl*-su3pZUd{!EQ)_G>gmR*7U-%Ni707=H5r$*{5uMTVOXDm%<_l3l|Y8S(w3c_7$GobE5o!SP2? z5}t7oGW3I9X7!dN{L?`$>Cj7B2t+wklmcLnLvWg+X@pCM6pvQbc7@V|FkbbIKxT=! z-C>AXUf2N29x6HzF8*lFhr6@b;!$NO5t{{PizG6=fpjIs zmwXIKZ0)d&Bz)$WQY>v3QKG^bokKr04!XrfAa`7HKfu?Fwnm12^nomB-1r7JeF+fptpQ3#SorLI+ zCYh+;`|HW37OPOI*Y3xC-+-?gNlL}yFTYWwWLDOogHqPw=6U2QzGe@&Ws-5tgmMX$ z^OY~amy2#b%@_|prAUD(hcY=1C|Xx41(v0!AoiOZj5rfO!7};m@=ljY#?>DXlzkj@ z8jJ$1{6Xd@0gGR1^>E-pndBgg15QQpRDTJM5w&i#ZY!KoObBD7wJZW7IlwwK@0Dl3 zRgT05HoW7EA}6L7Qe~2C?M)(3YB+D5L9b~X^n+h1J{M!VUM5+}iU8bLT6RnP8ZJ3a zhEhND)YsZ{RG?lasox@!I;G)`^DLxhDJL{}_3c^ix>d;rU>kCiR+>9cJBRveB);LC zQa531%$G?bGam+1ZFc<}RKKI!1A`rS>Ul*!F^Z0|4k)0`Nc#rx^XE||ja(qjWx=D) zLkV!?Z{}<^wy*RkjLn0aPt9rK1*rN(PL3IGyU?Uk5cEikV4*&UJ(v}_b^*MPXh!qI zMMdPQ&jFXxrk~Z?FwX9@#adJ1PC-hKYDMOJ$5|26WyOj_E_=`=w*l@4L#e<;>KwS|WweF0 zQP53wec!aqQ6Q7FNbkT<<;+a~9(|-bTUI>$pH1^RLu8T`fz}9?Hrn)0kms!)EbVN= z#aEP5qjEG{&VmUlgW%0kXLIot^t0x?Kl4LVA6t|&fuM>cYG`8W+aJ)=EH4R5u;G`l z{!cDJ&H#>T`jTJ~mi+To^ng0vf}i?PS%Ipn^>R8);3IV~a+Lkq>GRf&52Es-LSIKsY*&jFS(-n-BT|m?q z%Ar&VB;SCGI(S4Rg9AjGz>A#AQF1Db_eZQy?k{Mr#!)%;i=vOzW&-_xpw3mGvwdz$ z(HB_fS+>M|VW^9}Zk`)63B7InuQ5&6-kxCVZ40MgiZOw4sFX<}uoC=Ps%Ey_1YOEH zm`JfY|5s)IsS_;J41r@f=IJMA_ zhI_2AT51h8crld=FQ>@+sYea73}X#j4C@R}7;YFY8BQAZ8FmQ+gl<9yp_R~5h&BAl zUP=jT*@G-$6nW!i?8Dw#MP6Fr+~w9+@v;@xe%Lk$S=@;0Uq)PSGdR(YTLmY6*eaxA zXrjz5=K8P^*N2U`K5UezZKQg@^7G~jRt_xf6nD2xWc-j@=2|Vn3 zc%Apb5Ag0TAOB)}mm=k0K$}ZRGmbja>h_@eY21kc=iIAKHX!{jYeIS;(nBp`FHf zoI}Srbd*D%bLa?%4s+-bhYoV+GhXBexF0{|(0&f>| z$e|B7^gf5)}Itq>Modnhd8kyBi|nABv$|xa{<6rGNp*UnRTP_=wm4&Yuja0 z+$)n}R9Tf$re2mP)=_>U;fIKQNh0h0W}()Daxtu+kYopFEpcX&xJ+X_y)8u649h-0 zLnZ~OQV2kDNbzG@3vsb#u)C$m`smo;GMQAN)a8z83CrE=PHI=108UhF)?RjP)()$)cgKF;yI`w7^kQ{;W%6t=18W zU?;gNP!vNVh$X&GyTrpRD+vT$Xa+d-H1Y1(c_wpP|M%fz_Jv2)EcV-|`Zqpij@@VGe^@)bCJOX^aR z3OL^^P$&;1vbFlW#4+)ElW}2|n2L+j#eAGTFEtzgodxd-)~Ab5*H#-yE9C(|1g&;n z+5+v(W<=-SG+Xgs>98F%e7kub+?BFiQN)guP7$Xxy0zaZmq~YU4Sa*-ApG7;@{J_C zGea!JhGl|#ZNBKhnVF)ds1Keq=@4!*ZxVhaQ!Ha~3Z3$Lrn1NEwj{`;Ik+m|_jzfc z-Y!dgg`J2fgU7Q(BkuTMs*<~lm-_%ofAF)tq?O3;&0`Cklr1)Bq;er!geJ<{0$Ub4 zepBV%z+%OGpO+r&`&)@iR0=cx=R>KvY+SY64aW6`<LWQ29 zzzn$;@KX|q^LdF!==Zi@!rIhEq~0tO*1{Y&>x+&=K7mDYPne`HFhi3ikLHNW@yT_H zY++}b^q08YZkPZ9=!KtvFD#pN@QmPF>e%xr(f-L|IqAd~vR3{H^?^3mYbPhw(k( z1Zd7{67JVdR9f&s=*{q{cFMw3{^rS*L?9qP?u_Pp#n~jE#l(KhtB~0v`9hKa(e+Ia z#}tS&1Y52=13m)f3oC6)TjCQ-nD(Mk(QiJH^@2shwNR@LBK%{5e`3O|I+)KLhR#@B zJ$UZKXI~kfyuxZUgbT{EXNlx-74CFh$i?%1g1;lt?0ax!V}{}*H^6u8cTH%m$HG-% zOgQC70onlndqwyJfBC&oi+{Te{!IhDsga2{oOXo!;%*Y0#?Q~kzN#^eyG5R?Q#57RPl=}zYz zwr5al(OTkrLJz_~@)(3=therRUc;H;NM7`_u#>4Ty1BM}kw2nhb_9z;^*}ikYVDp% zxHI_FZs)uFdRPkpWe6ItNe-@W19Zv}{~+vn$C;&PDODQgMV1d$hq|#$1!`H9-O|z9dba` zs}??01Z8=aUVVR$bBV_2FD#0W#SeeXJsB7&^#hXL!EB$$Pel6W$IdIPe*693J zdY>XqbyBO;n@$R4DoPr^-x(=9^?=OPmHkepA5}d!Od@ZmY9O^p{k*JCopqXo^n0H= znV+YQA1{%&Q_wZQp1}wQoO9IFWgOmkz{#G$sN-uTVjuD!RvIO49B|HMhw9Qncrvu? zv!)BUPwD{+@CTvvNo#nAK6C!Y7y^PO-!7Agj`sFeO1=4qocB>34M~jVc=sV?=Tue( zNyHCPEAan12Yjyk#sTYhdgGx2c!LRxpjvq~W)3kBrQ73zXqne5usRqV_YofN!6dZCU z;^&V#%V`P;mR&}VzDm(vBE~P$?dH$U!4V1(&pYO9tLchJ`wJ3#Cypt{Lrpx2J9IWf zts^$y+!o!6ahs665VbRofE_B|)Ky zFERb3b1ObM#>|XbuP>Y$Um`F}9uB+~gJ+OvFoS_lWCniy7tWCy1D|`!$zlmA#XukV zULb;$qv}Kr%okhWMW>wINudOuX%PqU)+t3NFrspqG}vg}LP|8?G~D1Q(TArM#mY2Q zsay+0&HjWs4!-85!PVr9}&(-TsbiXTzL#4z#%Xhz5rGSNjmUglh}cxRDJHX;#f zC{`MTl^u6Jt?9EjpHKwH=4mGnqVtT2%tQU_B>cfw&R1}=^~$=|e(lu8U}I#`_X5ua zsSk!{oO3Q#ina7;nWhzk!=WYXm^##%H{-(Hd~3JQCJX0}m{D|Eb`Da|1nU_~r#CGjr-hHS$oH z2smSvtn!Pnm61BA4IjP;_kzSBs<-s_MQ7;mi>cwiFS-onnZ)xFra0|mY&q68mW6Ro z#tt{X6m!v(EM}l##%8APgel>|S?(&q9>|tcU?MNr7uqiSV}`pc%k@GLH~|mObTh-o zcBGq3TEnY?aJ{L>vVHj5EO!dtk?9U=H0+dYw-s;7Qb;3?ehZkVxJ0RLUdVFa&*ViF z2~cP&r*q{bn5PK(3|Noz)@=8acw5e*r%)HMEEq+@L8VtrB?2Vnos*% zE8ze;f)B{a@ae(;SVtF$qHnc^Pe=3Bmrt8>)lDdqo5P3m`YI*D8*SVWoScq4rTX7E zqn(_f$`U^Pc833;Svk5YBYcWcWMf8{GejSvJ2W%4Y+8-xVBlgPEWo%^6>h7R7QYiTK4fh`Zj z1GFo8me=jocyh3IgunD^lQM9qcNMq|v)X;Cvy(rOw;x%d!REublGs|qf6S|6o--XX zC32E<^%U{|KLzdc2cganHcC(-qiB#Br}Gd&{C46e3N; zmE>s~Ej-q~>R!c8s^c{`vvVvfRxh=H_s$Q5h@U$c@`m(n$79dC7b%yT{Go-Sjc(@f zv2S+|5!beN0G^)F1ZM44_ZQTarX7^Eue*JkUA-?ve5+#Uj-g)U;5Xc}HNESUZ4CPi4E5 zEIDC#KKOp@jYw!an6GHJX5E{LuA-wG`bflLR4qwoMlZbLUH7P{SXn?73FJ1Ay?6$+ zO<#%h9uxsXw6$o{2kyxl>G(h74A?_b8Vv3UEr;~@cG~Th+3}@o1yQn7(}o@dK3O1}Y!uYHH`Y z9CP2x?i~Fy9tV$ssUY4DJIA~93yIVp0I3(q(}51087sZnPwoQ)96e!B9A|XwAa|Ch z!eHn>$fr6!Kb`%xmXZ5G_0nd7$X_q9;7|9vneWUfJIHlFSq79uH~I5jjMDWf=mPps zE=t+(DR-*-$Wwq4Vu3u$;GR$2qnO_8dI0<`vL>ees^!Tr4*bkKnk-v&06Y)s_QN{2<3hp*#^N)Go#)N8Qz$0{@>!xeH$`Phg){ zc0Px>$(#1Tv7|m3jbBV3a|WVX9?KS>=5>?trGxH@sNX*5km7SFJ}gVezYO7-KB1wL zOj>paH$W+cXkGF*0Eo>9pFiSm)1=?QitiqQzl6~LsB&Vd?ruEpZanU3Jf7BgJiYOF zM&t3!UB|QHj-#3270e7fp3nZ(Zi?xnd(Kdx!|je+mf+*tv)`RmVEF(3fAW`#@YY}B z2NJ96V}Grvd%xz*!rQKi=k?)+%c934Ld3=*AfT>oMN!vXb=B{8?|mm3`2U~h`*xqnZo;|uo_qTFojWU5 zQ0)qQR(rMW)}&Nx+wB;)YlAb{vC(xjNEr-PnL(}?A{~ z!KVLBPu*szpgC7(i}uy&yXfhIMDL(iZBhztmhk`f5PgGA=cYDE-%)4ylWsBV^JuQ5 zZ;bAj^mps%A)Ay+lkJj&R@<>=7k+4ep)cB`j?}{by2U0rbowc@!K@z|&9>+b`V_l9 zRX1Vwtj3v>=1($Qi~o%{XoF4)(qTI6VxpaOl9%S|u#1=d_=`=lQaaQ(CTo%;Ry zQ~F%}d3_&!iGHzureUgSo9SuOYE#%W-!$2Dn{9<{u5F_2R{a=#z2Oeiho+;ZHwReH12l1J}$SW;=Z)8ds(BW*5?gBE4#lIUwri<543S{(E# z{BV97uNC8W^q}36PnVi4H9B%L9qq7mq+J;8*LKSYy2Nf77Cmc}Zqm{LL|RYx*er+W zRf2QVt(FFQ+<_mx9G1@XWuw$N8n#+Wwb9?qmRv2ZFYQGBfzHrNjnu4{8VK{YJ+!z*yOYzkFuKD|OAp6&%zj4im857?`@Meo#;*E# z+TZ`*{~N{LUubF3&KxJL+L+TTDh+in)keoYX4tei-!w_%(mbG%CXquV*Dy{`bzf<} z)jVJ?Fzz?noE6q0$77bs=IIWJ8RGcp11=JhoS~vfceh4cZVwbDYqeQ4w*#rAI|Hfd z^ium&16|#L6wwvs2D2k05(HhbJ&?ueKI%X!X}80Al`fb}3TVqadXvHWtClb}wOVKB zo=64O^j;W9E7W0cM@MoW%{*sKr`tP{D%$9An;rckJ;m|%a-A-V8olHey7rVioi=(& z{}@RRkko?1PHdNXDdaXf_hjs3aA>1%rVu+FbDPCS>;Gr9YKs{Ih1zIADwz&8vLof# zAjclC=dcY;gdV{1}uc4-NlA&GCBzl)KG&yo7HV8}!*aPgK7t={2_3XhxRT-p*IAd^RG)M}j z1k&0wk~JA*3LTnl;3DzRPMIW2&<%`?0$u*3Kw2WBo0v%^#?k#JlVl3IJ0rsx&!vI1 zVn%m)8A+i1vd9SPnUj!Ct8z#?`cO8p&=0dnwpbC248w{tdmxQVW5syVfp*D;nz?3T zEAj+!ZDc6h9>lTj+tYt$lM34BCh!d6K7wvWo7aBBe#1r+Un4dKq}wx!CdkxUHh;rou*HR9zkdmtTJlP@W^eT4u(erQW>wo zoE*9#ha}UMPUMtew`~>8CO%=QWs#BK#a|XkO=Tjn=9A%Z#Bx5#r7gc1E%wN*Agdmw za&k1EETZAZJ?XSQ0%MxdnfUp3&RLNWV4^If4Wx4W+ufOjV(m-Fq~I*mg!P@|jX*r#1% zKW5Ffow3X^T`>jWr?(rX>xXLw>IyU^8k*rs$aIC4MP^{*ictGNDr1K}9wckxME`a_ z;-p*d^XBTLA$0q{ynQ^O;gNBSf5_uH{@*M{6rbA<%Ha0M`7iGV{&~!p5*dR4{7SJH11JK&qdy6AF;cahj_uQB~}i5t)V!%a$??!+O`1 zkWd_5MX9P`$Nb1t&{fZ2CIhLiY;)EjXhzjh_)8m)Gp%R9_iDMoMUF8x;!V4LVV0SGRLKGWq}0x?@2RnBW@yL;o!zL>%rKnPKS$eDk&&(@Ut~6R z8`9(sq%qTveqBW}wRGrzB;>I*q&=RU`hf zgACG9%`_JJCnbclB6C1q6RH%EGFV(p3+hNZP3}wXrwyG`c(Sq4*@J`&WYU(OEW|N5 z(g=wlwWXCZ`$(=qYTHz4OQ%J@BXDo3QH9{iiOj_AwV~dD)F2aGPLMtsBq`L~kF28y zyFtp_0^wi*VS%Q#v?@<`Q$mv?au-(iZ*nq&VL}hkZ>vZK%^N^|j5C<>KFAw))~XVu z&cQj?Tus)<-B?@Qlo&{3{u5nSOOmv-VNVA&TU{E6*fQq0nI{&DBeDS0Ec}VsmN^jN zFP7~+6vX*lqaICUJ_ySZCAegR#hs}3EpHO7xP{zJ4|?JBEw`u=b~PnL=7D%bcy=JI zH{&6%jwEVnHy^0>_eB(3HYgU=>LPPNIV1#19@X{@BJbWP`F96n2~*vkdZKk@koPrq z%{Ln31ZQ7+$Z*WoAPuyRwv0CI)qkveTlRi=boY`_B<1$pIR?+@>WIOfz4P^xqUV{Fpi|asoCCgsKGx%?qG=5up|m4?VHV%fqPIIYRb;t|pw- zR{V9OId_w{TdB*$!9!#>lMdX2XSs0;$+)+MC$1+n((e|>{LU#mKwTNiSN+bvggkjYwS_Kw$DF0= zJwdiJ@+P%vidNoB(&O3Kw>i;7o0keRG#Sv;one9CR6^Jyci?O#2)0_4_w7=$gzgNA zOxl4yUE{XWEz3kSH9HIB_E=TJG|1})6zRKZ_|FVggC~~5e3(Tx*Sic*;YN+K&he;y zz3qbanl;yQt7aqZu!1PAa6sB8vmhVG3O+)u1R2wxsX6yfE0t=x@q8cqzP@(yNin4G2Iv*@*+c@WUY zfhIV_>N`^`4!Ia~Ls)IVQaQD*giWHPA~1pef$ta02~fvMWX0R= zLondEf$#gzmBdYhrD>fUqh&wnOJ@aAd42r*eehjQ$MYMwMGj%zXocJrbOBs0gPnh2 z6}eS}85Hj&BFrq83%S%$xAVJfh>(ZHUZ6>$<75bm?=?p5g1u_77t5`5qfBPfxd(A) zoN1>)C04tSkqhGKniSCSPMP!8;eO;^zTYi!KIq^MB3MkiAMOxdBqVO9-`x*)sK3o& zvCDZND@6anBYWusWJ2q)HhSOzm>+9X9C9aI!ykf5cq^cN@VfS6+k?!lnMi_iF81@| zSWfq^2gy)TuW8Dxe5^d6xG(W(D%M2LNQ@p)apPMaK7=CNr#{^~nFOW3B!iv;sUet$!58!06o7GJ{uoqrzhl zNU*q&{Nn0;dNVKT#7N{0l}xOH!Pj7+B8h9Wy}pATkd-0CvZ zUKds3EOK8caUgPYDp$@&>(NHExy||~h>Je|I4fgd#GZTPYEV})YA&ywLV9jJNu}fo zGM&zS0LI6S6sx+5h}Bc>g9ZIV^8%@*tUb8=1UXLEH72Qq-#v-2b>%Oy_~w>-gSs54 zPuO(EljONpTeVS8cj4Q5d~y{CYgy|UE4c4?ns{l;0k4scdWx*Neifn4r@;~T!|`$@ z1XDh#?TAD=^Jy|Z&be_8Uty+)#%R-M)kVF@F<9<}W$1+Y#c9tCq(Rs^zwnU_;=*Wd z41F|C?QD(o7CC8HuOF)$tDPNf_Y4`Ob%kcicYy1{P?j1~E^j1}$L3nqsLW4o-h}YW z_(U1TpqR=K{FbA0r#2xza{uNh-nTh+p|SF9;H(T6;@$D_&A@iTb&0Bkr#_43l=Xo+ z&8@7u9GVa=l1F0aI&>@CAA`kZ^z`%SR-S$i<=B~L(H8K=koP#to`-$mA4F^Ko+&d> zW-vUtoV!c8So!O7fa3a0cB+jL79rNM?y#H8z?q8B0=4!y`8-l$*b?iY`0n@H-MP-t zV43x&t@71wfqZw4$1Tv!_7JvYkXgrD9O|z|#t*ijGWuZwS`E;+=!O>n&T-MrlE-tx zWR>v33owW_0Oi3KW1(@P%;1gSLNzqL^CHTGSZcA+Ia^ucXR*6h9t*m1D4&Pg-CNNS zbGo8!q`M#)Av4gU8YCPz3vWYA;BEvGH%)pehEepC83yUERMWtym&izwnOj~`3vfrZ z%rHney$IOemjOcY(|BOt{4&ct41UX(N8y?SnYS?44d#^5cQ?TqA9)3ZhWQofv0)yB z6Ehc=_=oiqBp+LE^4Sf4YOILw`i;gr$lpn>ZoT8O{a0;Y+t=2g%uhvq+exn06|R>V zXjB@mRioX?9ps8YmU(Dw-pPasU3-W;l@W$2L~#(z$)a8Np``hKC*YcxwA^&VF4fyT zcgPd5z#m>DWD(3srK|TK8veM8oTuwtSt{%QeHGTn7&e%`(sW`roaGE9%Tpku0)#h? z)BdmiWLo)6nnXt(j&&6Qxsh!jPEg%=&0zpz;d^75kDJFsN1zgBfgYF4fUn9XHNOOr zSX*8v?W3O@A*Ogs#4?^)O<5MN#;wlk#Q%hb$nzobSf3Xx&Zq8EsH7h`hDi-aHCqbN zO&E`>?TjAFs5?{VZ6B(W7h`4qG%?Kx7ANu=g>HP$JBSNa8osD;QpROEzH>@vHY_pI z3&&%{U5UI9Yg-GAKvXe;p+GGhr^sL8Ao+8rV8+Z-U~deE@`o~1 z$R_zTk%hf+@tir$vIx4pPIi5|#-*LA!LVGi53;SXUa-0?Rx>ePF_uU->8iC;Ngu<# zdfGcX<9%9Hubt%#ja1B#MPRJzt&86!GX%83%eurfY8rLiu9!eqdZ!vvhn_*J!Y4MI z?&=wFDHCm(l@oP_G86`HWdVKWBG~snvTGY~Fz7Hb(!391>gc0bLENcYg{sGYfHva> z;;%l4$)ZtVI9D*FSEZZtAwqR*j^Uvne5lr;&czDDgvvr*HNGT$gzVC1SGpRaPkj_i zZ=Dqh$LE7ro+({tNtq~YIefeHY)oBg3PZRCvMz_0oo}5bTU*OcJ6-b++>ym9r%qwm zQST7khz|+Q{{tWsN28F}?DX+-!XFqRL)c0gY{jecPtE}a<25xZV|BVWK+%Ca;6!!~ zL0=0AMQ5OK{ zwN={hTtHgqbS3g~(6u^>|2;SgS2}N9o_$Z1wWD5M2D$<`BTsFK7s*t5CJ_iVcf4&E zg?%#DSc(e=P+E)J@byK&794ouXy)lpMBp?vr@KkAc9upr!RbX?+tKrH9KHBGt7;rw~ zbA}pxW4G89)*ouFWZ{tQr&0mO7 zklATgvOrkF+INmZT>gTr7f2D`anqNvqHKziiIe-+u|5XZ`SwdDABHHc_zLqAo>mP? z2KEZDDFCy;U`_`8{!27vKYoP>$5S6@ljxSOS&CpK{&FQ93(9f0m|_t`o!2mk`1Hu z%Kn04tEo4_*Trz(TY^@6`F$^&cHQp(WCCl~Ixs(=v(i42vv4IKCj(a?K7R4O^uG@|-Pmu6lfhpdil!LIAjg`37A4`zd z#t}Z)P7-)nQV)fpQ2D5i61mjX3h(=az9ia^EOn)6iIOqSVn!rNnOqb)bBB0W8Lcq< zt+GaixECi$)5Up+L5XzSdFUsly`+{}iL7^$xMPmO zP`Cs z46Ljbc{gfEk;bVrY998ukv^U(Wr&T-m0ln(Lmwa-p$}3egzX)e4{_?#KB=>CjC&OJ z8mk6jo24vFs9POl`_hy_bY>a|Jrgojl<{zygandaNR@NziX{bAfI|dCCU&cYkR=Y{m-9FL-}}j`BsKhC}8HIe928O zWk?cD%>-0Xl$2t2_fzVz2!n7xf9T`S5sNngeEgltIWnPnEUGf81DXKF&hfS=cZ+7G?SbZ@q_T3AUNOW_=tH(TOqr`&=igONA< z{E3UdK)qUy)CA0kbAC(fATE@dDX>%Ac0=Z4*qE3H)H93%exo|IfcSF7{^ z-9TKJKT_D&Ny2-gy=eBge}HDcHV+qVdBeyJb_R=4QxZaRl$*g&dB_^@v#1vIw--X% zoQG$OXD+Aa(eK~)n(4xPUlzZCL!;Eu(TX-J&P?1d`5V}eq{xGz_^A5!S)pH=Bbpm7n3H~0_|2D$2;Q1HU({42-KBRFDtBQypd~vW zCa(-gO<`~g@SqjsZM(bVC z@U^6VI?OA{o@ho(>I-B|y;&rXhki*~C`Ak|3qDqSZ1~vmap2>`$Aym@9}hkW__V_( z5uYS{+T+s!pJaSGMh!0S?;Dbhzi7-lV9-yJLj4)*{g(CS9VVx4w&PH=<96=|B$%<0 zWl-x77L<4=v~j2R)404bV;A0gv`v~vMi}x^ig$i!tq?@DuX?`{K@=}E)e>YzgrOywAkx zq40IDcnrZt&jy!fD;~7&_oivLvRKjN0+u)&+mvEPAbQ@t`@Jiv?!TB^^Q5(;6gB0N z12LhGl?Q=(U^pj`7Q~@+#{us@<2qGt*(VNqF(+ZAMYhahX$5-&!$r{U@7|sNKh)}s zq-e0)V*8`#<0Ve^VJ}8a>^TbW(0C*Hx5Hk%++j%l6q!AU#ehOQozx%k&J~+-;-`;z z@g9cJjg(o?EL@^aVy+(X&J*Ut>Fz)36&Sm7oIC(r2Ezb(IgEMysQ2f%)4w|ARYwGU zWERK#165qV@VNI*p-5hf?mq4n6Ew#NnWf*>Il+&|y_3ZN6TP9GHl6SytFuN|W(l{o z(LH&>J2kFS2W6|N;|oVcdLG_X`RMzJgp5#yPUx@e@f!_iyYncBku!LH>9Il!hG=&Y>Qu^N*Ru0 zFhp;yl7`bA4QwpJ%BiLgy?H`5#mZ1F&ha-S-CNIjUl5~6o=FQoh7+h|jFz$xw+?{P zM1{@F@P3;Y4)m^%y^G>Xq{Q=ZZ+_?53LBh_EDiWsj~ny{1oN}cd&jFx9q@UYFT{AS zRoMJ2zM@K|ptaN47rZ6R<8TYHz)CM(fFgL6m#d7%P5R?q`}H+J(M9hDb>hJax8EqC}1%+HxVk($YqhSU00#w)t0yTupPIYW6$N9^xM;O4JO?#c4?itDOB&+>^kcAgrA zy=bZl!y&nNasqUEj;|xF%=Sg;na^X>gtQ!=8W3ug_Sm?rTm@>U!@SFogXuPd}jNyg5_A*7j9RxSL{2|ND~!OL9ktuN>K)OX^2ly=y;piTukujcvg z5aV19Io_M^QxkWwlF%wE?DuNEuQINMyR*P&r1_n(DNkE|#e>uRVZ8C=lAYe!cPOr} z1-069PE**cxc-=|act$u0v|E}d%VI62s~-`i}cf3aWO8TDGo9Kqf4S2y7;o=Soom} zq{?!YbE(4K#TBs8I;ZYl=u@9!avbKjLKN+s%@Tz@iSvhnM0lBy%DVcF#qC(^kFn`h z>^MFR)hK@r)aVCa9HDo021NLtAL3_*xTnacYwVh#acb_?l2k(jzG`N(^flkEt2Q+o zpOOx1=Q;BneQkX;ck_@M>K5tiX8P*q3@wVVtUV0S0Dmj?uWi1ibnRl+qp-o*6EFEn zg&=N;u-2itEMVu5YS|{=Nb%Z}3+%uqUsq1&uEEx@EK~s;1uf2?{5uc6&AuwS$&T+* z!~{&}qnmvhV)LR1Ytvg1_mi7_6>-HG9)-H-fM;U{;fqv(I0G`_UEb1XVf_5D5I3+3 z&%$(hdiv~k>blq;Mfd%bGM#~xCHfy9hf2(-acN_imXzcqehhFum-%l%$ z8SdWt!1o`!VeoAj?0`M7xHm{_VO zpQ^25X}c%VcD>Bz9Hlst4;%aJ^-Yb-AMfqOWtq-t%YEMcjwNzBh->lcfHxEg`+Q35 zJ(G8||{t^9SG%e1`!Fdzgf#CLS+KqJ;;2 z{o^VJvLFR-bXj3$8lruP3G)RAGoY}y9?0@`?s>+;$Xsm}!5gl{hht7rq09g=h8~4H zs=av_4vhhZ`VCI@J<8@IJ_|jU=FFty-qI6CP+@OADzSivjYUU%gG6|VeKpiZ-#X$m z(f=J`&dqZ9U}YLspdKyY{1hMcjS+!s+fmdXoTOHn3KG;;1zcC!V@S$;iV6}Ztv!Yx zCy!!3VRjQW&od|4_H9`BD%Q~$Dokdmk>M9TaL$!zgB4w`?n%u$+X=`U+=hj zvgst!DKAt$o|bHD3)yJyVa|`bp2@l9bXq>u z{9XII#&X7Tz_Q)4fsSjj^<&e9656=hc$WHheZc-#w<=uDQq2zNb9y=*!1F z=e5!B-#vRI`pU2Fl4$ySl1u$E@;mQ%7Dh*$@@$0=espKhnm^ob+Wola?&$p|Jtf-z E18cMvPyhe` diff --git a/reports/current.md b/reports/current.md index e551db1..ad44671 100644 --- a/reports/current.md +++ b/reports/current.md @@ -1,6 +1,6 @@ # NATS .NET Porting Status Report -Generated: 2026-02-26 21:02:04 UTC +Generated: 2026-02-26 21:06:51 UTC ## Modules (12 total) @@ -13,9 +13,9 @@ Generated: 2026-02-26 21:02:04 UTC | Status | Count | |--------|-------| -| complete | 1736 | +| complete | 2048 | | n_a | 77 | -| not_started | 1767 | +| not_started | 1455 | | stub | 93 | ## Unit Tests (3257 total) @@ -36,4 +36,4 @@ Generated: 2026-02-26 21:02:04 UTC ## Overall Progress -**2324/6942 items complete (33.5%)** +**2636/6942 items complete (38.0%)** diff --git a/reports/report_5a2c8a3.md b/reports/report_5a2c8a3.md new file mode 100644 index 0000000..ad44671 --- /dev/null +++ b/reports/report_5a2c8a3.md @@ -0,0 +1,39 @@ +# NATS .NET Porting Status Report + +Generated: 2026-02-26 21:06:51 UTC + +## Modules (12 total) + +| Status | Count | +|--------|-------| +| complete | 11 | +| not_started | 1 | + +## Features (3673 total) + +| Status | Count | +|--------|-------| +| complete | 2048 | +| n_a | 77 | +| not_started | 1455 | +| stub | 93 | + +## Unit Tests (3257 total) + +| Status | Count | +|--------|-------| +| complete | 319 | +| n_a | 181 | +| not_started | 2533 | +| stub | 224 | + +## Library Mappings (36 total) + +| Status | Count | +|--------|-------| +| mapped | 36 | + + +## Overall Progress + +**2636/6942 items complete (38.0%)**