DEV Community

DrummondReed8257
DrummondReed8257

Posted on

Realtime Stock Watchlist Moderation: An Auditable Event-Clock Test Harness

Short answer: test participant moderation against a recorded event clock, not wall-clock sleeps. For a stock trading watchlist, the invariant is simple: a participant who loses access must not receive later symbol updates, while an already-authorized participant still receives them in order. Make that invariant observable, replayable, and independent of network timing.

I run a one-person SaaS, so my useful unit is revenue per hour. A flaky realtime test burns an hour twice: once while it fails in CI, then again while I prove the failure was noise. The practical choice is to outsource timing to a deterministic harness and spend the saved time shipping the feature.

What should a moderation test prove for a stock watchlist?

Moderation is an authorization transition, not a UI event. Model each watchlist participant as a state machine: active, muted, removed, or rejoined. The test should assert what each state may observe, rather than assert that a callback fired after 200 milliseconds.

For a watchlist containing AAPL and NVDA, I use a tiny event log:

type Event =
  | { kind: "join"; participant: string }
  | { kind: "moderate"; participant: string; state: "muted" | "removed" }
  | { kind: "quote"; symbol: string; price: number; seq: number };

const log: Event[] = [
  { kind: "join", participant: "trader-7" },
  { kind: "quote", symbol: "AAPL", price: 214.12, seq: 1 },
  { kind: "moderate", participant: "trader-7", state: "removed" },
  { kind: "quote", symbol: "NVDA", price: 139.44, seq: 2 },
];
Enter fullscreen mode Exit fullscreen mode

The key assertion is a delivery set. trader-7 may see sequence 1, but sequence 2 must be absent after removal. A second participant should receive both, in sequence order. This catches a common race where the moderator updates the database but a fan-out worker has already copied the next message into a socket queue.

I hit that race in a local test by waiting for a timer and got an intermittent expected 1, received 2. The failure disappeared under a debugger. That was the clue: the test was measuring scheduler luck, not policy.

How does an event clock remove flaky timing?

Inject a clock with explicit turns. A moderation command, authorization read, and quote fan-out become steps in a trace that the test controls. No setTimeout, no “wait until eventually,” and no dependence on a busy CI runner.

type Delivery = { participant: string; event: Event };

class EventClock {
  private turn = 0;
  next(): number {
    return ++this.turn;
  }
}

function deliver(
  participant: string,
  state: "active" | "muted" | "removed",
  event: Event,
): Delivery[] {
  if (event.kind === "quote" && state !== "active") return [];
  return [{ participant, event }];
}

const clock = new EventClock();
const trace = [
  { turn: clock.next(), action: "authorize trader-7" },
  { turn: clock.next(), action: "publish AAPL" },
  { turn: clock.next(), action: "remove trader-7" },
  { turn: clock.next(), action: "publish NVDA" },
];

if (trace[3].turn <= trace[2].turn) {
  throw new Error("moderation must precede the next fan-out turn");
}
Enter fullscreen mode Exit fullscreen mode

In production, the clock is usually a queue or log offset, not this class. The test double only exposes the ordering contract. Keep the transport behind a generic interface so the same assertions can run against WebSocket, WebRTC data channels, or a broker-backed worker. WebRTC’s recommendation describes ordered and reliable data-channel options; the application still owns authorization and replay policy.

Failure modes worth replaying

The harness earns its keep when it replays ugly traces. I keep fixtures for duplicate quote sequence numbers, a reconnect that presents an old membership token, moderation arriving between authorization and enqueue, and a delayed read receipt. Each fixture names the expected visible sequence for every participant.

A muted participant is a useful edge case. In one product, mute meant “no quote payloads but presence is visible”; in another, it meant complete silence. Pick one semantic and test it explicitly. Ambiguity here becomes a support ticket, not a protocol problem.

My replay runner stores each fixture as a plain JSON trace with three columns: the event, the membership version observed, and the delivery decision. Before a test starts, it creates two participants with different roles, publishes one quote, and snapshots both inboxes. Then it applies moderation, advances the clock one turn, and publishes a second quote. The assertion compares complete sequences, including duplicates and gaps, instead of checking only the last message. When a trace fails, the runner prints the first divergent turn and the policy version that made the decision. That detail matters during a release: a queue retry can make a duplicate look like a moderation leak, while a stale membership cache can make a genuine leak look like a transport issue. I keep the trace in version control and run it unchanged against the in-memory dispatcher and the staging broker. The fixture is small enough to review in a pull request, yet it captures the race that a sleep-based test missed.

That is the whole test.

Healthtech has the same shape. For typing indicators and read receipts, a removed participant should not receive a later receipt, while the audit stream can retain the moderation event. The payload differs; the event-clock invariant does not.

For observability, emit the trace ID, membership version, and quote sequence in test logs. Do not log patient or account identifiers. A failed assertion should answer “which policy version admitted this event?” without requiring a packet capture.

What changes when this runs at scale?

At scale I would shard the event log by watchlist, persist a monotonic sequence per shard, and make membership reads versioned. The fan-out worker checks the membership version immediately before enqueueing. That does not make delivery instantaneous; it makes the boundary explicit and measurable.

The trade-off is real. A strict ordered channel can head-of-line block a fresh quote behind a slow client. An unordered channel lowers latency but pushes gap detection and reconciliation into the client. Choose ordered delivery for the policy stream, and consider a separate best-effort channel for rapidly changing prices.

The harness should also run against a real transport in a small nightly suite. Deterministic tests prove policy ordering; they cannot prove TLS setup, browser behavior, or a proxy’s idle timeout. Your mileage may vary across mobile networks, so record those tests separately instead of weakening the invariant.

Stick with wall-clock integration tests when the requirement itself is temporal, such as an expiry at 09:30 UTC. For moderation authorization, use the event clock first. It is faster to review, easier to replay, and less likely to hide a real access-control regression behind a green build.

References

Top comments (0)