Remote / foldkit-remote

Testing what a reader sees: Trace

"A list on screen never blinks to Loading" depends on the reducer, the read entries and the order answers land in. foldkit-remote/testing checks it in Vitest, with no browser. Trace.run runs your update and the domain's own Subscriptions from a Model. It holds every request they make. Each step is one of your Messages, or a Respond that lets one held request go on, answered by the RemoteClient you pass. Every Model in between is recorded, so a regression that lasts one Message cannot fall between samples.

ts
import { Respond, Trace } from 'foldkit-remote/testing'

const trace = await Trace.run(Data, {
  start: loaded, // a Model showing the list
  update,
  active,
  client: server, // a fake, or your server's in-process client
  steps: [
    Message.Created({ id: 'p2', name: 'Two' }),
    Respond.mutation(Create), // the answer lands first
    Respond.read(Project, 'p2'), // then the new row's fields
  ],
})

expect(Trace.regressions(trace, { list })).toEqual([])
expect(Trace.tags(trace, list)).toEqual(['Ready', 'Refreshing', 'Ready'])
  • The trace decides when; the client decides what. Respond.query(ref), Respond.read(Entity, id) and Respond.mutation(M) each answer the oldest held request of their kind, and each takes { fail } to answer with an error instead. Respond.all answers everything held, and what that leads to, until nothing is. Reorder the steps to try another interleaving.
  • Trace.regressions names each time a read that showed a value read Loading, Initial, or Failed without it. It gives the step, the Message and Data.why; it is Data.timeline over the trace, kept to its regressions. NotFound is not a regression, since the server said the row is gone. Trace.tags is each change of a read's tag.
  • It fails loudly. A Respond that matches nothing held throws and lists what is held. A request abandoned by a restart is no longer held. A Subscription or Command that dies throws. An update that does not take Remote's Messages is a type error.
  • What it leaves out. Its clock stands still, at now (0 unless given): every Command it runs and every answer it asks for reads that time (Remote.now, an answer's date), and a retention grace, an age-out and a live restart never fire. A live stream sends nothing, so script its events as LiveReceived Messages. Only Remote's Commands run, and entry.skipped names the others. It drives a domain whose Messages are in your union, not a Remote.fold.
  • Durable writes need the outbox. Pass outbox (your Data.outbox wiring) and the trace starts from its restore, over an empty store of its own, and runs its Subscriptions as one tab: a durable write is stored and sent by it, and Respond.mutation answers that send.