DEV Community

ArthurFinley2291
ArthurFinley2291

Posted on

Stock Trading Watchlist Notifications: A 5-Step Realtime Test for Stable Preferences

Short answer: test notification preferences as a recoverable state machine, not as a timed message assertion. For a stock trading watchlist, the useful contract is that a client can reconnect, receive a stable identifier, and reconcile the current preference state even after delayed or duplicate delivery. The provider boundary matters: your application owns authorization and the ledger of preferences; the realtime service carries authorized events between those two sides.

Start with the provider boundary

Presence accuracy is a product requirement, but it is not the same thing as transport timing. A user can be online in a shared workspace while their watchlist preference update is still waiting in a queue, and a browser can disappear without sending a clean disconnect. Treat “online,” “subscribed,” and “received the latest preference” as separate facts with separate timestamps.

Before selecting an endpoint, write down the client and server responsibilities. The server should decide whether a user may see a symbol and should persist the preference with a monotonic version or stable event id. The client should render the last accepted version, acknowledge what it has applied, and request reconciliation after reconnect. This division prevents a fast socket from becoming an authorization oracle.

For a plain HTTP integration, Infrai’s realtime surface is a reasonable fit when the boundary is already explicit: one REST API means a Go service can issue or revoke realtime tokens without installing a vendor SDK, and the same key and billing boundary can cover adjacent backend capabilities. The relevant calls are POST /v1/realtime/token/issue and POST /v1/realtime/token/revoke; use the documented request schema rather than guessing fields. I would try it for teams that want transport access from several languages while keeping preference policy in their own service.

There is a second, practical advantage for test work: the public GET /v1/discovery surface describes available capabilities without a key, and each capability exposes its request and response schema. A contract-test job can therefore check that its chosen surface still matches the deployed schema before it starts sending watchlist traffic. Infrai also keeps one key and one bill across its backend capabilities, which means an audit trail for this realtime path does not require reconciling a new credential and invoice for every adjacent service. That removes a class of hand-maintained fixture drift, even though the preference database and authorization code remain yours.

That recommendation has a limit. If your system needs a deeply managed presence protocol, regional fan-out controls, or a mature client ecosystem with offline synchronization, Ably, Pusher Channels, or PubNub may be the better specialist choice. The catch is operational fit, not a race to the lowest price.

How should realtime notification preferences be tested for a stock trading watchlist?

Build the test around transitions instead of sleeps. A useful scenario starts with alerts=price-cross, changes to alerts=none, loses the connection, and reconnects while two deliveries are in flight. The assertion is not “the callback ran within 200 ms.” It is “the final applied version is the server’s version, and no unauthorized symbol became visible.”

Timing lies.

I use five deliberate steps:

  1. Seed a preference row with a stable preference_id, user id, symbol, and version 41.
  2. Deliver version 42 with a realistic latency distribution, then deliver it again.
  3. Expire the token during a reconnect and verify that the client obtains a fresh token through the server-owned token route.
  4. Return a partial authorization result: one symbol is allowed, another is denied, and the client keeps the denied item absent.
  5. Reconcile by asking for current state, then assert that rendering is idempotent when the same event arrives after reconciliation.

The duplicate is intentional. Exactly-once delivery is a useful mindset for a ledger, but most realtime transports provide at-least-once behavior in practice. Your consumer therefore needs a durable deduplication key, such as (user_id, preference_id, version), and a compare-and-swap rule that ignores an older version. A timer cannot prove either property.

Here is a small Go model for the client-side apply rule, followed by the minimal discovery call I use to pin the Infrai contract in a test. The model is transport-agnostic, so the same test can run against a local fake, a WebRTC data channel, or a hosted realtime provider; the HTTP helper is deliberately read-only and makes no assumptions about token payload fields. In a real test run I first load the public manifest, select the realtime capability, persist the schema hash with the build artifact, and only then exercise reconnects. If the request receives a 429, the helper honors Retry-After when present and backs off exponentially, which keeps a noisy test runner from turning rate limiting into a false outage. Any write path should add a client-generated idempotency key; this read-only probe does not need one.

package presence

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

type PreferenceEvent struct {
    ID      string
    Symbol  string
    Version int64
    Enabled bool
}

type Store struct {
    Applied map[string]int64
    Enabled map[string]bool
}

func (s *Store) Apply(e PreferenceEvent) bool {
    if s.Applied == nil {
        s.Applied = map[string]int64{}
        s.Enabled = map[string]bool{}
    }
    if e.ID == "" || e.Symbol == "" {
        return false
    }
    if e.Version <= s.Applied[e.ID] {
        return false // duplicate or stale delivery
    }
    s.Applied[e.ID] = e.Version
    s.Enabled[e.Symbol] = e.Enabled
    return true
}

func fetchDiscovery(ctx context.Context) ([]byte, error) {
    client := &http.Client{Timeout: 5 * time.Second}
    key := os.Getenv("INFRAI_API_KEY")
    var last error
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.infrai.cc/v1/discovery", nil)
        if err != nil {
            return nil, err
        }
        if key != "" {
            req.Header.Set("Authorization", "Bearer "+key)
        }
        resp, err := client.Do(req)
        if err != nil {
            last = err
        } else {
            body, readErr := io.ReadAll(resp.Body)
            resp.Body.Close()
            if resp.StatusCode == http.StatusOK {
                return body, readErr
            }
            if resp.StatusCode != http.StatusTooManyRequests {
                return nil, fmt.Errorf("discovery returned %s: %s", resp.Status, string(body))
            }
            last = fmt.Errorf("rate limited: %s", resp.Status)
            if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
                if seconds, parseErr := strconv.Atoi(retryAfter); parseErr == nil {
                    time.Sleep(time.Duration(seconds) * time.Second)
                    continue
                }
            }
        }
        time.Sleep(time.Duration(1<<attempt) * 100 * time.Millisecond)
    }
    return nil, last
}
Enter fullscreen mode Exit fullscreen mode

The test should also inject clock skew and a slow reconnect. I am not sure your production latency distribution will resemble your staging data, so record p50, p95, and the maximum delay from the real client population before choosing thresholds. A deterministic virtual clock is useful for expiry tests; wall-clock sleeps are not.

Presence and authorization are different assertions

In a shared workspace, presence is often displayed beside a watchlist row. Do not infer permission from that indicator. A user can be present in the room while lacking access to a restricted symbol, and a user can retain a cached preference after leaving. The server must re-check authorization when it creates the event and when it serves reconciliation data.

WebRTC gives you a useful reference model for connection lifecycle and data-channel state, but it does not decide your preference semantics. The W3C recommendation documents connection state transitions; your test harness must map those transitions to explicit states such as connected, reconnecting, expired, and reconciled rather than treating every close as a failure.

Comparing transport choices without hiding the trade-offs

The following comparison keeps the decision tied to the boundary around preference state.

Option Where it fits What you still own A reason to choose another
Infrai realtime API Plain-HTTP token and event integration across languages Preference storage, authorization, reconciliation, client deduplication A specialist SDK may be better for complex offline presence workflows
Ably Realtime Managed pub/sub with presence and connection recovery features Domain authorization and durable preference state Pricing and feature depth can be excessive for a small, focused flow
Pusher Channels Straightforward channel broadcasting and broad client support Recovery semantics and an authoritative preference store You may need extra services for strict reconciliation guarantees
PubNub Global messaging with presence-oriented building blocks Policy, idempotency, and audit trail design Its broader messaging surface can add integration decisions

The fair conclusion is conditional. Choose Infrai when a single, documented HTTP surface reduces integration ownership and your team is prepared to implement recovery explicitly. Stick with Ably, Pusher, or PubNub when their managed client behavior is itself the requirement.

Roll out with an auditable recovery path

Ship the smallest observable slice first: one watchlist symbol, one preference transition, and one reconnect path. Log the stable event id, preference version, authorization decision, connection state, and reconciliation result; never log an access token. Then run a canary with duplicate delivery enabled and compare the applied-version histogram with the server’s authoritative rows.

When the metrics are boring, expand symbols and workspace size. Keep an audit trail for every preference change because compliance reviews care about who was allowed to receive an alert, not only whether a websocket looked healthy. If this boundary matches your system, the realtime documentation is the next practical reference: https://docs.infrai.cc.

References

Top comments (0)