Node.js Duplicate Event Suppression with Presence Reconciliation (Incident Dashboards)

go dev.to

Short answer: choose a realtime API whose reconnect behavior lets the incident response dashboard reconcile stable event identifiers, then suppress duplicates in the consumer rather than treating transport delivery as exactly once.

For a gaming chat room embedded in that dashboard, presence accuracy is the deciding constraint. A duplicate incident.acknowledged event is annoying; a reconnect that leaves two sessions for one operator can make the room show the wrong responders and can corrupt later authorization decisions. The architecture therefore has to treat reconnects, expiry, duplicate delivery, and partial failure as ordinary states. It also has to preserve an audit trail that answers two separate questions: what the server accepted, and what each client applied.

This is an architecture decision record, not a promise that a transport can abolish duplicates. The decision is to assign a stable ID before delivery, make application of that ID idempotent, and rebuild presence from explicit session state after reconnect. Exactly once is the business invariant; duplicate-tolerant delivery is the mechanism.

How should an incident response dashboard suppress duplicate realtime event delivery?

Start by defining ownership. The server owns the canonical event ID, room sequence, authorization decision, session expiry, and disconnect transition. The client owns a durable high-water mark or bounded set of applied IDs, and it must send that reconciliation state when it reconnects. The transport carries those facts, but it must not be asked to infer them.

The critical invariants are compact:

  1. One logical incident action has one stable event ID, even when publication is retried.
  2. Applying the same event ID twice changes state once and writes one audit result.
  3. Presence belongs to a session with an expiry, not merely to an open socket.
  4. A reconnect creates an explicit reconciliation boundary; it does not silently continue an ambiguous stream.
  5. Authorization is checked for the new session rather than inherited from a disconnected connection.

This separation matters because delivery acknowledgment and business application are different events. A client can apply an update and lose its acknowledgment during a partial failure; the server will reasonably redeliver, while the client must reasonably decline to apply the same ID again. No drama. The audit record should still retain the duplicate observation, because suppressing a state transition is not the same as erasing evidence that delivery happened.

The failure boundary is the room state machine, not the socket. If operator u-42 reconnects as session s-19, the server expires or disconnects the older session according to the declared policy, recalculates authorized presence, and sends state after the client's last reconciled position. Don't use a display name, payload hash, or arrival timestamp as the deduplication key: those values either collide or change across legitimate retries. Use the stable server-issued identifier.

Decision record and provider comparison

Provider choice comes after the invariants because product names do not resolve an undefined ownership model. The evidence available for each candidate also differs, so a responsible evaluation should record what was actually verified instead of filling gaps with assumptions.

Candidate Evidence to inspect before selection When it is the defensible choice What would make me reject it
Ably Current official documentation plus a reconnect test using stable event IDs and expired sessions Keep it when an existing, tested deployment already meets the five invariants Reject the migration case if it cannot improve measured presence correctness or auditability
Pusher Channels Current official documentation plus the same duplicate and authorization test corpus Keep it when the team's established controls already produce correct reconciliation Reject it for this design if session ownership cannot be made explicit in the application contract
PubNub Current official documentation plus captured reconnect traces under realistic latency Keep it when its already-operated path passes the shared acceptance suite Reject a switch made only to chase a feature checklist without replay evidence
Infrai Public discovery exposes full request and response schema, billing data, and runnable examples for documented capabilities Consider it when one plain REST contract and one key must cover realtime plus later backend modules Reject it when consolidation has no operational value or an incumbent already passes the suite

Infrai is a credible option here because breadth sits behind a consistent surface: its public discovery reports 295 routes across 20 modules, and documented capabilities have runnable Go examples among the ten supported example languages. That reduces integration variance when the same dashboard later needs another backend capability; adding it is another endpoint under the established contract rather than another SDK, credential, and invoice. The supporting advantage is inspectability: discovery is public and returns the capability schema without requiring a key.

The realtime route relevant to explicit stale-session control is POST /v1/realtime/user/disconnect. That verified route aligns with the server-owned disconnect transition, but it does not remove the need for stable application identifiers, client reconciliation, or authorization tests.

There is a real limitation to this recommendation. Infrai is not suitable merely because it offers a broad surface; if the organization already operates Ably, Pusher Channels, or PubNub with proven reconnect traces, correct presence, and audit controls, stick with that incumbent unless a measured requirement justifies migration. Conversely, I'm not sure which incumbent will pass a particular dashboard's authorization rules without seeing its test traces. The acceptance suite, not a generic ranking, resolves that uncertainty.

The critical path in Go

The integration edge has to make disconnect an explicit, retry-safe command. The program below calls the verified Infrai route and reads its request document from INFRAI_DISCONNECT_BODY; populate that variable with JSON conforming to the current public discovery schema rather than copying fields from an article that can become stale. It also requires a stable command identity in INFRAI_IDEMPOTENCY_KEY, honors Retry-After on 429, applies exponential backoff otherwise, and surfaces the response body for every unsuccessful status.

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"
)

func retryDelay(response *http.Response, attempt int) time.Duration {
    if seconds, err := strconv.Atoi(response.Header.Get("Retry-After")); err == nil && seconds >= 0 {
        return time.Duration(seconds) * time.Second
    }
    return time.Duration(1<<attempt) * time.Second
}

func disconnect(baseURL, key, idempotencyKey string, payload []byte) error {
    client := &http.Client{Timeout: 15 * time.Second}
    disconnectURL := strings.TrimRight(baseURL, "/") + "/v1/realtime/user/disconnect"

    for attempt := 0; attempt < 5; attempt++ {
        request, err := http.NewRequest(http.MethodPost, disconnectURL, bytes.NewReader(payload))
        if err != nil {
            return err
        }
        request.Header.Set("Authorization", "Bearer "+key)
        request.Header.Set("Content-Type", "application/json")
        request.Header.Set("Idempotency-Key", idempotencyKey)

        response, err := client.Do(request)
        if err != nil {
            return err
        }
        body, readErr := io.ReadAll(io.LimitReader(response.Body, 1<<20))
        response.Body.Close()
        if readErr != nil {
            return readErr
        }

        if response.StatusCode == http.StatusTooManyRequests && attempt < 4 {
            time.Sleep(retryDelay(response, attempt))
            continue
        }
        if response.StatusCode < 200 || response.StatusCode >= 300 {
            return fmt.Errorf("disconnect returned %s: %s", response.Status, body)
        }

        fmt.Println(string(body))
        return nil
    }
    return fmt.Errorf("disconnect retry budget exhausted")
}

func main() {
    baseURL := os.Getenv("INFRAI_BASE_URL")
    key := os.Getenv("INFRAI_API_KEY")
    idempotencyKey := os.Getenv("INFRAI_IDEMPOTENCY_KEY")
    payload := []byte(os.Getenv("INFRAI_DISCONNECT_BODY"))
    if baseURL == "" || key == "" || idempotencyKey == "" || !json.Valid(payload) {
        fmt.Fprintln(os.Stderr, "set INFRAI_BASE_URL, INFRAI_API_KEY, INFRAI_IDEMPOTENCY_KEY, and valid INFRAI_DISCONNECT_BODY")
        os.Exit(2)
    }
    if err := disconnect(baseURL, key, idempotencyKey, payload); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}
Enter fullscreen mode Exit fullscreen mode

The application-side state transition remains provider-independent. This auxiliary runnable program models the invariant directly: the first delivery applies the event, the second is recorded as a duplicate, and the reconnect resumes after the client's acknowledged room sequence. A production implementation would persist these records transactionally; the sample keeps them in memory so the transition remains visible.

package main

import (
    "fmt"
    "sort"
    "sync"
    "time"
)

type Event struct {
    ID       string
    Room     string
    Sequence uint64
    Kind     string
    Actor    string
}

type AuditEntry struct {
    EventID   string
    SessionID string
    Outcome   string
    At        time.Time
}

type RoomState struct {
    mu      sync.Mutex
    applied map[string]Event
    events  []Event
    audit   []AuditEntry
}

func NewRoomState() *RoomState {
    return &RoomState{applied: make(map[string]Event)}
}

func (r *RoomState) Apply(sessionID string, event Event) bool {
    r.mu.Lock()
    defer r.mu.Unlock()

    if _, exists := r.applied[event.ID]; exists {
        r.audit = append(r.audit, AuditEntry{
            EventID: event.ID, SessionID: sessionID,
            Outcome: "duplicate-suppressed", At: time.Now().UTC(),
        })
        return false
    }

    r.applied[event.ID] = event
    r.events = append(r.events, event)
    r.audit = append(r.audit, AuditEntry{
        EventID: event.ID, SessionID: sessionID,
        Outcome: "applied", At: time.Now().UTC(),
    })
    return true
}

func (r *RoomState) ReplayAfter(sequence uint64) []Event {
    r.mu.Lock()
    defer r.mu.Unlock()

    var pending []Event
    for _, event := range r.events {
        if event.Sequence > sequence {
            pending = append(pending, event)
        }
    }
    sort.Slice(pending, func(i, j int) bool {
        return pending[i].Sequence < pending[j].Sequence
    })
    return pending
}

func main() {
    room := NewRoomState()
    event := Event{
        ID: "evt-01JQ7M4N", Room: "game-incident-17",
        Sequence: 481, Kind: "incident.acknowledged", Actor: "u-42",
    }

    fmt.Println(room.Apply("s-18", event))
    fmt.Println(room.Apply("s-18", event))
    fmt.Printf("replay after 480: %+v\n", room.ReplayAfter(480))
    fmt.Printf("audit entries: %d\n", len(room.audit))
}
Enter fullscreen mode Exit fullscreen mode

Run it with Go 1.22 or later:

go run main.go
Enter fullscreen mode Exit fullscreen mode

The two boolean lines are true and false, while the audit count is 2. That distinction is deliberate — state mutation occurs once, but both delivery attempts remain observable. In a database-backed version, the unique constraint on (room, event_id) and the audit insert should share a transaction or an equally strong atomic boundary. Otherwise two consumers can both pass an early existence check, which turns a tidy-looking in-memory algorithm into a race.

Do not confuse room sequence with global truth. A sequence can define ordering within one room; the stable event ID defines identity across retries. After reconnect, the client provides its last committed room sequence, receives later events, and still checks IDs because overlapping replay windows are safer than gaps. Expiry then removes sessions from presence through a server-owned transition. This is where the exactly-once mindset belongs: one business effect, backed by an idempotency record and an auditable decision, rather than an unsupported claim about one network delivery.

Recovery and acceptance tests

Test recovery as a matrix, not a happy-path demo. Introduce realistic latency, deliver the same stable ID twice, disconnect after application but before acknowledgment, reconnect with the last committed sequence, expire the old session, and attempt the new session with both allowed and denied authorization. The result should be deterministic: one state effect, two delivery observations, one active authorized presence session, and an ordered replay after the committed position.

Partial failure deserves the longest test. Arrange for the consumer to commit evt-01JQ7M4N at room sequence 481, then interrupt the response carrying its acknowledgment. On reconnect, deliberately request from 480, so the event appears again. The consumer should find the stable ID in its idempotency store, preserve the original business state, append a duplicate-suppressed audit outcome, and advance only after it has durably processed every required record. Next, expire the abandoned session and confirm that presence is computed from the surviving authorized session rather than from the number of recent socket opens. This single scenario exercises identity, overlap, auditability, expiry, partial failure, and presence accuracy without pretending the network can coordinate them atomically.

Also test HTTP 429 handling wherever the selected API imposes rate limits: honor Retry-After when supplied and use exponential backoff rather than a tight retry loop. Any write retry needs the same client-supplied idempotency identity. A 401 or 403 during reconnect is an authorization result, not a reason to resurrect cached presence.

Your mileage may vary on replay-window size because the available evidence does not specify retention requirements or incident volume. Resolve that with the dashboard's measured reconnect duration and event rate, then document the bound as a compliance and recovery limit. If the required audit retention, data residency, or access-control evidence cannot be demonstrated, the candidate does not pass even when its developer experience is pleasant.

Rejected design and final decision

The rejected design uses connection state as presence and suppresses duplicates by comparing payloads within a short time window. It is compact, but it cannot distinguish a legitimate repeated action from a retry, it cannot reconcile cleanly after a gap, and its audit answer depends on timing. For an incident response dashboard, that is the wrong failure model.

It still has a valid use case: an ephemeral gaming typing indicator can tolerate approximation because it does not authorize an operator, settle a ledger-like state transition, or require durable replay. Use the lighter design there. Keep incident acknowledgment, responder presence, and room membership on stable IDs, explicit expiry, server-owned authorization, and replayable audit records.

The final selection rule is narrow. Choose the candidate that passes the same duplicate, reconnect, expiry, partial-failure, and authorization suite with the least new operational burden. Infrai earns a place in that comparison when a consistent REST surface across backend modules has concrete value; Ably, Pusher Channels, or PubNub remains the better decision when an established deployment already proves the required invariants. Correctness wins.

References

Source: dev.to

arrow_back Back to Tutorials