Appearance
Testing: Race Conditions
Real networks deliver messages out of order. The test harness lets you reproduce these scenarios deterministically. See testing-network-control.md for the base controlled-client setup.
Poke Arrives Before Push Response
A classic race: the server broadcasts a poke (containing the result of our own mutation) before the client receives the push response acknowledging that mutation. Use flushMatching to deliver messages in a specific order:
typescript
import { hasInboundType } from "poe-apps-sdk/v1/test-utils.js";
test("poke arrives before push response", async () => {
const { store, transport } = await harness.createControlledClient();
await transport.flushUntil(store.waitForServerData());
transport.outbound.disableAutoFlush();
transport.inbound.disableAutoFlush();
const { confirmed } = await store.mutate.setValue({
key: "race",
value: "value",
});
// Send the push request to the server
await transport.outbound.flushNext();
// Deliver the poke BEFORE the push response
await transport.inbound.flushMatching(hasInboundType("poke"));
await transport.inbound.flushMatching(hasInboundType("push_response"));
await confirmed;
const value = await store.query((tx) => tx.table("data").get("race"));
expect(value).toBe("value");
expect(store.getPendingCount()).toBe(0);
harness.dispose();
});flushMatching reorders the queue to deliver the first message matching the predicate, leaving other messages in place. This is the key tool for testing message reordering.
Concurrent Mutations from Multiple Clients
Two clients mutating simultaneously must converge to the same final state:
typescript
test("concurrent mutations from multiple clients", async () => {
const alice = await harness.createControlledClient({ userId: "alice" });
const bob = await harness.createControlledClient({ userId: "bob" });
// Initialize both clients
await Promise.all([
alice.transport.flushUntil(alice.store.waitForServerData()),
bob.transport.flushUntil(bob.store.waitForServerData()),
]);
// Both clients mutate before either flushes
await alice.store.mutate.increment({ key: "counter", amount: 1 });
await bob.store.mutate.increment({ key: "counter", amount: 1 });
// Step both clients through their network queues
await Promise.all([alice.transport.step(), bob.transport.step()]);
// Step again to deliver cross-client pokes
await Promise.all([alice.transport.step(), bob.transport.step()]);
const value = await alice.store.query((tx) => tx.table("data").get("counter"));
expect(value).toBe(2);
harness.dispose();
});Pattern
disableAutoFlush()on both directions.- Drive mutations from each client.
- Use
flushMatching,flushNext, orstepto deliver messages in the order you want. - Assert convergence at the end.