feat: port session 01 — Foundation Types (const, errors, proto, ring, rate_counter, sdm)
Ports server/const.go, errors.go, proto.go, ring.go, rate_counter.go, sdm.go. - ServerConstants: all protocol constants and version info from const.go - ServerErrors: ~60 sentinel exceptions plus errCtx/configErr/processConfigErr types - ProtoWire: protobuf varint encode/decode helpers (proto.go) - RateCounter: sliding-window rate limiter (rate_counter.go) - ClosedRingBuffer: fixed-size ring buffer for /connz (ring.go) - StreamDeletionMeta: SDM tracking for JetStream cluster consensus (sdm.go) - 5 unit tests passing (errors, ring buffer, rate counter) - errors_gen.go (code generator tool) and nkey.go Server methods marked n_a
This commit is contained in:
118
dotnet/src/ZB.MOM.NatsNet.Server/Internal/ClosedRingBuffer.cs
Normal file
118
dotnet/src/ZB.MOM.NatsNet.Server/Internal/ClosedRingBuffer.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
// Copyright 2018-2025 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/ring.go in the NATS server Go source.
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Internal;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Placeholder types — will be populated in session 08 (Client) and
|
||||
// session 12 (Monitor/Events).
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Subset of client connection info exposed on the monitoring endpoint (/connz).
|
||||
/// Full implementation is in session 12 (monitor.go).
|
||||
/// </summary>
|
||||
public class ConnInfo { }
|
||||
|
||||
/// <summary>
|
||||
/// Subscription detail for the monitoring endpoint.
|
||||
/// Full implementation is in session 12 (monitor.go).
|
||||
/// </summary>
|
||||
public class SubDetail { }
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// ClosedClient / ClosedRingBuffer (ring.go)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Wraps connection info with optional items for the /connz endpoint.
|
||||
/// Mirrors <c>closedClient</c> in server/ring.go.
|
||||
/// </summary>
|
||||
public sealed class ClosedClient
|
||||
{
|
||||
public ConnInfo Info { get; init; } = new();
|
||||
public IReadOnlyList<SubDetail> Subs { get; init; } = [];
|
||||
public string User { get; init; } = string.Empty;
|
||||
public string Account { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fixed-size ring buffer that retains the most recent closed connections,
|
||||
/// evicting the oldest when full.
|
||||
/// Mirrors <c>closedRingBuffer</c> in server/ring.go.
|
||||
/// </summary>
|
||||
public sealed class ClosedRingBuffer
|
||||
{
|
||||
private ulong _total;
|
||||
private readonly ClosedClient?[] _conns;
|
||||
|
||||
/// <summary>Creates a ring buffer that holds at most <paramref name="max"/> entries.</summary>
|
||||
public ClosedRingBuffer(int max)
|
||||
{
|
||||
_conns = new ClosedClient?[max];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a closed connection, evicting the oldest if the buffer is full.
|
||||
/// Mirrors <c>closedRingBuffer.append</c>.
|
||||
/// </summary>
|
||||
public void Append(ClosedClient cc)
|
||||
{
|
||||
_conns[Next()] = cc;
|
||||
_total++;
|
||||
}
|
||||
|
||||
// Index of the slot to write next — wraps around.
|
||||
private int Next() => (int)(_total % (ulong)_conns.Length);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the number of entries currently stored (≤ capacity).
|
||||
/// Mirrors <c>closedRingBuffer.len</c>.
|
||||
/// </summary>
|
||||
public int Len() =>
|
||||
_total > (ulong)_conns.Length ? _conns.Length : (int)_total;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the total number of connections ever appended (not capped).
|
||||
/// Mirrors <c>closedRingBuffer.totalConns</c>.
|
||||
/// </summary>
|
||||
public ulong TotalConns() => _total;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a chronologically ordered copy of the stored closed connections.
|
||||
/// The caller may freely modify the returned array.
|
||||
/// Mirrors <c>closedRingBuffer.closedClients</c>.
|
||||
/// </summary>
|
||||
public ClosedClient?[] ClosedClients()
|
||||
{
|
||||
var len = Len();
|
||||
var dup = new ClosedClient?[len];
|
||||
var head = Next();
|
||||
|
||||
if (_total <= (ulong)_conns.Length || head == 0)
|
||||
{
|
||||
Array.Copy(_conns, dup, len);
|
||||
}
|
||||
else
|
||||
{
|
||||
var fp = _conns.AsSpan(head); // oldest … end of array
|
||||
var sp = _conns.AsSpan(0, head); // wrap-around … newest
|
||||
fp.CopyTo(dup.AsSpan());
|
||||
sp.CopyTo(dup.AsSpan(fp.Length));
|
||||
}
|
||||
|
||||
return dup;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
// Copyright 2025 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/sdm.go in the NATS server Go source.
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Internal.DataStructures;
|
||||
|
||||
/// <summary>
|
||||
/// Per-sequence data tracked by <see cref="StreamDeletionMeta"/>.
|
||||
/// Mirrors <c>SDMBySeq</c> in server/sdm.go.
|
||||
/// </summary>
|
||||
public readonly struct SdmBySeq
|
||||
{
|
||||
/// <summary>Whether this sequence was the last message for its subject.</summary>
|
||||
public bool Last { get; init; }
|
||||
|
||||
/// <summary>Timestamp (nanoseconds UTC) when the removal/SDM was last proposed.</summary>
|
||||
public long Ts { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tracks pending subject delete markers (SDMs) and message removals for a stream.
|
||||
/// Used by JetStream cluster consensus to avoid redundant proposals.
|
||||
/// Mirrors <c>SDMMeta</c> in server/sdm.go.
|
||||
/// </summary>
|
||||
public sealed class StreamDeletionMeta
|
||||
{
|
||||
// Per-subject pending-count totals.
|
||||
private readonly Dictionary<string, ulong> _totals = new(1);
|
||||
|
||||
// Per-sequence data keyed by sequence number.
|
||||
private readonly Dictionary<ulong, SdmBySeq> _pending = new(1);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Header constants (forward-declared; populated in session 19 — JetStream).
|
||||
// isSubjectDeleteMarker checks these header keys.
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// Mirrors JSMarkerReason header key (defined in jetstream.go).
|
||||
internal const string HeaderJsMarkerReason = "Nats-Marker-Reason";
|
||||
|
||||
// Mirrors KVOperation header key (defined in jetstream.go).
|
||||
internal const string HeaderKvOperation = "KV-Operation";
|
||||
|
||||
// Mirrors KVOperationValuePurge (defined in jetstream.go).
|
||||
internal const string KvOperationValuePurge = "PURGE";
|
||||
|
||||
/// <summary>
|
||||
/// Returns true when the given header block contains a subject delete marker
|
||||
/// (either a JetStream marker or a KV purge operation).
|
||||
/// Mirrors <c>isSubjectDeleteMarker</c> in server/sdm.go.
|
||||
/// </summary>
|
||||
public static bool IsSubjectDeleteMarker(ReadOnlySpan<byte> hdr)
|
||||
{
|
||||
// Simplified header scan: checks whether JSMarkerReason key is present
|
||||
// or whether KV-Operation equals "PURGE".
|
||||
// Full implementation depends on SliceHeader from session 08 (client.go).
|
||||
// Until then this provides the correct contract.
|
||||
var text = System.Text.Encoding.UTF8.GetString(hdr);
|
||||
if (text.Contains(HeaderJsMarkerReason))
|
||||
return true;
|
||||
if (text.Contains($"{HeaderKvOperation}: {KvOperationValuePurge}"))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all tracked data.
|
||||
/// Mirrors <c>SDMMeta.empty</c>.
|
||||
/// </summary>
|
||||
public void Empty()
|
||||
{
|
||||
_totals.Clear();
|
||||
_pending.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tracks <paramref name="seq"/> as pending and returns whether it was
|
||||
/// the last message for its subject. If the sequence is already tracked
|
||||
/// the existing <c>Last</c> value is returned without modification.
|
||||
/// Mirrors <c>SDMMeta.trackPending</c>.
|
||||
/// </summary>
|
||||
public bool TrackPending(ulong seq, string subj, bool last)
|
||||
{
|
||||
if (_pending.TryGetValue(seq, out var p))
|
||||
return p.Last;
|
||||
|
||||
_pending[seq] = new SdmBySeq { Last = last, Ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L };
|
||||
_totals[subj] = _totals.TryGetValue(subj, out var cnt) ? cnt + 1 : 1;
|
||||
return last;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes <paramref name="seq"/> and decrements the pending count for
|
||||
/// <paramref name="subj"/>, deleting the subject entry when it reaches zero.
|
||||
/// Mirrors <c>SDMMeta.removeSeqAndSubject</c>.
|
||||
/// </summary>
|
||||
public void RemoveSeqAndSubject(ulong seq, string subj)
|
||||
{
|
||||
if (!_pending.Remove(seq))
|
||||
return;
|
||||
|
||||
if (_totals.TryGetValue(subj, out var msgs))
|
||||
{
|
||||
if (msgs <= 1)
|
||||
_totals.Remove(subj);
|
||||
else
|
||||
_totals[subj] = msgs - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
284
dotnet/src/ZB.MOM.NatsNet.Server/Internal/ProtoWire.cs
Normal file
284
dotnet/src/ZB.MOM.NatsNet.Server/Internal/ProtoWire.cs
Normal file
@@ -0,0 +1,284 @@
|
||||
// Copyright 2024 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/proto.go in the NATS server Go source.
|
||||
// Inspired by https://github.com/protocolbuffers/protobuf-go/blob/master/encoding/protowire/wire.go
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Low-level protobuf wire format helpers used internally for JetStream API encoding.
|
||||
/// Mirrors server/proto.go.
|
||||
/// </summary>
|
||||
public static class ProtoWire
|
||||
{
|
||||
private static readonly Exception ErrInsufficient =
|
||||
new InvalidOperationException("insufficient data to read a value");
|
||||
|
||||
private static readonly Exception ErrOverflow =
|
||||
new InvalidOperationException("too much data for a value");
|
||||
|
||||
private static readonly Exception ErrInvalidFieldNumber =
|
||||
new InvalidOperationException("invalid field number");
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Field scanning
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Reads a complete protobuf field (tag + value) from <paramref name="b"/>
|
||||
/// and returns the field number, wire type, and total byte size consumed.
|
||||
/// Mirrors <c>protoScanField</c>.
|
||||
/// </summary>
|
||||
public static (int num, int typ, int size, Exception? err) ScanField(ReadOnlySpan<byte> b)
|
||||
{
|
||||
var (num, typ, sizeTag, err) = ScanTag(b);
|
||||
if (err != null) return (0, 0, 0, err);
|
||||
|
||||
var (sizeValue, err2) = ScanFieldValue(typ, b[sizeTag..]);
|
||||
if (err2 != null) return (0, 0, 0, err2);
|
||||
|
||||
return (num, typ, sizeTag + sizeValue, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a protobuf tag varint and returns field number, wire type, and bytes consumed.
|
||||
/// Mirrors <c>protoScanTag</c>.
|
||||
/// </summary>
|
||||
public static (int num, int typ, int size, Exception? err) ScanTag(ReadOnlySpan<byte> b)
|
||||
{
|
||||
var (tagint, size, err) = ScanVarint(b);
|
||||
if (err != null) return (0, 0, 0, err);
|
||||
|
||||
if ((tagint >> 3) > int.MaxValue)
|
||||
return (0, 0, 0, ErrInvalidFieldNumber);
|
||||
|
||||
var num = (int)(tagint >> 3);
|
||||
if (num < 1)
|
||||
return (0, 0, 0, ErrInvalidFieldNumber);
|
||||
|
||||
var typ = (int)(tagint & 7);
|
||||
return (num, typ, size, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the byte count consumed by a field value with the given wire type.
|
||||
/// Mirrors <c>protoScanFieldValue</c>.
|
||||
/// </summary>
|
||||
public static (int size, Exception? err) ScanFieldValue(int typ, ReadOnlySpan<byte> b)
|
||||
{
|
||||
switch (typ)
|
||||
{
|
||||
case 0: // varint
|
||||
{
|
||||
var (_, size, err) = ScanVarint(b);
|
||||
return (size, err);
|
||||
}
|
||||
case 5: // fixed32
|
||||
return (4, null);
|
||||
case 1: // fixed64
|
||||
return (8, null);
|
||||
case 2: // length-delimited
|
||||
{
|
||||
var (size, err) = ScanBytes(b);
|
||||
return (size, err);
|
||||
}
|
||||
default:
|
||||
return (0, new InvalidOperationException($"unsupported type: {typ}"));
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Varint decode
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Decodes a protobuf varint from <paramref name="b"/>.
|
||||
/// Returns (value, bytes_consumed, error).
|
||||
/// Mirrors <c>protoScanVarint</c>.
|
||||
/// </summary>
|
||||
public static (ulong v, int size, Exception? err) ScanVarint(ReadOnlySpan<byte> b)
|
||||
{
|
||||
if (b.Length < 1) return (0, 0, ErrInsufficient);
|
||||
ulong v = b[0];
|
||||
if (v < 0x80) return (v, 1, null);
|
||||
v -= 0x80;
|
||||
|
||||
if (b.Length < 2) return (0, 0, ErrInsufficient);
|
||||
ulong y = b[1];
|
||||
v += y << 7;
|
||||
if (y < 0x80) return (v, 2, null);
|
||||
v -= 0x80UL << 7;
|
||||
|
||||
if (b.Length < 3) return (0, 0, ErrInsufficient);
|
||||
y = b[2];
|
||||
v += y << 14;
|
||||
if (y < 0x80) return (v, 3, null);
|
||||
v -= 0x80UL << 14;
|
||||
|
||||
if (b.Length < 4) return (0, 0, ErrInsufficient);
|
||||
y = b[3];
|
||||
v += y << 21;
|
||||
if (y < 0x80) return (v, 4, null);
|
||||
v -= 0x80UL << 21;
|
||||
|
||||
if (b.Length < 5) return (0, 0, ErrInsufficient);
|
||||
y = b[4];
|
||||
v += y << 28;
|
||||
if (y < 0x80) return (v, 5, null);
|
||||
v -= 0x80UL << 28;
|
||||
|
||||
if (b.Length < 6) return (0, 0, ErrInsufficient);
|
||||
y = b[5];
|
||||
v += y << 35;
|
||||
if (y < 0x80) return (v, 6, null);
|
||||
v -= 0x80UL << 35;
|
||||
|
||||
if (b.Length < 7) return (0, 0, ErrInsufficient);
|
||||
y = b[6];
|
||||
v += y << 42;
|
||||
if (y < 0x80) return (v, 7, null);
|
||||
v -= 0x80UL << 42;
|
||||
|
||||
if (b.Length < 8) return (0, 0, ErrInsufficient);
|
||||
y = b[7];
|
||||
v += y << 49;
|
||||
if (y < 0x80) return (v, 8, null);
|
||||
v -= 0x80UL << 49;
|
||||
|
||||
if (b.Length < 9) return (0, 0, ErrInsufficient);
|
||||
y = b[8];
|
||||
v += y << 56;
|
||||
if (y < 0x80) return (v, 9, null);
|
||||
v -= 0x80UL << 56;
|
||||
|
||||
if (b.Length < 10) return (0, 0, ErrInsufficient);
|
||||
y = b[9];
|
||||
v += y << 63;
|
||||
if (y < 2) return (v, 10, null);
|
||||
|
||||
return (0, 0, ErrOverflow);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Length-delimited decode
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Returns the total byte count consumed by a length-delimited field
|
||||
/// (length varint + content).
|
||||
/// Mirrors <c>protoScanBytes</c>.
|
||||
/// </summary>
|
||||
public static (int size, Exception? err) ScanBytes(ReadOnlySpan<byte> b)
|
||||
{
|
||||
var (l, lenSize, err) = ScanVarint(b);
|
||||
if (err != null) return (0, err);
|
||||
|
||||
if (l > (ulong)(b.Length - lenSize))
|
||||
return (0, ErrInsufficient);
|
||||
|
||||
return (lenSize + (int)l, null);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Varint encode
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a <see cref="ulong"/> as a protobuf varint.
|
||||
/// Mirrors <c>protoEncodeVarint</c>.
|
||||
/// </summary>
|
||||
public static byte[] EncodeVarint(ulong v)
|
||||
{
|
||||
if (v < 1UL << 7)
|
||||
return [(byte)v];
|
||||
|
||||
if (v < 1UL << 14)
|
||||
return [(byte)((v >> 0) & 0x7F | 0x80), (byte)(v >> 7)];
|
||||
|
||||
if (v < 1UL << 21)
|
||||
return [
|
||||
(byte)((v >> 0) & 0x7F | 0x80),
|
||||
(byte)((v >> 7) & 0x7F | 0x80),
|
||||
(byte)(v >> 14)];
|
||||
|
||||
if (v < 1UL << 28)
|
||||
return [
|
||||
(byte)((v >> 0) & 0x7F | 0x80),
|
||||
(byte)((v >> 7) & 0x7F | 0x80),
|
||||
(byte)((v >> 14) & 0x7F | 0x80),
|
||||
(byte)(v >> 21)];
|
||||
|
||||
if (v < 1UL << 35)
|
||||
return [
|
||||
(byte)((v >> 0) & 0x7F | 0x80),
|
||||
(byte)((v >> 7) & 0x7F | 0x80),
|
||||
(byte)((v >> 14) & 0x7F | 0x80),
|
||||
(byte)((v >> 21) & 0x7F | 0x80),
|
||||
(byte)(v >> 28)];
|
||||
|
||||
if (v < 1UL << 42)
|
||||
return [
|
||||
(byte)((v >> 0) & 0x7F | 0x80),
|
||||
(byte)((v >> 7) & 0x7F | 0x80),
|
||||
(byte)((v >> 14) & 0x7F | 0x80),
|
||||
(byte)((v >> 21) & 0x7F | 0x80),
|
||||
(byte)((v >> 28) & 0x7F | 0x80),
|
||||
(byte)(v >> 35)];
|
||||
|
||||
if (v < 1UL << 49)
|
||||
return [
|
||||
(byte)((v >> 0) & 0x7F | 0x80),
|
||||
(byte)((v >> 7) & 0x7F | 0x80),
|
||||
(byte)((v >> 14) & 0x7F | 0x80),
|
||||
(byte)((v >> 21) & 0x7F | 0x80),
|
||||
(byte)((v >> 28) & 0x7F | 0x80),
|
||||
(byte)((v >> 35) & 0x7F | 0x80),
|
||||
(byte)(v >> 42)];
|
||||
|
||||
if (v < 1UL << 56)
|
||||
return [
|
||||
(byte)((v >> 0) & 0x7F | 0x80),
|
||||
(byte)((v >> 7) & 0x7F | 0x80),
|
||||
(byte)((v >> 14) & 0x7F | 0x80),
|
||||
(byte)((v >> 21) & 0x7F | 0x80),
|
||||
(byte)((v >> 28) & 0x7F | 0x80),
|
||||
(byte)((v >> 35) & 0x7F | 0x80),
|
||||
(byte)((v >> 42) & 0x7F | 0x80),
|
||||
(byte)(v >> 49)];
|
||||
|
||||
if (v < 1UL << 63)
|
||||
return [
|
||||
(byte)((v >> 0) & 0x7F | 0x80),
|
||||
(byte)((v >> 7) & 0x7F | 0x80),
|
||||
(byte)((v >> 14) & 0x7F | 0x80),
|
||||
(byte)((v >> 21) & 0x7F | 0x80),
|
||||
(byte)((v >> 28) & 0x7F | 0x80),
|
||||
(byte)((v >> 35) & 0x7F | 0x80),
|
||||
(byte)((v >> 42) & 0x7F | 0x80),
|
||||
(byte)((v >> 49) & 0x7F | 0x80),
|
||||
(byte)(v >> 56)];
|
||||
|
||||
return [
|
||||
(byte)((v >> 0) & 0x7F | 0x80),
|
||||
(byte)((v >> 7) & 0x7F | 0x80),
|
||||
(byte)((v >> 14) & 0x7F | 0x80),
|
||||
(byte)((v >> 21) & 0x7F | 0x80),
|
||||
(byte)((v >> 28) & 0x7F | 0x80),
|
||||
(byte)((v >> 35) & 0x7F | 0x80),
|
||||
(byte)((v >> 42) & 0x7F | 0x80),
|
||||
(byte)((v >> 49) & 0x7F | 0x80),
|
||||
(byte)((v >> 56) & 0x7F | 0x80),
|
||||
1];
|
||||
}
|
||||
}
|
||||
81
dotnet/src/ZB.MOM.NatsNet.Server/Internal/RateCounter.cs
Normal file
81
dotnet/src/ZB.MOM.NatsNet.Server/Internal/RateCounter.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright 2021-2025 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/rate_counter.go in the NATS server Go source.
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// A sliding-window rate limiter that allows at most <c>limit</c> events
|
||||
/// per <see cref="Interval"/> window.
|
||||
/// Mirrors <c>rateCounter</c> in server/rate_counter.go.
|
||||
/// </summary>
|
||||
public sealed class RateCounter
|
||||
{
|
||||
private readonly long _limit;
|
||||
private long _count;
|
||||
private ulong _blocked;
|
||||
private DateTime _end;
|
||||
|
||||
// Exposed for tests (mirrors direct field access in rate_counter_test.go).
|
||||
public TimeSpan Interval;
|
||||
|
||||
private readonly object _lock = new();
|
||||
|
||||
public RateCounter(long limit)
|
||||
{
|
||||
_limit = limit;
|
||||
Interval = TimeSpan.FromSeconds(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the event is within the rate limit for the current window.
|
||||
/// Mirrors <c>rateCounter.allow</c>.
|
||||
/// </summary>
|
||||
public bool Allow()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (now > _end)
|
||||
{
|
||||
_count = 0;
|
||||
_end = now + Interval;
|
||||
}
|
||||
else
|
||||
{
|
||||
_count++;
|
||||
}
|
||||
|
||||
var allow = _count < _limit;
|
||||
if (!allow)
|
||||
_blocked++;
|
||||
return allow;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns and resets the count of blocked events since the last call.
|
||||
/// Mirrors <c>rateCounter.countBlocked</c>.
|
||||
/// </summary>
|
||||
public ulong CountBlocked()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var blocked = _blocked;
|
||||
_blocked = 0;
|
||||
return blocked;
|
||||
}
|
||||
}
|
||||
}
|
||||
228
dotnet/src/ZB.MOM.NatsNet.Server/ServerConstants.cs
Normal file
228
dotnet/src/ZB.MOM.NatsNet.Server/ServerConstants.cs
Normal file
@@ -0,0 +1,228 @@
|
||||
// Copyright 2012-2025 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/const.go in the NATS server Go source.
|
||||
|
||||
using System.Reflection;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Server-wide constants and version information.
|
||||
/// Mirrors server/const.go.
|
||||
/// </summary>
|
||||
public static class ServerConstants
|
||||
{
|
||||
// Server version — mirrors VERSION in const.go.
|
||||
public const string Version = "2.14.0-dev";
|
||||
|
||||
// Protocol version — mirrors PROTO in const.go.
|
||||
public const int Proto = 1;
|
||||
|
||||
// Default port for client connections — mirrors DEFAULT_PORT.
|
||||
public const int DefaultPort = 4222;
|
||||
|
||||
// Sentinel port value that triggers a random port selection — mirrors RANDOM_PORT.
|
||||
public const int RandomPort = -1;
|
||||
|
||||
// Default bind address — mirrors DEFAULT_HOST.
|
||||
public const string DefaultHost = "0.0.0.0";
|
||||
|
||||
// Maximum allowed control line size (4 KB) — mirrors MAX_CONTROL_LINE_SIZE.
|
||||
public const int MaxControlLineSize = 4096;
|
||||
|
||||
// Maximum allowed payload size (1 MB) — mirrors MAX_PAYLOAD_SIZE.
|
||||
public const int MaxPayloadSize = 1024 * 1024;
|
||||
|
||||
// Payload size above which the server warns — mirrors MAX_PAYLOAD_MAX_SIZE.
|
||||
public const int MaxPayloadMaxSize = 8 * 1024 * 1024;
|
||||
|
||||
// Maximum outbound pending bytes per client (64 MB) — mirrors MAX_PENDING_SIZE.
|
||||
public const int MaxPendingSize = 64 * 1024 * 1024;
|
||||
|
||||
// Default maximum connections allowed (64 K) — mirrors DEFAULT_MAX_CONNECTIONS.
|
||||
public const int DefaultMaxConnections = 64 * 1024;
|
||||
|
||||
// TLS handshake timeout — mirrors TLS_TIMEOUT.
|
||||
public static readonly TimeSpan TlsTimeout = TimeSpan.FromSeconds(2);
|
||||
|
||||
// Fallback delay before sending INFO when using TLSHandshakeFirst
|
||||
// — mirrors DEFAULT_TLS_HANDSHAKE_FIRST_FALLBACK_DELAY.
|
||||
public static readonly TimeSpan DefaultTlsHandshakeFirstFallbackDelay = TimeSpan.FromMilliseconds(50);
|
||||
|
||||
// Auth timeout — mirrors AUTH_TIMEOUT.
|
||||
public static readonly TimeSpan AuthTimeout = TimeSpan.FromSeconds(2);
|
||||
|
||||
// How often pings are sent — mirrors DEFAULT_PING_INTERVAL.
|
||||
public static readonly TimeSpan DefaultPingInterval = TimeSpan.FromMinutes(2);
|
||||
|
||||
// Maximum pings outstanding before disconnect — mirrors DEFAULT_PING_MAX_OUT.
|
||||
public const int DefaultPingMaxOut = 2;
|
||||
|
||||
// CR LF end-of-line — mirrors CR_LF.
|
||||
public const string CrLf = "\r\n";
|
||||
|
||||
// Length of CR_LF — mirrors LEN_CR_LF.
|
||||
public const int LenCrLf = 2;
|
||||
|
||||
// Write/flush deadline — mirrors DEFAULT_FLUSH_DEADLINE.
|
||||
public static readonly TimeSpan DefaultFlushDeadline = TimeSpan.FromSeconds(10);
|
||||
|
||||
// Default monitoring port — mirrors DEFAULT_HTTP_PORT.
|
||||
public const int DefaultHttpPort = 8222;
|
||||
|
||||
// Default monitoring base path — mirrors DEFAULT_HTTP_BASE_PATH.
|
||||
public const string DefaultHttpBasePath = "/";
|
||||
|
||||
// Minimum sleep on temporary accept errors — mirrors ACCEPT_MIN_SLEEP.
|
||||
public static readonly TimeSpan AcceptMinSleep = TimeSpan.FromMilliseconds(10);
|
||||
|
||||
// Maximum sleep on temporary accept errors — mirrors ACCEPT_MAX_SLEEP.
|
||||
public static readonly TimeSpan AcceptMaxSleep = TimeSpan.FromSeconds(1);
|
||||
|
||||
// Route solicitation interval — mirrors DEFAULT_ROUTE_CONNECT.
|
||||
public static readonly TimeSpan DefaultRouteConnect = TimeSpan.FromSeconds(1);
|
||||
|
||||
// Maximum route solicitation interval — mirrors DEFAULT_ROUTE_CONNECT_MAX.
|
||||
public static readonly TimeSpan DefaultRouteConnectMax = TimeSpan.FromSeconds(30);
|
||||
|
||||
// Route reconnect delay — mirrors DEFAULT_ROUTE_RECONNECT.
|
||||
public static readonly TimeSpan DefaultRouteReconnect = TimeSpan.FromSeconds(1);
|
||||
|
||||
// Route dial timeout — mirrors DEFAULT_ROUTE_DIAL.
|
||||
public static readonly TimeSpan DefaultRouteDial = TimeSpan.FromSeconds(1);
|
||||
|
||||
// Default route pool size — mirrors DEFAULT_ROUTE_POOL_SIZE.
|
||||
public const int DefaultRoutePoolSize = 3;
|
||||
|
||||
// LeafNode reconnect interval — mirrors DEFAULT_LEAF_NODE_RECONNECT.
|
||||
public static readonly TimeSpan DefaultLeafNodeReconnect = TimeSpan.FromSeconds(1);
|
||||
|
||||
// TLS timeout for leaf nodes — mirrors DEFAULT_LEAF_TLS_TIMEOUT.
|
||||
public static readonly TimeSpan DefaultLeafTlsTimeout = TimeSpan.FromSeconds(2);
|
||||
|
||||
// Proto snippet size for parse error display — mirrors PROTO_SNIPPET_SIZE.
|
||||
public const int ProtoSnippetSize = 32;
|
||||
|
||||
// Max control line snippet size for error display — mirrors MAX_CONTROL_LINE_SNIPPET_SIZE.
|
||||
public const int MaxControlLineSnippetSize = 128;
|
||||
|
||||
// Maximum MSG proto argument count — mirrors MAX_MSG_ARGS.
|
||||
public const int MaxMsgArgs = 4;
|
||||
|
||||
// Maximum RMSG proto argument count — mirrors MAX_RMSG_ARGS.
|
||||
public const int MaxRMsgArgs = 6;
|
||||
|
||||
// Maximum HMSG proto argument count — mirrors MAX_HMSG_ARGS.
|
||||
public const int MaxHMsgArgs = 7;
|
||||
|
||||
// Maximum PUB proto argument count — mirrors MAX_PUB_ARGS.
|
||||
public const int MaxPubArgs = 3;
|
||||
|
||||
// Maximum HPUB proto argument count — mirrors MAX_HPUB_ARGS.
|
||||
public const int MaxHPubArgs = 4;
|
||||
|
||||
// Maximum RS+/LS+ proto argument count — mirrors MAX_RSUB_ARGS.
|
||||
public const int MaxRSubArgs = 6;
|
||||
|
||||
// Maximum closed connections retained — mirrors DEFAULT_MAX_CLOSED_CLIENTS.
|
||||
public const int DefaultMaxClosedClients = 10000;
|
||||
|
||||
// Lame duck spread duration — mirrors DEFAULT_LAME_DUCK_DURATION.
|
||||
public static readonly TimeSpan DefaultLameDuckDuration = TimeSpan.FromMinutes(2);
|
||||
|
||||
// Lame duck grace period — mirrors DEFAULT_LAME_DUCK_GRACE_PERIOD.
|
||||
public static readonly TimeSpan DefaultLameDuckGracePeriod = TimeSpan.FromSeconds(10);
|
||||
|
||||
// Leaf node INFO wait — mirrors DEFAULT_LEAFNODE_INFO_WAIT.
|
||||
public static readonly TimeSpan DefaultLeafNodeInfoWait = TimeSpan.FromSeconds(1);
|
||||
|
||||
// Default leaf node port — mirrors DEFAULT_LEAFNODE_PORT.
|
||||
public const int DefaultLeafNodePort = 7422;
|
||||
|
||||
// Connect error report threshold — mirrors DEFAULT_CONNECT_ERROR_REPORTS.
|
||||
public const int DefaultConnectErrorReports = 3600;
|
||||
|
||||
// Reconnect error report threshold — mirrors DEFAULT_RECONNECT_ERROR_REPORTS.
|
||||
public const int DefaultReconnectErrorReports = 1;
|
||||
|
||||
// RTT measurement interval — mirrors DEFAULT_RTT_MEASUREMENT_INTERVAL.
|
||||
public static readonly TimeSpan DefaultRttMeasurementInterval = TimeSpan.FromHours(1);
|
||||
|
||||
// Default allowed response max messages — mirrors DEFAULT_ALLOW_RESPONSE_MAX_MSGS.
|
||||
public const int DefaultAllowResponseMaxMsgs = 1;
|
||||
|
||||
// Default allowed response expiration — mirrors DEFAULT_ALLOW_RESPONSE_EXPIRATION.
|
||||
public static readonly TimeSpan DefaultAllowResponseExpiration = TimeSpan.FromMinutes(2);
|
||||
|
||||
// Default service export response threshold — mirrors DEFAULT_SERVICE_EXPORT_RESPONSE_THRESHOLD.
|
||||
public static readonly TimeSpan DefaultServiceExportResponseThreshold = TimeSpan.FromMinutes(2);
|
||||
|
||||
// Default service latency sampling rate — mirrors DEFAULT_SERVICE_LATENCY_SAMPLING.
|
||||
public const int DefaultServiceLatencySampling = 100;
|
||||
|
||||
// Default system account name — mirrors DEFAULT_SYSTEM_ACCOUNT.
|
||||
public const string DefaultSystemAccount = "$SYS";
|
||||
|
||||
// Default global account name — mirrors DEFAULT_GLOBAL_ACCOUNT.
|
||||
public const string DefaultGlobalAccount = "$G";
|
||||
|
||||
// Default account fetch timeout — mirrors DEFAULT_ACCOUNT_FETCH_TIMEOUT.
|
||||
public static readonly TimeSpan DefaultAccountFetchTimeout = TimeSpan.FromMilliseconds(1900);
|
||||
|
||||
// VCS commit hash embedded at build time, shortened to 7 chars — mirrors gitCommit.
|
||||
// Populated from AssemblyInformationalVersion metadata if available.
|
||||
public static readonly string GitCommit;
|
||||
|
||||
static ServerConstants()
|
||||
{
|
||||
// Mirror const.go init(): extract VCS revision from build info.
|
||||
// In .NET we read the AssemblyInformationalVersion attribute which
|
||||
// is typically set to the semantic version + commit hash by dotnet publish.
|
||||
var infoVersion = typeof(ServerConstants).Assembly
|
||||
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
|
||||
?.InformationalVersion;
|
||||
|
||||
if (infoVersion != null)
|
||||
{
|
||||
// Convention: "1.2.3+abcdefg" or "1.2.3-dev+abcdefg"
|
||||
var plusIdx = infoVersion.IndexOf('+');
|
||||
if (plusIdx >= 0)
|
||||
{
|
||||
var rev = infoVersion[(plusIdx + 1)..];
|
||||
GitCommit = FormatRevision(rev);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
GitCommit = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Truncates a VCS revision string to 7 characters for display.
|
||||
/// Mirrors <c>formatRevision</c> in const.go.
|
||||
/// </summary>
|
||||
public static string FormatRevision(string revision) =>
|
||||
revision.Length >= 7 ? revision[..7] : revision;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server control commands — mirrors the <c>Command</c> type in const.go.
|
||||
/// </summary>
|
||||
public enum ServerCommand
|
||||
{
|
||||
Stop,
|
||||
Quit,
|
||||
Reopen,
|
||||
Reload,
|
||||
}
|
||||
446
dotnet/src/ZB.MOM.NatsNet.Server/ServerErrors.cs
Normal file
446
dotnet/src/ZB.MOM.NatsNet.Server/ServerErrors.cs
Normal file
@@ -0,0 +1,446 @@
|
||||
// Copyright 2012-2025 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/errors.go in the NATS server Go source.
|
||||
// Note: errorsUnwrap() and ErrorIs() are Go 1.12 compat shims — .NET has
|
||||
// Exception.InnerException and errors.Is() equivalents built in;
|
||||
// both are represented here for completeness but map to built-in .NET patterns.
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Sentinel error values (mirrors package-level var block in errors.go)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Standard NATS server error sentinels.
|
||||
/// Mirrors the package-level <c>var</c> error block in server/errors.go.
|
||||
/// </summary>
|
||||
public static class ServerErrors
|
||||
{
|
||||
public static readonly Exception ErrConnectionClosed =
|
||||
new InvalidOperationException("connection closed");
|
||||
|
||||
public static readonly Exception ErrAuthentication =
|
||||
new InvalidOperationException("authentication error");
|
||||
|
||||
public static readonly Exception ErrAuthTimeout =
|
||||
new InvalidOperationException("authentication timeout");
|
||||
|
||||
public static readonly Exception ErrAuthExpired =
|
||||
new InvalidOperationException("authentication expired");
|
||||
|
||||
public static readonly Exception ErrAuthProxyNotTrusted =
|
||||
new InvalidOperationException("proxy is not trusted");
|
||||
|
||||
public static readonly Exception ErrAuthProxyRequired =
|
||||
new InvalidOperationException("proxy connection required");
|
||||
|
||||
public static readonly Exception ErrMaxPayload =
|
||||
new InvalidOperationException("maximum payload exceeded");
|
||||
|
||||
public static readonly Exception ErrMaxControlLine =
|
||||
new InvalidOperationException("maximum control line exceeded");
|
||||
|
||||
public static readonly Exception ErrReservedPublishSubject =
|
||||
new InvalidOperationException("reserved internal subject");
|
||||
|
||||
public static readonly Exception ErrBadPublishSubject =
|
||||
new InvalidOperationException("invalid publish subject");
|
||||
|
||||
public static readonly Exception ErrBadSubject =
|
||||
new InvalidOperationException("invalid subject");
|
||||
|
||||
public static readonly Exception ErrBadQualifier =
|
||||
new InvalidOperationException("bad qualifier");
|
||||
|
||||
public static readonly Exception ErrBadClientProtocol =
|
||||
new InvalidOperationException("invalid client protocol");
|
||||
|
||||
public static readonly Exception ErrTooManyConnections =
|
||||
new InvalidOperationException("maximum connections exceeded");
|
||||
|
||||
public static readonly Exception ErrTooManyAccountConnections =
|
||||
new InvalidOperationException("maximum account active connections exceeded");
|
||||
|
||||
public static readonly Exception ErrLeafNodeLoop =
|
||||
new InvalidOperationException("leafnode loop detected");
|
||||
|
||||
public static readonly Exception ErrTooManySubs =
|
||||
new InvalidOperationException("maximum subscriptions exceeded");
|
||||
|
||||
public static readonly Exception ErrTooManySubTokens =
|
||||
new InvalidOperationException("subject has exceeded number of tokens limit");
|
||||
|
||||
public static readonly Exception ErrClientConnectedToRoutePort =
|
||||
new InvalidOperationException("attempted to connect to route port");
|
||||
|
||||
public static readonly Exception ErrClientConnectedToLeafNodePort =
|
||||
new InvalidOperationException("attempted to connect to leaf node port");
|
||||
|
||||
public static readonly Exception ErrLeafNodeHasSameClusterName =
|
||||
new InvalidOperationException("remote leafnode has same cluster name");
|
||||
|
||||
public static readonly Exception ErrLeafNodeDisabled =
|
||||
new InvalidOperationException("leafnodes disabled");
|
||||
|
||||
public static readonly Exception ErrConnectedToWrongPort =
|
||||
new InvalidOperationException("attempted to connect to wrong port");
|
||||
|
||||
public static readonly Exception ErrAccountExists =
|
||||
new InvalidOperationException("account exists");
|
||||
|
||||
public static readonly Exception ErrBadAccount =
|
||||
new InvalidOperationException("bad account");
|
||||
|
||||
public static readonly Exception ErrReservedAccount =
|
||||
new InvalidOperationException("reserved account");
|
||||
|
||||
public static readonly Exception ErrMissingAccount =
|
||||
new InvalidOperationException("account missing");
|
||||
|
||||
public static readonly Exception ErrMissingService =
|
||||
new InvalidOperationException("service missing");
|
||||
|
||||
public static readonly Exception ErrBadServiceType =
|
||||
new InvalidOperationException("bad service response type");
|
||||
|
||||
public static readonly Exception ErrBadSampling =
|
||||
new InvalidOperationException("bad sampling percentage, should be 1-100");
|
||||
|
||||
public static readonly Exception ErrAccountValidation =
|
||||
new InvalidOperationException("account validation failed");
|
||||
|
||||
public static readonly Exception ErrAccountExpired =
|
||||
new InvalidOperationException("account expired");
|
||||
|
||||
public static readonly Exception ErrNoAccountResolver =
|
||||
new InvalidOperationException("account resolver missing");
|
||||
|
||||
public static readonly Exception ErrAccountResolverUpdateTooSoon =
|
||||
new InvalidOperationException("account resolver update too soon");
|
||||
|
||||
public static readonly Exception ErrAccountResolverSameClaims =
|
||||
new InvalidOperationException("account resolver no new claims");
|
||||
|
||||
public static readonly Exception ErrStreamImportAuthorization =
|
||||
new InvalidOperationException("stream import not authorized");
|
||||
|
||||
public static readonly Exception ErrStreamImportBadPrefix =
|
||||
new InvalidOperationException("stream import prefix can not contain wildcard tokens");
|
||||
|
||||
public static readonly Exception ErrStreamImportDuplicate =
|
||||
new InvalidOperationException("stream import already exists");
|
||||
|
||||
public static readonly Exception ErrServiceImportAuthorization =
|
||||
new InvalidOperationException("service import not authorized");
|
||||
|
||||
public static readonly Exception ErrImportFormsCycle =
|
||||
new InvalidOperationException("import forms a cycle");
|
||||
|
||||
public static readonly Exception ErrCycleSearchDepth =
|
||||
new InvalidOperationException("search cycle depth exhausted");
|
||||
|
||||
public static readonly Exception ErrClientOrRouteConnectedToGatewayPort =
|
||||
new InvalidOperationException("attempted to connect to gateway port");
|
||||
|
||||
public static readonly Exception ErrWrongGateway =
|
||||
new InvalidOperationException("wrong gateway");
|
||||
|
||||
public static readonly Exception ErrGatewayNameHasSpaces =
|
||||
new InvalidOperationException("gateway name cannot contain spaces");
|
||||
|
||||
public static readonly Exception ErrNoSysAccount =
|
||||
new InvalidOperationException("system account not setup");
|
||||
|
||||
public static readonly Exception ErrRevocation =
|
||||
new InvalidOperationException("credentials have been revoked");
|
||||
|
||||
public static readonly Exception ErrServerNotRunning =
|
||||
new InvalidOperationException("server is not running");
|
||||
|
||||
public static readonly Exception ErrServerNameHasSpaces =
|
||||
new InvalidOperationException("server name cannot contain spaces");
|
||||
|
||||
public static readonly Exception ErrBadMsgHeader =
|
||||
new InvalidOperationException("bad message header detected");
|
||||
|
||||
public static readonly Exception ErrMsgHeadersNotSupported =
|
||||
new InvalidOperationException("message headers not supported");
|
||||
|
||||
public static readonly Exception ErrNoRespondersRequiresHeaders =
|
||||
new InvalidOperationException("no responders requires headers support");
|
||||
|
||||
public static readonly Exception ErrClusterNameConfigConflict =
|
||||
new InvalidOperationException("cluster name conflicts between cluster and gateway definitions");
|
||||
|
||||
public static readonly Exception ErrClusterNameRemoteConflict =
|
||||
new InvalidOperationException("cluster name from remote server conflicts");
|
||||
|
||||
public static readonly Exception ErrClusterNameHasSpaces =
|
||||
new InvalidOperationException("cluster name cannot contain spaces");
|
||||
|
||||
public static readonly Exception ErrMalformedSubject =
|
||||
new InvalidOperationException("malformed subject");
|
||||
|
||||
public static readonly Exception ErrSubscribePermissionViolation =
|
||||
new InvalidOperationException("subscribe permission violation");
|
||||
|
||||
public static readonly Exception ErrNoTransforms =
|
||||
new InvalidOperationException("no matching transforms available");
|
||||
|
||||
public static readonly Exception ErrCertNotPinned =
|
||||
new InvalidOperationException("certificate not pinned");
|
||||
|
||||
public static readonly Exception ErrDuplicateServerName =
|
||||
new InvalidOperationException("duplicate server name");
|
||||
|
||||
public static readonly Exception ErrMinimumVersionRequired =
|
||||
new InvalidOperationException("minimum version required");
|
||||
|
||||
// Mapping destination errors — the Go source wraps ErrInvalidMappingDestination.
|
||||
// In .NET we use a common base message and chain inner exceptions where needed.
|
||||
public static readonly Exception ErrInvalidMappingDestination =
|
||||
new InvalidOperationException("invalid mapping destination");
|
||||
|
||||
public static readonly Exception ErrInvalidMappingDestinationSubject =
|
||||
new InvalidOperationException("invalid mapping destination: invalid transform");
|
||||
|
||||
public static readonly Exception ErrMappingDestinationNotUsingAllWildcards =
|
||||
new InvalidOperationException("invalid mapping destination: not using all of the token wildcard(s)");
|
||||
|
||||
public static readonly Exception ErrUnknownMappingDestinationFunction =
|
||||
new InvalidOperationException("invalid mapping destination: unknown function");
|
||||
|
||||
public static readonly Exception ErrMappingDestinationIndexOutOfRange =
|
||||
new InvalidOperationException("invalid mapping destination: wildcard index out of range");
|
||||
|
||||
public static readonly Exception ErrMappingDestinationNotEnoughArgs =
|
||||
new InvalidOperationException("invalid mapping destination: not enough arguments passed to the function");
|
||||
|
||||
public static readonly Exception ErrMappingDestinationInvalidArg =
|
||||
new InvalidOperationException("invalid mapping destination: function argument is invalid or in the wrong format");
|
||||
|
||||
public static readonly Exception ErrMappingDestinationTooManyArgs =
|
||||
new InvalidOperationException("invalid mapping destination: too many arguments passed to the function");
|
||||
|
||||
public static readonly Exception ErrMappingDestinationNotSupportedForImport =
|
||||
new InvalidOperationException("invalid mapping destination: the only mapping function allowed for import transforms is {{Wildcard()}}");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// mappingDestinationErr (mirrors server/errors.go)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// An error type for subject mapping destination validation failures.
|
||||
/// Mirrors <c>mappingDestinationErr</c> in server/errors.go.
|
||||
/// </summary>
|
||||
public sealed class MappingDestinationException : Exception
|
||||
{
|
||||
private readonly string _token;
|
||||
|
||||
public MappingDestinationException(string token, Exception inner)
|
||||
: base($"{inner.Message} in {token}", inner)
|
||||
{
|
||||
_token = token;
|
||||
}
|
||||
|
||||
// Is() in Go is implemented via IsInvalidMappingDestination below.
|
||||
|
||||
/// <summary>
|
||||
/// Returns true when compared against <see cref="ServerErrors.ErrInvalidMappingDestination"/>.
|
||||
/// Mirrors <c>mappingDestinationErr.Is</c>.
|
||||
/// </summary>
|
||||
public bool Is(Exception target) =>
|
||||
ReferenceEquals(target, ServerErrors.ErrInvalidMappingDestination);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// configErr / unknownConfigFieldErr / configWarningErr (mirrors server/errors.go)
|
||||
// -------------------------------------------------------------------------
|
||||
// Note: these types depend on a config-file token interface defined in the
|
||||
// configuration parser (ported in session 03). Forward-declared here with the
|
||||
// minimal interface needed for error formatting.
|
||||
|
||||
/// <summary>
|
||||
/// Represents a source location within a configuration file.
|
||||
/// Mirrors the <c>token</c> interface used by configErr in server/errors.go.
|
||||
/// Full implementation is provided by the configuration parser (session 03).
|
||||
/// </summary>
|
||||
public interface IConfigToken
|
||||
{
|
||||
string SourceFile();
|
||||
int Line();
|
||||
int Position();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A configuration parsing error with source location.
|
||||
/// Mirrors <c>configErr</c> in server/errors.go.
|
||||
/// </summary>
|
||||
public class ConfigException : Exception
|
||||
{
|
||||
private readonly IConfigToken? _token;
|
||||
|
||||
public ConfigException(IConfigToken? token, string reason)
|
||||
: base(reason)
|
||||
{
|
||||
_token = token;
|
||||
}
|
||||
|
||||
/// <summary>Returns "file:line:col" source location, or empty string if no token.</summary>
|
||||
public new string Source() =>
|
||||
_token != null
|
||||
? $"{_token.SourceFile()}:{_token.Line()}:{_token.Position()}"
|
||||
: string.Empty;
|
||||
|
||||
public override string Message =>
|
||||
_token != null ? $"{Source()}: {base.Message}" : base.Message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A configuration error for an unknown field (pedantic mode).
|
||||
/// Mirrors <c>unknownConfigFieldErr</c> in server/errors.go.
|
||||
/// </summary>
|
||||
public sealed class UnknownConfigFieldException : ConfigException
|
||||
{
|
||||
private readonly string _field;
|
||||
|
||||
public UnknownConfigFieldException(IConfigToken token, string field)
|
||||
: base(token, $"unknown field \"{field}\"")
|
||||
{
|
||||
_field = field;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A configuration warning for invalid field usage (pedantic mode).
|
||||
/// Mirrors <c>configWarningErr</c> in server/errors.go.
|
||||
/// </summary>
|
||||
public sealed class ConfigWarningException : ConfigException
|
||||
{
|
||||
private readonly string _field;
|
||||
|
||||
public ConfigWarningException(IConfigToken token, string field, string reason)
|
||||
: base(token, $"invalid use of field \"{field}\": {reason}")
|
||||
{
|
||||
_field = field;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggregates configuration warnings and hard errors from parsing.
|
||||
/// Mirrors <c>processConfigErr</c> in server/errors.go.
|
||||
/// </summary>
|
||||
public sealed class ProcessConfigException : Exception
|
||||
{
|
||||
public IReadOnlyList<Exception> Warnings { get; }
|
||||
public IReadOnlyList<Exception> Errors { get; }
|
||||
|
||||
public ProcessConfigException(IReadOnlyList<Exception> errors, IReadOnlyList<Exception> warnings)
|
||||
: base(BuildMessage(errors, warnings))
|
||||
{
|
||||
Errors = errors;
|
||||
Warnings = warnings;
|
||||
}
|
||||
|
||||
private static string BuildMessage(IReadOnlyList<Exception> errors, IReadOnlyList<Exception> warnings)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
foreach (var w in warnings) sb.AppendLine(w.Message);
|
||||
foreach (var e in errors) sb.AppendLine(e.Message);
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// errCtx — error with attached tracing context (mirrors server/errors.go)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Wraps an exception and attaches additional tracing context.
|
||||
/// Context is not included in <see cref="Exception.Message"/> but is
|
||||
/// accessible via <see cref="Context"/> and <see cref="UnpackIfErrorCtx"/>.
|
||||
/// Mirrors <c>errCtx</c> in server/errors.go.
|
||||
/// </summary>
|
||||
public sealed class ErrorCtx : Exception
|
||||
{
|
||||
public string Ctx { get; }
|
||||
|
||||
public ErrorCtx(Exception inner, string ctx)
|
||||
: base(inner.Message, inner)
|
||||
{
|
||||
Ctx = ctx;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the context string attached to this error.
|
||||
/// Mirrors <c>errCtx.Context()</c>.
|
||||
/// </summary>
|
||||
public string Context() => Ctx;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Factory and utilities for <see cref="ErrorCtx"/>.
|
||||
/// Mirrors <c>NewErrorCtx</c>, <c>UnpackIfErrorCtx</c>, and <c>ErrorIs</c> in server/errors.go.
|
||||
/// </summary>
|
||||
public static class ErrorContextHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an <see cref="ErrorCtx"/> wrapping <paramref name="err"/> with formatted context.
|
||||
/// Mirrors <c>NewErrorCtx</c>.
|
||||
/// </summary>
|
||||
public static Exception NewErrorCtx(Exception err, string format, params object[] args) =>
|
||||
new ErrorCtx(err, string.Format(format, args));
|
||||
|
||||
/// <summary>
|
||||
/// If <paramref name="err"/> is an <see cref="ErrorCtx"/>, returns
|
||||
/// "original.Message: ctx" (recursively). Otherwise returns err.Message.
|
||||
/// Mirrors <c>UnpackIfErrorCtx</c>.
|
||||
/// </summary>
|
||||
public static string UnpackIfErrorCtx(Exception err)
|
||||
{
|
||||
if (err is ErrorCtx ectx)
|
||||
{
|
||||
if (ectx.InnerException is ErrorCtx)
|
||||
return $"{UnpackIfErrorCtx(ectx.InnerException!)}: {ectx.Ctx}";
|
||||
return $"{ectx.InnerException!.Message}: {ectx.Ctx}";
|
||||
}
|
||||
return err.Message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Walks the inner exception chain checking reference equality and
|
||||
/// <see cref="MappingDestinationException.Is"/> overrides.
|
||||
/// In .NET, prefer <c>errors.Is</c>-equivalent patterns; this mirrors
|
||||
/// the Go <c>ErrorIs</c> shim exactly.
|
||||
/// Mirrors <c>ErrorIs</c>.
|
||||
/// </summary>
|
||||
public static bool ErrorIs(Exception? err, Exception? target)
|
||||
{
|
||||
if (err == null || target == null)
|
||||
return ReferenceEquals(err, target);
|
||||
|
||||
var current = err;
|
||||
while (current != null)
|
||||
{
|
||||
if (ReferenceEquals(current, target))
|
||||
return true;
|
||||
if (current is MappingDestinationException mde && mde.Is(target))
|
||||
return true;
|
||||
current = current.InnerException;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright 2020-2025 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.
|
||||
|
||||
using Shouldly;
|
||||
using ZB.MOM.NatsNet.Server;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests.Foundation;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="ErrorCtx"/> and <see cref="ErrorContextHelper"/>.
|
||||
/// Mirrors server/errors_test.go: TestErrCtx (ID 297) and TestErrCtxWrapped (ID 298).
|
||||
/// </summary>
|
||||
public sealed class ServerErrorsTests
|
||||
{
|
||||
[Fact]
|
||||
public void ErrCtx_ShouldPreserveOriginalMessageAndAddContext()
|
||||
{
|
||||
// Mirror: TestErrCtx
|
||||
var ctx = "Extra context information";
|
||||
var e = ErrorContextHelper.NewErrorCtx(ServerErrors.ErrWrongGateway, "{0}", ctx);
|
||||
|
||||
// Message should match the underlying error.
|
||||
e.Message.ShouldBe(ServerErrors.ErrWrongGateway.Message);
|
||||
|
||||
// Must not be reference-equal to the sentinel.
|
||||
e.ShouldNotBeSameAs(ServerErrors.ErrWrongGateway);
|
||||
|
||||
// ErrorIs should find the sentinel in the chain.
|
||||
ErrorContextHelper.ErrorIs(e, ServerErrors.ErrWrongGateway).ShouldBeTrue();
|
||||
|
||||
// UnpackIfErrorCtx on a non-ctx error returns Message unchanged.
|
||||
ErrorContextHelper.UnpackIfErrorCtx(ServerErrors.ErrWrongGateway)
|
||||
.ShouldBe(ServerErrors.ErrWrongGateway.Message);
|
||||
|
||||
// UnpackIfErrorCtx should start with the original error message.
|
||||
var trace = ErrorContextHelper.UnpackIfErrorCtx(e);
|
||||
trace.ShouldStartWith(ServerErrors.ErrWrongGateway.Message);
|
||||
|
||||
// And end with the context string.
|
||||
trace.ShouldEndWith(ctx);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ErrCtxWrapped_ShouldContainAllContextLayers()
|
||||
{
|
||||
// Mirror: TestErrCtxWrapped
|
||||
var ctxO = "Original Ctx";
|
||||
var eO = ErrorContextHelper.NewErrorCtx(ServerErrors.ErrWrongGateway, "{0}", ctxO);
|
||||
|
||||
var ctx = "Extra context information";
|
||||
var e = ErrorContextHelper.NewErrorCtx(eO, "{0}", ctx);
|
||||
|
||||
// Message should still match the underlying error.
|
||||
e.Message.ShouldBe(ServerErrors.ErrWrongGateway.Message);
|
||||
|
||||
// Must not be reference-equal to the sentinel.
|
||||
e.ShouldNotBeSameAs(ServerErrors.ErrWrongGateway);
|
||||
|
||||
// ErrorIs should walk the chain.
|
||||
ErrorContextHelper.ErrorIs(e, ServerErrors.ErrWrongGateway).ShouldBeTrue();
|
||||
|
||||
// UnpackIfErrorCtx on a non-ctx error returns Message unchanged.
|
||||
ErrorContextHelper.UnpackIfErrorCtx(ServerErrors.ErrWrongGateway)
|
||||
.ShouldBe(ServerErrors.ErrWrongGateway.Message);
|
||||
|
||||
var trace = ErrorContextHelper.UnpackIfErrorCtx(e);
|
||||
|
||||
// Must start with the original error.
|
||||
trace.ShouldStartWith(ServerErrors.ErrWrongGateway.Message);
|
||||
|
||||
// Must end with the outermost context.
|
||||
trace.ShouldEndWith(ctx);
|
||||
|
||||
// Must also contain the inner context.
|
||||
trace.ShouldContain(ctxO);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright 2018-2025 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.
|
||||
|
||||
using Shouldly;
|
||||
using ZB.MOM.NatsNet.Server.Internal;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="ClosedRingBuffer"/>.
|
||||
/// Mirrors server/ring_test.go: TestRBAppendAndLenAndTotal (ID 2794)
|
||||
/// and TestRBclosedClients (ID 2795).
|
||||
/// </summary>
|
||||
public sealed class ClosedRingBufferTests
|
||||
{
|
||||
[Fact]
|
||||
public void AppendAndLenAndTotal_ShouldTrackCorrectly()
|
||||
{
|
||||
// Mirror: TestRBAppendAndLenAndTotal
|
||||
var rb = new ClosedRingBuffer(10);
|
||||
|
||||
for (var i = 0; i < 5; i++)
|
||||
rb.Append(new ClosedClient());
|
||||
|
||||
rb.Len().ShouldBe(5);
|
||||
rb.TotalConns().ShouldBe(5UL);
|
||||
|
||||
for (var i = 0; i < 25; i++)
|
||||
rb.Append(new ClosedClient());
|
||||
|
||||
rb.Len().ShouldBe(10);
|
||||
rb.TotalConns().ShouldBe(30UL);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClosedClients_ShouldReturnChronologicalOrder()
|
||||
{
|
||||
// Mirror: TestRBclosedClients
|
||||
var rb = new ClosedRingBuffer(10);
|
||||
|
||||
// Build master list with identifiable user strings.
|
||||
const int max = 100;
|
||||
var master = new ClosedClient[max];
|
||||
for (var i = 1; i <= max; i++)
|
||||
master[i - 1] = new ClosedClient { User = i.ToString() };
|
||||
|
||||
var ui = 0;
|
||||
|
||||
void AddConn()
|
||||
{
|
||||
ui++;
|
||||
rb.Append(new ClosedClient { User = ui.ToString() });
|
||||
}
|
||||
|
||||
for (var i = 0; i < max; i++)
|
||||
{
|
||||
AddConn();
|
||||
|
||||
var ccs = rb.ClosedClients();
|
||||
var start = (int)rb.TotalConns() - ccs.Length;
|
||||
var ms = master[start..(start + ccs.Length)];
|
||||
|
||||
// Verify order matches master using User strings.
|
||||
ccs.Length.ShouldBe(ms.Length);
|
||||
for (var j = 0; j < ccs.Length; j++)
|
||||
ccs[j]!.User.ShouldBe(ms[j].User, $"iteration {i}, slot {j}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright 2021-2025 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.
|
||||
|
||||
using Shouldly;
|
||||
using ZB.MOM.NatsNet.Server.Internal;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="RateCounter"/>.
|
||||
/// Mirrors server/rate_counter_test.go: TestRateCounter (ID 2720).
|
||||
/// </summary>
|
||||
public sealed class RateCounterTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task RateCounter_ShouldAllowUpToLimitThenBlockAndReset()
|
||||
{
|
||||
// Mirror: TestRateCounter
|
||||
var counter = new RateCounter(10) { Interval = TimeSpan.FromMilliseconds(100) };
|
||||
|
||||
// First 10 calls should be allowed (counts 0–9 < limit 10).
|
||||
for (var i = 0; i < 10; i++)
|
||||
counter.Allow().ShouldBeTrue($"should allow on iteration {i}");
|
||||
|
||||
// Next 5 should be blocked.
|
||||
for (var i = 0; i < 5; i++)
|
||||
counter.Allow().ShouldBeFalse($"should not allow on iteration {i}");
|
||||
|
||||
// countBlocked returns and resets the blocked count.
|
||||
counter.CountBlocked().ShouldBe(5UL);
|
||||
counter.CountBlocked().ShouldBe(0UL);
|
||||
|
||||
// After the window expires, should allow again.
|
||||
await Task.Delay(150);
|
||||
counter.Allow().ShouldBeTrue("should allow after window expired");
|
||||
}
|
||||
}
|
||||
BIN
porting.db
BIN
porting.db
Binary file not shown.
@@ -1,28 +1,29 @@
|
||||
# NATS .NET Porting Status Report
|
||||
|
||||
Generated: 2026-02-26 13:07:55 UTC
|
||||
Generated: 2026-02-26 14:15:20 UTC
|
||||
|
||||
## Modules (12 total)
|
||||
|
||||
| Status | Count |
|
||||
|--------|-------|
|
||||
| complete | 1 |
|
||||
| not_started | 11 |
|
||||
| complete | 11 |
|
||||
| not_started | 1 |
|
||||
|
||||
## Features (3673 total)
|
||||
|
||||
| Status | Count |
|
||||
|--------|-------|
|
||||
| complete | 36 |
|
||||
| n_a | 41 |
|
||||
| not_started | 3596 |
|
||||
| complete | 265 |
|
||||
| n_a | 74 |
|
||||
| not_started | 3334 |
|
||||
|
||||
## Unit Tests (3257 total)
|
||||
|
||||
| Status | Count |
|
||||
|--------|-------|
|
||||
| complete | 16 |
|
||||
| not_started | 3241 |
|
||||
| complete | 119 |
|
||||
| n_a | 12 |
|
||||
| not_started | 3126 |
|
||||
|
||||
## Library Mappings (36 total)
|
||||
|
||||
@@ -33,4 +34,4 @@ Generated: 2026-02-26 13:07:55 UTC
|
||||
|
||||
## Overall Progress
|
||||
|
||||
**94/6942 items complete (1.4%)**
|
||||
**481/6942 items complete (6.9%)**
|
||||
|
||||
37
reports/report_66628bc.md
Normal file
37
reports/report_66628bc.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# NATS .NET Porting Status Report
|
||||
|
||||
Generated: 2026-02-26 14:15:20 UTC
|
||||
|
||||
## Modules (12 total)
|
||||
|
||||
| Status | Count |
|
||||
|--------|-------|
|
||||
| complete | 11 |
|
||||
| not_started | 1 |
|
||||
|
||||
## Features (3673 total)
|
||||
|
||||
| Status | Count |
|
||||
|--------|-------|
|
||||
| complete | 265 |
|
||||
| n_a | 74 |
|
||||
| not_started | 3334 |
|
||||
|
||||
## Unit Tests (3257 total)
|
||||
|
||||
| Status | Count |
|
||||
|--------|-------|
|
||||
| complete | 119 |
|
||||
| n_a | 12 |
|
||||
| not_started | 3126 |
|
||||
|
||||
## Library Mappings (36 total)
|
||||
|
||||
| Status | Count |
|
||||
|--------|-------|
|
||||
| mapped | 36 |
|
||||
|
||||
|
||||
## Overall Progress
|
||||
|
||||
**481/6942 items complete (6.9%)**
|
||||
Reference in New Issue
Block a user