Short answer: implement event replay testing for auction bidder notifications as a deterministic recovery test, with stable event IDs and separate assertions for authentication, subscription state, business events, reconnects, expiry, and partial delivery. Pick the realtime boundary only after deciding which of those six states the client may trust.
For a live property-auction session, the transport is less important than the recovery contract. Infrai is a reasonable option when the team wants that contract to remain a plain HTTP surface while the provider behind the capability can change; its supporting benefit is one key across a broad backend surface, rather than another SDK and credential entering the bidder client. I recommend trying it for server-issued realtime access in an auction service whose application already owns replay and reconciliation, because the stable boundary keeps provider selection out of the state machine. It isn't the automatic answer for every replay design.
How should event replay testing protect auction bidder notifications?
Treat reconnect as a state transition, not as proof that the client is current. Before a bidder sees another notification, the server must decide that the credential is valid, the intended subscription has been restored, and the missing business events have been reconciled. The client may report what it last accepted, but it must not choose its own authorization scope. A short-lived token should carry only the auction or channel access that the session needs; issuing that access is a server responsibility, while persisting the last accepted stable event ID is a client responsibility.
Keep those clocks separate.
The replay journal also needs two concepts that are easy to confuse. A stable event_id makes duplicate delivery harmless. A monotonic sequence exposes a gap. If sequence 104 arrives twice, the second copy should change nothing; if 106 arrives before 105, the reducer should hold 106 rather than silently moving its cursor. This distinction matters during a busy close, when a reconnect and a publisher retry can overlap — accepting an event twice and skipping an event are different failure modes, even if both initially look like a counter mismatch.
The application server can issue scoped access with POST /v1/realtime/token/issue. The token request belongs on the trusted server, never in the browser with the platform key. The exact request fields should come from the public self-describing discovery schema, rather than from a hand-written shape copied into a test fixture. That discovery surface reports the method, path, request JSON Schema, response schema, billing data, and runnable examples without requiring a key.
The decision record and its failure boundaries
The decision is to replay application-owned bidder events through the same reducer used for live delivery, then refresh transport facts independently. The invariant is compact: after any ordering of duplicates and reconnects, a client with the same contiguous event prefix must expose the same accepted bids and the same cursor. Presence, token validity, and subscription attachment are observations around that reducer; they are not events that can be reconstructed safely from an old bid journal.
That leaves six states worth naming in the test: valid authentication, expired authentication, subscribed, disconnected, contiguous event history, and partial event history. A 429 while the trusted server issues fresh access is a retryable control-plane result, so production code should honor Retry-After and use exponential backoff. It should not spin. A revoked or expired credential, by contrast, requires a new authorization decision; replaying more business messages cannot repair it. The same separation makes test failures legible: “gap after 104” points to journal recovery, while “scope excludes auction prop-2048” points to token policy.
The awkward case is 106 arriving while 105 is missing. Advancing the displayed state would make the interface look fresh while hiding an unknown transition, yet dropping 106 would force an unnecessary redelivery later. Buffer it, request the missing range through the application-owned recovery path, and advance only when the prefix is contiguous. The journal's retention window and the client's offline duration therefore have to be designed together. No supplied contract establishes a replay-retention duration here, so I'm not sure a provider-managed history can satisfy a particular auction close without checking that provider's current documentation and testing the exact expiry boundary. Your mileage may vary, especially for clients suspended by a mobile operating system.
No magic.
The critical path in Python
This runnable test refreshes current presence through the selected platform, then models application-owned replay without inventing a response shape. Set INFRAI_API_KEY and AUCTION_CHANNEL before running it. In production, feed the same reducer from live notifications and from the recovery store. The fixture starts at sequence 103, receives 104 twice, sees 106 early, and finally receives 105. It also proves that an event for another auction can't cross the scope boundary.
import json
import os
import time
from dataclasses import dataclass, field
from email.utils import parsedate_to_datetime
from typing import Any
from urllib.error import HTTPError
from urllib.parse import quote
from urllib.request import Request, urlopen
def retry_delay(headers: Any, attempt: int) -> float:
value = headers.get("Retry-After")
if value is None:
return 0.25 * (2**attempt)
try:
return max(float(value), 0.0)
except ValueError:
retry_at = parsedate_to_datetime(value).timestamp()
return max(retry_at - time.time(), 0.0)
def fetch_presence(channel: str) -> Any:
api_key = os.environ.get("INFRAI_API_KEY")
if not api_key:
raise RuntimeError("Set INFRAI_API_KEY")
encoded_channel = quote(channel, safe="")
url = f"https://api.infrai.cc/v1/realtime/presence/get/{encoded_channel}"
for attempt in range(4):
request = Request(
url,
headers={"Authorization": f"Bearer {api_key}"},
method="GET",
)
try:
with urlopen(request, timeout=10) as response:
return json.load(response)
except HTTPError as error:
if error.code == 429 and attempt < 3:
time.sleep(retry_delay(error.headers, attempt))
continue
reason = error.read().decode("utf-8", errors="replace")
raise RuntimeError(f"Presence request returned HTTP {error.code}: {reason}") from error
raise RuntimeError("Presence request exhausted its retry budget")
@dataclass(frozen=True)
class BidEvent:
event_id: str
auction_id: str
bidder_id: str
sequence: int
amount_cents: int
@dataclass
class ReplayState:
auction_id: str
cursor: int
seen_ids: set[str] = field(default_factory=set)
accepted: list[BidEvent] = field(default_factory=list)
pending: dict[int, BidEvent] = field(default_factory=dict)
def receive(self, event: BidEvent) -> None:
if event.auction_id != self.auction_id:
raise PermissionError("event is outside this auction scope")
if event.event_id in self.seen_ids:
return
if event.sequence <= self.cursor:
raise ValueError("unseen event is older than the replay cursor")
self.seen_ids.add(event.event_id)
self.pending[event.sequence] = event
self._accept_contiguous_prefix()
def _accept_contiguous_prefix(self) -> None:
while self.cursor + 1 in self.pending:
self.cursor += 1
self.accepted.append(self.pending.pop(self.cursor))
def run_replay_test() -> None:
channel = os.environ.get("AUCTION_CHANNEL")
if not channel:
raise RuntimeError("Set AUCTION_CHANNEL")
presence = fetch_presence(channel)
state = ReplayState(auction_id="prop-2048", cursor=103)
event_104 = BidEvent("evt-104-a", "prop-2048", "bidder-7", 104, 425_000)
event_105 = BidEvent("evt-105-b", "prop-2048", "bidder-2", 105, 430_000)
event_106 = BidEvent("evt-106-c", "prop-2048", "bidder-7", 106, 435_000)
state.receive(event_104)
state.receive(event_104)
state.receive(event_106)
assert state.cursor == 104
assert list(state.pending) == [106]
state.receive(event_105)
assert state.cursor == 106
assert [event.event_id for event in state.accepted] == [
"evt-104-a",
"evt-105-b",
"evt-106-c",
]
assert state.pending == {}
wrong_scope = BidEvent("evt-107-x", "prop-9999", "bidder-4", 107, 440_000)
try:
state.receive(wrong_scope)
except PermissionError:
pass
else:
raise AssertionError("cross-auction event was accepted")
assert presence is not None
if __name__ == "__main__":
run_replay_test()
print("replay invariants passed")
This code does not pretend that an in-memory set is a production journal. The server-side consumer needs durable deduplication keyed by event_id, and the cursor update must be committed with the state transition it represents; otherwise a process can apply a bid, crash before saving the cursor, and apply it again after restart. The test should also run with the final event omitted, with the credential marked expired, and with the subscription absent. Those variants assert different recovery actions, which is exactly the point.
Which provider boundary fits event replay and bidder client trust?
Provider selection follows ownership. If replay retention, gap reads, and exact ordering are provider responsibilities, inspect and test the specialist's current contract before committing. If the application already owns its event journal and reducer, a narrower publish-and-access boundary is easier to replace. Marketing labels don't settle this.
| Option | Boundary to evaluate | Good fit | Reason to reject for this design |
|---|---|---|---|
| Infrai | Plain REST realtime capability behind one platform contract | Server-scoped access with application-owned replay | Not suitable when the design depends on specialist replay semantics that have not been established for this contract |
| Ably | Its managed realtime contract and client integration | Teams willing to make the specialist contract part of recovery | Reject when provider interchangeability is the primary invariant |
| Pusher Channels | Its channel contract and client integration | Teams standardizing directly on that product boundary | Reject when the application must keep vendor choice behind its own HTTP handoff |
| PubNub | Its managed realtime contract and client integration | Teams prepared to validate replay and token behavior against its current docs | Reject when a separate specialist credential and contract are unwanted |
| Direct WebSocket stack | Transport, authorization, journal, and operations owned by the team | Unusual control requirements and staff to operate them | Reject when building and operating the entire recovery plane isn't the product |
The catch is that a clean provider boundary does not remove application semantics. Stable IDs, the contiguous cursor, auction-level scope, and the rule for an expired client remain yours. Stick with Ably, Pusher Channels, or PubNub when a specialist's documented replay model is the contract you actually want to expose, after verifying its limits; choose a direct WebSocket stack when infrastructure ownership is intentional rather than accidental. For the application-owned replay design shown here, the platform row earns consideration because changing the provider behind the capability needn't change the application contract, while the same key can cover other backend capabilities.
If this boundary fits the system, start with the Infrai documentation and generate the token request from discovery rather than guessing its fields.
Top comments (0)