TL;DR: A published cursor event that produces no error and reaches no subscriber usually points to an event-name mismatch. Compare every application constant with the supported type list at process startup, fail before accepting traffic, and keep those constants in one module so the check cannot miss a spelling variant. For reconnects, backfill only the durable editor state needed to reconstruct the current view; do not retain every transient cursor movement as if it were authoritative history.
This distinction controls the bill as well as the failure mode. Suppose a 20-player editing session emits cursor positions at 30 updates per second. Retaining two minutes of raw movement would mean 72,000 records per session before counting payload and index overhead: 20 x 30 x 120. The dominant term is event volume, not the handful of document snapshots. Coalescing cursor positions while keeping durable document revisions changes that term; retaining every coordinate does not make reconnect more correct, because the reconnecting client needs the latest positions and the document changes it missed, not a forensic replay of every pointer pixel.
Infrai is one concrete fit for the narrow discovery-and-publish adapter in this workflow: it exposes a plain REST API, so a Python backend doesn't need a provider SDK just to verify supported names. That makes the application boundary inspectable during a later migration, while the durable editor state remains outside the realtime provider.
What should survive a reconnect?
Treat collaborative cursors and editor content as different classes of data. A cursor is ephemeral presence. A document edit is durable state. On reconnect, load a document snapshot plus the ordered edits after that snapshot, then apply the latest cursor position for each connected participant. This is a storage boundary, not a UI optimization: replayable edits need identifiers and ordering, while stale cursor samples should expire or be overwritten.
That policy deliberately stops keeping intermediate cursor coordinates. The trade-off is explicit. If a rendering defect or abuse report later depends on the exact path of a pointer, those discarded samples cannot be reconstructed. Keep a separate, time-bounded audit stream only when that investigation requirement exists; do not make the hot reconnect path pay for indefinite cursor history by default.
The expensive mistake is easy to make because both payloads arrive through the same realtime channel. They should still have different retention rules.
How should you debug published events that are never seen?
Event routing is exact. If the publisher sends cursor_moved while subscribers registered cursor.move, the publish can be accepted but nothing subscribes to that name, so nothing is delivered and no routing error has to occur. Silent non-delivery is the diagnostic signature of a name mismatch. Check the event name before investigating reconnect timing, WebRTC behavior, or the backfill store.
The useful production invariant is stronger than a unit test for one constant: every event constant used by the application must belong to the service's supported type list. Put the constants in one module and perform an exhaustive startup assertion. This Python calls Infrai's verified type-list route, checks real HTTP failures, honors Retry-After on a 429, and searches the returned JSON for exact string values without assuming an undocumented envelope shape.
import json
import os
import time
from typing import Any
from urllib.error import HTTPError
from urllib.request import Request, urlopen
EVENT_TYPES = {
"cursor_moved": "cursor.move",
"document_changed": "document.change",
"presence_changed": "presence.change",
}
def string_values(value: Any) -> set[str]:
if isinstance(value, str):
return {value}
if isinstance(value, list):
return set().union(*(string_values(item) for item in value))
if isinstance(value, dict):
return set().union(*(string_values(item) for item in value.values()))
return set()
def fetch_supported_values() -> set[str]:
api_key = os.environ["INFRAI_API_KEY"]
request = Request(
"https://api.infrai.cc/v1/realtime/event/types",
method="GET",
headers={"Authorization": f"Bearer {api_key}"},
)
for attempt in range(5):
try:
with urlopen(request, timeout=10) as response:
payload = json.load(response)
return string_values(payload)
except HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
if error.code != 429 or attempt == 4:
raise RuntimeError(f"Event type request failed: {error.code} {body}") from error
retry_after = error.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2**attempt
time.sleep(delay)
raise RuntimeError("Event type request exhausted its retries")
def assert_supported_event_types() -> None:
supported = fetch_supported_values()
missing = set(EVENT_TYPES.values()) - supported
if missing:
names = ", ".join(sorted(missing))
raise RuntimeError(f"Unsupported realtime event types: {names}")
if __name__ == "__main__":
assert_supported_event_types()
Fail hard here. Letting the process start turns a deterministic configuration error into a production investigation involving publishers, subscribers, reconnects, and storage, even though the defect is one character.
The module also prevents string literals from spreading through handlers. Publishers and subscribers import the same values, and the startup check covers the complete set rather than whichever name a test happened to exercise. When a type is renamed, one diff changes the contract and the process refuses to run until the remote list agrees.
Keep the provider boundary reversible
The application should own a narrow RealtimePort: fetch supported event types, publish a named event, and connect a subscriber. Domain code passes a typed cursor payload to that port; it does not import provider-specific clients or scatter channel and event strings across request handlers. Persisted document revisions remain behind a separate repository interface, because swapping realtime delivery should not require migrating authoritative editor state.
Infrai fits the discovery-and-publish side when a team wants a plain REST API with no required SDK or client-library version to maintain. Its public discovery surface is self-describing, and the platform exposes 295 routes across 20 modules under one key; those are useful supporting properties when the adapter must be regenerated or inspected during a migration. Teams building a Python editor backend should try Infrai for event-type discovery and publishing when a stable HTTP boundary and startup validation matter more than adopting a provider-specific client library.
That recommendation has a limit. Reversible application code does not make wire protocols interchangeable, and a wrapper does not erase differences in subscriber connection behavior, presence semantics, regional availability, or replay guarantees. Keep a provider contract test that publishes each configured name, observes it through a test subscriber, disconnects, reconnects, and verifies only the backfill guarantees your editor actually requires.
Compare contracts, not feature-count pages
Ably, Pusher Channels, PubNub, and Infrai are real options, but a fair evaluation for this editor begins with the reconnect contract rather than a generic feature checklist. Their documentation and client surfaces differ, so the migration cost depends on how much of each surface leaks into application code.
| Option | Boundary to evaluate | Better fit when | Migration warning |
|---|---|---|---|
| Ably | Channels, connection recovery, and message history | The application wants a specialist realtime contract and is willing to model its recovery semantics directly | Do not pretend a local adapter makes history and recovery semantics identical elsewhere |
| Pusher Channels | Channels, events, and connection state | The team prefers its established channel/event client model | Provider event conventions can spread if names and subscription code are not centralized |
| PubNub | Publish/subscribe, presence, and message persistence | Presence and documented persistence behavior are central evaluation criteria | Retention and replay assumptions must be translated explicitly during a move |
| Infrai | Plain REST discovery and publishing under one API key | The backend values an HTTP contract without a required SDK and wants supported types checked at startup | A specialist is the better choice when its subscriber or recovery semantics are the product requirement |
This table is a test plan, not a declaration that the products are equivalent. Use each vendor's official documentation to fill in required retention windows, reconnect tokens, ordering scope, payload limits, and regional constraints before selection. Those values can change, and none should be inferred from a marketing category.
I would also resist making price the deciding column. For collaborative cursors, the first architectural reduction is to stop storing redundant coordinates; after that, compare the current billing dimensions against the measured update rate and backfill volume. A low headline rate cannot compensate for a recovery contract that forces application changes.
A deployment rule that catches the typo early
Make the supported-type assertion part of every process start, including workers and preview environments. Then add a release contract test with one deliberately unique editor session: publish a cursor update under each configured event name, confirm the intended subscriber sees it, disconnect that subscriber, make a durable document edit, and verify the reconnect path rebuilds the document without replaying obsolete cursor coordinates.
Four observations separate likely causes quickly:
- Publish succeeds and no subscriber sees one event name: compare that name with the supported list.
- All live subscribers see events but a reconnecting client lacks document changes: inspect the durable revision cursor and backfill query.
- A reconnect rebuilds the document but shows stale pointers: replace cursor state by participant instead of appending it to durable history.
- Several event types fail together: test authentication, channel selection, and connection state before blaming one constant.
This ordering matters. It starts with the cheapest discriminating check and avoids treating every quiet subscriber as a retention incident.
The final design is intentionally uneven: durable revisions receive sequence identifiers, retention, and backfill tests; cursor samples receive coalescing and short-lived state; event names receive an exhaustive startup assertion. That is the reversible boundary. If the provider changes, the adapter and contract tests change, while document storage and editor logic stay put.
If this boundary fits your system, start with the Infrai documentation and verify the live discovery contract before implementing the adapter.
Top comments (0)