PubSubCClient 1.0.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package PubSubCClient --version 1.0.0
                    
NuGet\Install-Package PubSubCClient -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="PubSubCClient" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PubSubCClient" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="PubSubCClient" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add PubSubCClient --version 1.0.0
                    
#r "nuget: PubSubCClient, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package PubSubCClient@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=PubSubCClient&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=PubSubCClient&version=1.0.0
                    
Install as a Cake Tool

tên PubSubC Mô tả PubSubC là một thư viện C# để quản lý phạm vi theo dõi trong 1 khu vực nhất định. Nó có các đối tượng chính: Region : đại diện cho 1 khu vực nhất định Cell : đại diện cho 1 ô vuông trong khu vực, chứa danh sách Watcher và Unit phân loại theo UnitType Watcher : đại diện cho 1 đối tượng theo dõi, đăng ký vào nhiều ô vuông theo radius Unit : đại diện cho 1 đơn vị, nằm trong đúng 1 cell tại mỗi thời điểm UnitGroup : nhóm các Unit cùng loại trong một Region, quản lý ping timeout riêng

Config Cấu hình qua command-line args khi khởi động: --cell-size : kích thước mỗi ô vuông (mặc định 50) --region-ids : danh sách Region ID, phân cách bằng dấu phẩy (mặc định "1,2,3,...10") --ping-expire-time : thời gian tối đa chờ ping, tính bằng ms (mặc định 10000) --nats-url : URL kết nối NATS (mặc định "nats://localhost:4222")

Mã hóa dữ liệu Protobuf để mã hóa/giải mã dữ liệu truyền qua NATS. Incoming: WatcherEvent.Parser.ParseFrom(byte[]) hoặc UnitEvent.Parser.ParseFrom(byte[]). Outgoing: tái sử dụng 3 instance proto (_outWatcher, _outUnit, _outWarning). Scalar fields ghi đè trực tiếp, repeated fields gọi .Clear() rồi .AddRange(). Publish: message.ToByteArray() → NATS publish. Đơn giản, 1 bước.

Phương thức giao tiếp Thông qua NATS (package NATS.Net, INatsConnection)

    Subscriber Subject (nhận event từ bên ngoài) #nats_subscriber
        PUB_SUB_C.Region.{regionId}.WatcherSubscriber
            byte[] => WatcherEvent.Parser.ParseFrom => xử lý trong Region
        PUB_SUB_C.Region.{regionId}.UnitSubscriber
            byte[] => UnitEvent.Parser.ParseFrom => xử lý trong Region

    Publisher Subject (gửi event ra bên ngoài) #nats_publisher
        PUB_SUB_C.Region.{regionId}.WatcherPublisher
            WatcherEvent => ToByteArray() => NATS publish
            Gửi khi: Watcher subscribe, unsubscribe, moved, hoặc bị timeout
        PUB_SUB_C.Region.{regionId}.UnitPublisher
            UnitEvent => ToByteArray() => NATS publish
            Gửi khi: Unit added (ENTER_REGION), removed (EXIT_REGION), moved
            Gửi khi: WatcherCheck phát hiện lệch → ENTER_REGION, EXIT_REGION, UNIT_UPDATE
            Chứa notifyWatcherIds: danh sách watcherId bị ảnh hưởng bởi sự kiện (trừ WatcherCheck)
            Chứa watcherCheckId: ID watcher được kiểm tra (chỉ khi WatcherCheck)
        PUB_SUB_C.Region.{regionId}.WarningPublisher
            WarningEvent => ToByteArray() => NATS publish
            Gửi khi: entity not found, wrong version, hoặc not responding (timeout)

Luồng Mỗi Region chạy trên một Task riêng (TaskCreationOptions.LongRunning). Event queue sử dụng Channel<QueueEntry> (UnboundedChannel, SingleReader). QueueEntry là readonly record struct(SubjectType, byte[]).

Vòng lặp chính (RegionLoopAsync):
    1. WaitToReadAsync với timeout 100ms (đánh thức khi có event hoặc hết timeout)
    2. Drain toàn bộ queue: TryRead liên tục, gọi ProcessEventAsync cho mỗi entry
    3. CheckPingTimeoutsAsync: kiểm tra watcher/unit quá hạn ping
    4. Lặp lại

RegionManager (IHostedService):
    StartAsync: tạo NatsConnection, tạo các Region theo --region-ids, subscribe NATS cho mỗi Region
    StopAsync: cancel subscription, dispose các Region, đóng NATS connection

Cấu trúc dữ liệu

Manager (RegionManager : IHostedService)
    NatsConnection nats
    Dictionary<long, Region> regions
    List<Task> subscriptionTasks
    Phương thức:
        StartAsync: kết nối NATS, tạo Region, subscribe NATS subjects
        StopAsync: cancel, dispose regions, đóng NATS

Region
    Thuộc tính
        long id
        int cellSize
        int pingExpireTimeMs
        INatsConnection nats
        Dictionary<string, Cell> cells
        Dictionary<long, Watcher> watchers
        Dictionary<string, UnitGroup> unitGroups

        Ping tracking (watcher):
            SortedSet<(long Timestamp, long Id)> watcherPingOrder
                Sắp xếp theo timestamp để scan timeout hiệu quả, dừng sớm khi gặp entry chưa hết hạn
            Dictionary<long, long> watcherPingTs
                watcherId → TickCount64, dùng cho lookup O(1) khi cập nhật ping

        Channel<QueueEntry> channel
            Unbounded, SingleReader. NATS subscriber ghi vào, Region thread đọc ra.

        Protobuf reuse (outgoing):
            WatcherEvent _outWatcher
            UnitEvent _outUnit
            WarningEvent _outWarning

        Temp collections (tái sử dụng, clear trước mỗi lần dùng, tránh allocation):
            HashSet<string> _tempCellIds
                Kết quả CellHelper.FillSurroundingCellIds
            List<string> _tempCellDiff
                Diff old/new cells khi watcher moved (thay cho Except().ToList())
            HashSet<long> _tempAffectedWatcherIds
                Thu thập watcherIds bị ảnh hưởng bởi unit event
            List<(string,long)> _tempExpiredUnits
                Danh sách unit hết hạn ping trong CheckPingTimeoutsAsync
            List<long> _tempWarnWatcherIds
                Tham số watcherIds cho PublishWarningAsync
            List<long> _tempWarnUnitIds
                Tham số unitIds cho PublishWarningAsync
            HashSet<long> _tempCheckUnitIds
                Thu thập unitIds server-side khi WatcherCheck, dùng HashSet để tra Contains O(1)
            Dictionary<long, int> _tempClientUnitVersions
                Bảng unitId → version từ client khi WatcherCheck

        Task loopTask
        CancellationTokenSource cts

    Phương thức
        EnqueueEvent(string subjectType, byte[] data)
            Ghi vào channel, đánh thức region loop
        DisposeAsync()
            Cancel, complete channel, chờ loopTask

Watcher
    long Id
    float PositionX, PositionY
    int Radius
    int Version
    HashSet<string> CellIds
        Danh sách cell mà Watcher đang theo dõi
    Dictionary<string, HashSet<long>> UnitIdsByUnitType
        UnitId mà Watcher nhìn thấy, phân loại theo UnitType

Unit
    long Id
    string Type
    float PositionX, PositionY
    int Version
    string? CellId
        Cell hiện tại chứa Unit (1 cell duy nhất)

UnitGroup
    string Type
    Dictionary<long, Unit> Units
    Dictionary<long, long> PingTimestamps
        unitId → TickCount64
    Lưu ý: khi rỗng thì bị xóa khỏi Region

Cell
    string Id
    HashSet<long> WatcherIds
    Dictionary<string, HashSet<long>> UnitIdsByUnitType
        UnitId trong cell, phân loại theo UnitType
        Key = unitType, Value = tập hợp unitId
    bool IsEmpty => WatcherIds.Count == 0 && UnitIdsByUnitType.Count == 0
    Lưu ý: khi rỗng thì bị xóa khỏi Region

Logic

Suy Position sang Cell
    cellX = floor(position.x / cellSize)
    cellY = floor(position.y / cellSize)
    cellId = "{cellX}_{cellY}"

Lấy Cell xung quanh Watcher theo radius
    CellHelper.FillSurroundingCellIds(x, y, radius, cellSize, resultHashSet)
        resultHashSet.Clear()
        for dx = -radius to +radius
            for dy = -radius to +radius
                cellX = floor((x + dx * cellSize) / cellSize)
                cellY = floor((y + dy * cellSize) / cellSize)
                resultHashSet.Add("{cellX}_{cellY}")

Xử lý WatcherEvent:
    SUBSCRIBER:
        Tạo Watcher mới, tính cellIds theo radius (FillSurroundingCellIds → _tempCellIds)
        Đăng ký watcher vào từng cell, ghi nhận unit hiện có vào watcher.UnitIdsByUnitType
        Cập nhật ping timestamp
        Publish WatcherEvent(SUBSCRIBER) ra WatcherPublisher
    UNSUBSCRIBER:
        Xóa watcher khỏi tất cả cell + Region.Watchers + ping tracking
        Cleanup cell rỗng
        Publish WatcherEvent(UNSUBSCRIBER) ra WatcherPublisher
    MOVED:
        Tính newCells (_tempCellIds), diff với oldCells (_tempCellDiff)
        Removed cells: xóa watcher khỏi cell, loại unit khỏi watcher.UnitIdsByUnitType
        Added cells: thêm watcher vào cell, ghi nhận unit mới
        Cập nhật position, radius, version, ping
        Publish WatcherEvent(MOVED) ra WatcherPublisher
    PING:
        Không tìm thấy → warning WATCHER_NOT_FOUND
        Version không khớp → warning WATCHER_WRONG_VERSION
        OK → cập nhật ping timestamp

Xử lý UnitEvent:
    ADDED:
        Tạo Unit, tính cellId, thêm vào cell (theo UnitType) và UnitGroup
        Cập nhật tất cả watcher đang theo dõi cell: thêm unitId vào watcher.UnitIdsByUnitType
        Thu thập affected watcherIds (_tempAffectedWatcherIds)
        Publish UnitEvent(ENTER_REGION, notifyWatcherIds) ra UnitPublisher
    REMOVED:
        Thu thập affected watcherIds TRƯỚC KHI xóa (_tempAffectedWatcherIds)
        Xóa unit khỏi cell, watcher.UnitIdsByUnitType, UnitGroup
        Cleanup cell rỗng, UnitGroup rỗng
        Publish UnitEvent(EXIT_REGION, notifyWatcherIds) ra UnitPublisher
    MOVED:
        Nếu đổi cell: xóa khỏi old cell + watcher cũ, thêm vào new cell + watcher mới
        Nếu cùng cell: chỉ thu thập watcher hiện tại
        Cập nhật position, version, ping
        Thu thập affected watcherIds từ cả old + new cell (_tempAffectedWatcherIds)
        Publish UnitEvent(MOVED, notifyWatcherIds) ra UnitPublisher
    PING:
        Group không tìm thấy → warning UNIT_NOT_FOUND (tất cả unitIds)
        Unit không tìm thấy → warning UNIT_NOT_FOUND (từng unitId)
        Version không khớp → warning UNIT_WRONG_VERSION
        OK → cập nhật ping timestamp
    WATCHER_CHECK:
        Mục đích: kiểm tra đồng bộ danh sách unit mà watcher đang theo dõi giữa client và server
        Input (UnitEvent):
            unitIds + versions: danh sách unit mà client đang biết (theo unitType)
            unitType: loại unit cần kiểm tra
            watcherCheckId: ID watcher cần kiểm tra
        Xử lý:
            Không tìm thấy watcher → warning WATCHER_NOT_FOUND
            Build bảng client: unitId → version (_tempClientUnitVersions)
            Thu thập tất cả unit (theo unitType) từ các cell watcher đang theo dõi (_tempCheckUnitIds)
            So sánh 3 chiều:
                Thiếu (server có, client không có) → publish ENTER_REGION
                Thừa (client có, server không có) → publish EXIT_REGION
                Lệch version (cả hai có, version khác nhau) → publish UNIT_UPDATE
        Output (UnitEvent qua UnitPublisher):
            Mỗi loại sai lệch publish riêng 1 event (chỉ khi có unitIds)
            Chứa: unitIds, eventType, unitType, regionId, watcherCheckId
            Không chứa: versions, positionX/Y, notifyWatcherIds
            
    MASTER_PING_UNIT
        Mục đích: kiểm tra đồng bộ và hoạt động của unit, tương tự Watcher PING nhưng cho hệ thống Server-side unit (không phải client unit)
        Group không tìm thấy trong system → MASTER_SURPLUS (gửi toàn bộ unit ping được vào event)
            Báo cho master data source có unit thừa trong Region, cần điều chỉnh dữ liệu hoặc loại bỏ unit này
        Unit master có, system không có → MASTER_SURPLUS (gửi unitId vào event)
            Báo cho master data source có unit không tồn tại trong Region, cần điều chỉnh dữ liệu hoặc loại bỏ unit này
        Unit system có, master không có → MASTER_SHORTAGE (gửi unitId vào event)
            Báo cho master data source có unit thiếu trong Region, cần điều chỉnh dữ liệu hoặc thêm unit này
        Version không khớp → MASTER_VERSION_MISMATCH (gửi unitId vào event)
            Báo cho master data source có unit lệch version, cần điều chỉnh dữ liệu hoặc cảnh báo hệ thống upstream

Ping timeout check (CheckPingTimeoutsAsync):
    Watcher: scan watcherPingOrder (sorted), nếu timestamp <= cutoff:
        Xóa watcher khỏi Region + tất cả cell
        Publish warning WATCHER_NOT_RESPONDING
        Publish WatcherEvent(UNSUBSCRIBER)
    Unit: duyệt tất cả UnitGroup.PingTimestamps, thu thập expired vào _tempExpiredUnits
        Xóa unit khỏi UnitGroup + cell + watcher.UnitIdsByUnitType
        Publish warning UNIT_NOT_RESPONDING
        Cleanup UnitGroup rỗng

Warning publish (3 convenience method, zero-alloc):
    PublishWatcherWarningAsync(warningType, watcherId)
        Dùng _tempWarnWatcherIds, single watcher
    PublishUnitWarningAsync(warningType, unitId, unitType)
        Dùng _tempWarnUnitIds, single unit
    PublishUnitsWarningAsync(warningType, unitIds, unitType)
        Dùng _tempWarnUnitIds, nhiều unit (từ RepeatedField trực tiếp)

Unit publish (2 phương thức):
    PublishUnitEventAsync(eventType, srcEvt, notifyWatcherIds)
        Gửi đầy đủ: unitIds, versions, positionX/Y, notifyWatcherIds, unitType, regionId
        Dùng cho: ADDED→ENTER_REGION, REMOVED→EXIT_REGION, MOVED
    PublishWatcherCheckResultAsync(eventType, srcEvt)
        Gửi tối giản: unitIds, eventType, unitType, regionId, watcherCheckId
        Dùng cho: kết quả WATCHER_CHECK (ENTER_REGION, EXIT_REGION, UNIT_UPDATE)

Cell helpers:
    AddUnitToCell(cell, unitType, unitId)
        Thêm unitId vào cell.UnitIdsByUnitType[unitType]
    RemoveUnitFromCell(cell, unitType, unitId)
        Xóa unitId, nếu set rỗng thì xóa key unitType
    AddCellUnitsToWatcher(watcher, cell)
        Duyệt cell.UnitIdsByUnitType, thêm trực tiếp vào watcher.UnitIdsByUnitType
    RemoveCellUnitsFromWatcher(watcher, cell)
        Duyệt cell.UnitIdsByUnitType, xóa khỏi watcher.UnitIdsByUnitType

Protobuf messages

File: Protos/pub_sub_c.proto
option csharp_namespace = "PubSubC.Proto"

message WatcherEvent {
    int64 watcherId = 1;
    EventType eventType = 2;
    int32 version = 3;
    float positionX = 4;
    float positionY = 5;
    int32 radius = 6;
    int64 regionId = 7;
}

message UnitEvent {
    repeated int64 unitIds = 1;
    EventType eventType = 2;
    repeated int32 versions = 3;
    repeated float positionX = 4;
    repeated float positionY = 5;
    string unitType = 6;
    int64 regionId = 7;
    repeated int64 notifyWatcherIds = 8;
        Danh sách watcherId bị ảnh hưởng bởi sự kiện unit này.
        Được thu thập từ cell.WatcherIds khi unit added/removed/moved.
    int64 watcherCheckId = 9;
        ID watcher cần kiểm tra đồng bộ.
        Dùng trong WATCHER_CHECK và các event phản hồi (ENTER_REGION, EXIT_REGION, UNIT_UPDATE).
}

enum EventType {
    MOVED = 0;          // Unit hoặc Watcher di chuyển. Subscriber + Publisher.
    ADDED = 1;          // Unit được thêm vào. Subscriber.
    REMOVED = 2;        // Unit bị xóa. Subscriber.
    SUBSCRIBER = 3;     // Watcher đăng ký. Subscriber + Publisher.
    UNSUBSCRIBER = 4;   // Watcher hủy đăng ký. Subscriber + Publisher.
    PING = 5;           // Ping xác nhận hoạt động. Subscriber.
    ENTER_REGION = 6;   // Unit vào region. Publisher only.
    EXIT_REGION = 7;    // Unit rời region. Publisher only.
    WATCHER_CHECK = 8;  // Kiểm tra đồng bộ unit của watcher. Subscriber.
    UNIT_UPDATE = 9;    // Unit lệch version khi watcher check. Publisher only.
}

message WarningEvent {
    repeated int64 watcherIds = 1;
    repeated int64 unitIds = 2;
    string unitType = 3;
    WarningType warningType = 4;
}

enum WarningType {
    WATCHER_NOT_FOUND = 0;
        Không tìm thấy Watcher trong Region khi có event liên quan.
    UNIT_NOT_FOUND = 1;
        Không tìm thấy Unit trong Region khi có event liên quan.
    WATCHER_WRONG_VERSION = 2;
        Version Watcher không khớp khi Ping.
    UNIT_WRONG_VERSION = 3;
        Version Unit không khớp khi Ping.
    WATCHER_NOT_RESPONDING = 4;
        Watcher không Ping quá hạn, bị xóa khỏi Region và tất cả Cell.
    UNIT_NOT_RESPONDING = 5;
        Unit không Ping quá hạn, bị xóa khỏi UnitGroup và Region.
}

Test

Mục tiêu
    Đảm bảo tính đúng đắn của logic theo dõi theo Region/Cell, tính nhất quán dữ liệu theo version,
    tính ổn định khi xử lý đồng thời theo Queue, và độ tin cậy của giao tiếp NATS + Protobuf.

Chiến lược test tổng thể
    1. Unit Test (logic thuần)
        - Suy Position -> Cell ID.
        - Tính danh sách Cell trong bán kính của Watcher.
        - Cập nhật state Watcher/Unit theo từng EventType.
        - Kiểm tra version khi xử lý Ping/Moved.
    2. Integration Test (thành phần phối hợp)
        - Region + Queue + Publisher/Subscriber.
        - Encode/Decode Protobuf round-trip.
        - Routing đúng NATS subject theo regionId.
    3. Concurrency Test
        - Nhiều producer đẩy event vào cùng Region, đảm bảo xử lý tuần tự theo Queue.
        - Nhiều Region chạy song song, không rò rỉ state chéo vùng.
    4. Failure/Recovery Test
        - Watcher/Unit không Ping quá hạn bị loại bỏ và phát WarningEvent.
        - Nhận event không hợp lệ hoặc sai version phải được xử lý an toàn.
    5. Performance/Soak Test
        - Đo throughput event/s theo từng Region.
        - Theo dõi GC allocation.

Môi trường test đề xuất
    - Framework: xUnit (hoặc NUnit), FluentAssertions.
    - Mock/Fake: Fake NATS connection + in-memory publisher/subscriber.
    - Tách test theo nhóm: Unit / Integration / Load.
    - Thiết lập cố định:
        cellSize = 50
        pingExpireTime = 10000ms
        regionIds = 1,2,3

Test cases

    [Unit - Mapping/Geometry]

    TC-001: Suy Position dương sang Cell
        Tiền điều kiện:
            cellSize = 50
        Bước thực hiện:
            Input position = (120, 70)
            Tính cellX = floor(120/50), cellY = floor(70/50)
        Kết quả mong đợi:
            cellId = "2_1"

    TC-002: Suy Position âm sang Cell
        Tiền điều kiện:
            cellSize = 50
        Bước thực hiện:
            Input position = (-1, -1)
            Áp dụng floor cho số âm
        Kết quả mong đợi:
            cellId = "-1_-1"

    TC-003: Lấy Cell xung quanh Watcher theo radius
        Tiền điều kiện:
            watcher.position = (100,100), radius = 1, cellSize = 50
        Bước thực hiện:
            Gọi FillSurroundingCellIds
        Kết quả mong đợi:
            Có đúng 9 cell (3x3), chứa "2_2" là cell trung tâm

    TC-004: Radius = 0 chỉ trả về 1 Cell hiện tại
        Tiền điều kiện:
            watcher.position = (25,25), radius = 0, cellSize = 50
        Bước thực hiện:
            Gọi FillSurroundingCellIds
        Kết quả mong đợi:
            Chỉ có 1 cell: "0_0"

    [Unit - State/Version]

    TC-005: Watcher SUBSCRIBER tạo mới watcher và cellIds
        Tiền điều kiện:
            Region chưa có watcherId=101
        Bước thực hiện:
            Enqueue WatcherEvent(eventType=SUBSCRIBER, watcherId=101, version=1, position=(120,70), radius=1)
            ProcessEvents()
        Kết quả mong đợi:
            Region.Watchers chứa watcherId=101, version=1
            watcher.CellIds được khởi tạo đúng theo radius

    TC-006: Watcher UNSUBSCRIBER xóa watcher khỏi Region và Cell
        Tiền điều kiện:
            watcherId=101 đang tồn tại và đã đăng ký nhiều cell
        Bước thực hiện:
            Enqueue WatcherEvent(eventType=UNSUBSCRIBER, watcherId=101)
            ProcessEvents()
        Kết quả mong đợi:
            watcherId=101 bị xóa khỏi Region.Watchers
            watcher không còn xuất hiện trong bất kỳ Cell.WatcherIds nào

    TC-007: Ping Watcher sai version phát cảnh báo WATCHER_WRONG_VERSION
        Tiền điều kiện:
            watcherId=101 tồn tại với version=5
        Bước thực hiện:
            Enqueue WatcherEvent(eventType=PING, watcherId=101, version=4)
            ProcessEvents()
        Kết quả mong đợi:
            Không cập nhật state watcher
            Phát WarningEvent.warningType = WATCHER_WRONG_VERSION

    TC-008: Ping Unit không tồn tại phát cảnh báo UNIT_NOT_FOUND
        Tiền điều kiện:
            UnitGroup loại "car" không có unitId=999
        Bước thực hiện:
            Enqueue UnitEvent(eventType=PING, unitType="car", unitIds=[999], versions=[1])
            ProcessEvents()
        Kết quả mong đợi:
            Phát WarningEvent.warningType = UNIT_NOT_FOUND

    [Unit - WatcherCheck]

    TC-017: WATCHER_CHECK phát hiện unit thiếu → ENTER_REGION
        Tiền điều kiện:
            watcherId=101 theo dõi cell "2_1"
            Cell "2_1" có unitType="car" chứa unitIds=[1,2,3]
        Bước thực hiện:
            Enqueue UnitEvent(eventType=WATCHER_CHECK, unitType="car", unitIds=[1,2], versions=[1,1], watcherCheckId=101)
            ProcessEvents()
        Kết quả mong đợi:
            Publish UnitEvent(ENTER_REGION, unitIds=[3], unitType="car", watcherCheckId=101)

    TC-018: WATCHER_CHECK phát hiện unit thừa → EXIT_REGION
        Tiền điều kiện:
            watcherId=101 theo dõi cell "2_1"
            Cell "2_1" có unitType="car" chứa unitIds=[1,2]
        Bước thực hiện:
            Enqueue UnitEvent(eventType=WATCHER_CHECK, unitType="car", unitIds=[1,2,3], versions=[1,1,1], watcherCheckId=101)
            ProcessEvents()
        Kết quả mong đợi:
            Publish UnitEvent(EXIT_REGION, unitIds=[3], unitType="car", watcherCheckId=101)

    TC-019: WATCHER_CHECK phát hiện lệch version → UNIT_UPDATE
        Tiền điều kiện:
            watcherId=101 theo dõi cell "2_1"
            Cell "2_1" có unitType="car" chứa unitId=1 (version=5)
        Bước thực hiện:
            Enqueue UnitEvent(eventType=WATCHER_CHECK, unitType="car", unitIds=[1], versions=[3], watcherCheckId=101)
            ProcessEvents()
        Kết quả mong đợi:
            Publish UnitEvent(UNIT_UPDATE, unitIds=[1], unitType="car", watcherCheckId=101)

    TC-020: WATCHER_CHECK watcher không tồn tại → warning WATCHER_NOT_FOUND
        Tiền điều kiện:
            Region không có watcherId=999
        Bước thực hiện:
            Enqueue UnitEvent(eventType=WATCHER_CHECK, unitType="car", watcherCheckId=999)
            ProcessEvents()
        Kết quả mong đợi:
            Phát WarningEvent.warningType = WATCHER_NOT_FOUND

    TC-021: WATCHER_CHECK không có sai lệch → không publish event nào
        Tiền điều kiện:
            watcherId=101 theo dõi cell "2_1"
            Cell "2_1" có unitType="car" chứa unitIds=[1,2] (versions=[1,1])
        Bước thực hiện:
            Enqueue UnitEvent(eventType=WATCHER_CHECK, unitType="car", unitIds=[1,2], versions=[1,1], watcherCheckId=101)
            ProcessEvents()
        Kết quả mong đợi:
            Không publish bất kỳ UnitEvent nào ra UnitPublisher

    [Integration - Queue/NATS/Protobuf]

    TC-009: Queue xử lý tuần tự đúng thứ tự enqueue
        Tiền điều kiện:
            Region queue rỗng
        Bước thực hiện:
            Enqueue lần lượt 3 event cho cùng watcher: SUBSCRIBER(v1), MOVED(v2), PING(v2)
            ProcessEvents()
        Kết quả mong đợi:
            State cuối cùng phản ánh đúng event cuối (position mới, version=2)
            Không có cảnh báo sai version

    TC-010: Routing đúng subject theo regionId cho Subscriber
        Tiền điều kiện:
            Có Region 1 và Region 2
        Bước thực hiện:
            Publish dữ liệu vào PUB_SUB_C.Region.2.WatcherSubscriber
        Kết quả mong đợi:
            Chỉ Region 2 nhận và xử lý event
            Region 1 không đổi state

    TC-011: Encode/Decode WatcherEvent bằng Protobuf không sai lệch dữ liệu
        Tiền điều kiện:
            Có message WatcherEvent mẫu (đủ field)
        Bước thực hiện:
            ToByteArray() -> ParseFrom()
        Kết quả mong đợi:
            Các field watcherId, eventType, version, positionX/Y, radius, regionId khớp 100%

    TC-012: UnitEvent notifyWatcherIds đúng danh sách watcher bị ảnh hưởng
        Tiền điều kiện:
            Region có watcher W1 theo dõi cell "2_1", watcher W2 theo dõi cell "3_3"
        Bước thực hiện:
            Thêm unit vào cell "2_1"
        Kết quả mong đợi:
            UnitEvent(ENTER_REGION).notifyWatcherIds chứa W1, không chứa W2

    [Failure/Recovery]

    TC-013: Watcher quá hạn ping bị remove và phát WATCHER_NOT_RESPONDING
        Tiền điều kiện:
            watcherId=101 tồn tại; ping timestamp cũ hơn pingExpireTime
        Bước thực hiện:
            Chạy chu kỳ kiểm tra timeout
        Kết quả mong đợi:
            watcherId=101 bị xóa khỏi Region và các Cell liên quan
            Phát WarningEvent.warningType = WATCHER_NOT_RESPONDING

    TC-014: Unit quá hạn ping bị remove khỏi UnitGroup và phát UNIT_NOT_RESPONDING
        Tiền điều kiện:
            unitId=501 tồn tại trong UnitGroup "person"; ping quá hạn
        Bước thực hiện:
            Chạy chu kỳ kiểm tra timeout
        Kết quả mong đợi:
            unitId=501 bị xóa khỏi UnitGroup và Region
            Nếu UnitGroup rỗng thì UnitGroup bị xóa
            Phát WarningEvent.warningType = UNIT_NOT_RESPONDING

    [Concurrency]

    TC-015: Nhiều producer enqueue đồng thời vào cùng Region
        Tiền điều kiện:
            10 thread producer, mỗi thread gửi 1000 event
        Bước thực hiện:
            Chạy đồng thời và chờ ProcessEvents hoàn tất
        Kết quả mong đợi:
            Không mất event, không race condition
            Số lượng version cuối cùng nhất quán theo thứ tự xử lý queue

    TC-016: Nhiều Region chạy song song không nhiễm chéo dữ liệu
        Tiền điều kiện:
            Region 1 và 2 cùng có watcherId giống nhau (101)
        Bước thực hiện:
            Gửi event khác nhau vào từng region
        Kết quả mong đợi:
            State watcherId=101 của mỗi region độc lập hoàn toàn
            Publisher chỉ phát đúng subject theo region tương ứng

Tiêu chí hoàn thành (Definition of Done cho Test)
    - 100% test case P0 (TC-001, 003, 005, 007, 009, 010, 011, 013, 014, 015) phải pass.
    - Không có bug mức Critical/High ở logic queue, ping timeout, và routing subject.
    - Có thể chạy lặp lại tối thiểu 3 lần và cho kết quả ổn định.
Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.1 113 4/11/2026
1.0.0 122 4/10/2026