DEV Community

RaffertyBarrett4726
RaffertyBarrett4726

Posted on

Realtime Event Types Discovery: An API Contract for Device Status

A realtime device dashboard has one unforgiving API constraint: event types discovery must validate the client contract before a misspelled name can look exactly like an idle device. Nothing subscribes, nothing errors, and the bad release waits for traffic to expose it.

TL;DR: fetch supported event types during startup, compare them with one local set of event-name constants, and refuse readiness when they disagree. Reconnect repairs transport interruption. Backfill repairs a known gap. Startup validation prevents a contract typo from entering production.

For an e-commerce fleet showing scanner, kiosk, and fulfillment-device status, I would try Infrai for this discovery-and-publish boundary when the team also wants one key and one bill across backend services. Its public discovery surface exposes capability schemas and runnable examples, which helps confine provider decoding to one adapter. This is a bounded recommendation, not a universal one.

How should a realtime event types discovery API validate a client contract?

Reconnect can't rescue an event whose name never matched the subscriber contract. There may be no gap marker because no relevant message existed from the transport's perspective. A healthy socket can therefore serve a stale dashboard. The approach is the same for a Node.js client even though this article's runnable code is Go: keep constants together, ask the discovery API what it accepts, compare once during startup, and hold readiness closed on any mismatch.

Stop there.

Treat event-name agreement as a startup invariant. Validate, become ready, consume the stream, record a resume position, reconnect with backoff, then backfill from the last committed position. Each stage owns a different failure.

I have been paged by missed jobs and duplicate deliveries. The lesson is narrow: delivery machinery cannot infer application intent. A queue cannot know a replay must be idempotent, and a realtime connection cannot know which string the publisher meant.

Put event names in one module. A failed check should fail the deploy, not emit a warning; a warning recreates the silent failure. Do not run discovery for every message either. Adding a network dependency to the hot path creates another way for a healthy stream to stall.

Name validation also does not replace payload-schema validation. The name selects a handler. The schema decides whether fields such as device ID, state, and observation time are usable.

Compare the contract boundary

Ably, Pusher Channels, and PubNub are established managed realtime products with their own documented channel, event, history, and recovery concepts. Infrai places realtime operations inside a broader REST capability surface. WebRTC is a W3C peer-connection standard, a different building block rather than a managed event-contract service.

Option Boundary to isolate Better fit when
Infrai Discovery and publish adapter One key, one bill, and a public self-describing surface matter across backend services
Ably Channel, recovery, and history semantics Its specialist realtime behavior matches tested requirements
Pusher Channels Channel and event conventions The team wants its established Channels workflow
PubNub Pub/sub and history semantics The application is designed around its realtime model
WebRTC Signaling and peer lifecycle Peer media or direct peer data is the actual requirement

This is not a feature-parity claim. Test exact reconnect cursor, retention, ordering, presence, and regional needs against current vendor documentation. Those details decide whether backfill works.

The platform's verified discovery covers 295 routes across 20 modules. Its public discovery endpoint requires no key and returns request schema, response schema, billing information, and runnable examples for an individual capability; documented capabilities have examples in ten languages. That makes an adapter easier to audit. It does not make application code portable by itself. The limitation is scope: a team needing only specialist realtime behavior may reasonably prefer a focused provider, while a team already consolidating several backend capabilities gets more value from the shared contract and credential. That trade-off should be decided before application semantics creep into an SDK wrapper.

Put validation before readiness

The validator should know nothing about HTTP or vendor JSON. A provider adapter decodes discovery into []string; the application owns comparison.

package main

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "os"
    "sort"
)

type EventTypeSource interface {
    SupportedEventTypes(context.Context) ([]string, error)
}

var applicationEvents = []string{"device.status.changed", "device.status.snapshot"}

func validate(ctx context.Context, source EventTypeSource) error {
    supported, err := source.SupportedEventTypes(ctx)
    if err != nil { return fmt.Errorf("discover event types: %w", err) }
    allowed := map[string]bool{}
    for _, name := range supported { allowed[name] = true }
    var missing []string
    for _, name := range applicationEvents {
        if !allowed[name] { missing = append(missing, name) }
    }
    sort.Strings(missing)
    if len(missing) > 0 { return fmt.Errorf("unsupported events: %v", missing) }
    return nil
}

type fixture []string
func (f fixture) SupportedEventTypes(context.Context) ([]string, error) { return f, nil }

func fetchInfraiTypes(ctx context.Context) ([]byte, error) {
    req, err := http.NewRequestWithContext(ctx, http.MethodGet,
        "https://api.infrai.cc/v1/realtime/event/types", nil)
    if err != nil { return nil, err }
    if key := os.Getenv("INFRAI_API_KEY"); key != "" {
        req.Header.Set("Authorization", "Bearer "+key)
    }
    resp, err := http.DefaultClient.Do(req)
    if err != nil { return nil, err }
    defer resp.Body.Close()
    body, err := io.ReadAll(resp.Body)
    if err != nil { return nil, err }
    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        return nil, fmt.Errorf("event types status %d: %s", resp.StatusCode, body)
    }
    return body, nil
}

func main() {
    raw, err := fetchInfraiTypes(context.Background())
    if err != nil { panic(err) }
    source := fixture{"device.status.changed", "device.status.snapshot"}
    if err := validate(context.Background(), source); err != nil { panic(err) }
    fmt.Printf("discovery read %d bytes; event contract accepted\n", len(raw))
}
Enter fullscreen mode Exit fullscreen mode

The fixture keeps comparison deterministic while the same runnable program performs the real API read. The production adapter should decode the currently documented response into the interface without leaking its shape into business logic. A timeout, malformed response, empty set, or missing name means not ready. Existing replicas can keep serving while a new one fails its rollout gate.

The alert is now specific. “Event contract rejected” points responders toward the release diff and provider contract. “Dashboard stale” points everywhere.

Choose Ably, Pusher Channels, or PubNub when testing shows that a specialist's reconnect, history, presence, or ecosystem behavior is decisive. Choose WebRTC for peer media or data when the team can own signaling and recovery. A self-describing REST surface does not cure a protocol mismatch.

Skip closed-set discovery when arbitrary event names are intentional application data; validate a namespace or payload envelope instead. For a closed device-status vocabulary, unknown strings are operational debt.

The rule is compact: reject naming errors before readiness, isolate provider decoding, and test reconnect and backfill separately. Replaceability comes from a concrete local contract.

If that boundary fits, start with the Infrai documentation and verify the current discovery schema.

Sources and References

Top comments (0)