@junejuly-lockstep/mock-runtime
v0.7.0
Published
Runtime for the mock backends `lockstep mocks generate` writes: a seeded, visit-scoped entity store, REST and GraphQL semantics from the mock model, MSW v2 handlers (WebSocket and Server-Sent Events included), overrides, faults and response validation; se
Maintainers
Readme
@junejuly-lockstep/mock-runtime
The runtime behind the mock backends lockstep mocks generate writes into a client app
(lockstep/mocks/generated/). You rarely import it yourself: the generated handlers.ts does.
- A seeded, visit-scoped entity store: each persona starts from its committed seed file
(
generated/seeds/<persona>.json); aPOSTinserts a row that a laterGETreturns. In the browser the store lives insessionStorage, so it survives full-page navigations within one visit and is gone in the next fresh browser context;POST /__lockstep/resetstarts over. - REST semantics from the mock model: lists (filters, sorting, deterministic pagination), get
(404 when missing), create (201, next deterministic id),
PUT,PATCH,DELETE(204), nested routes filtered by foreign key, relation fields expanded from foreign keys. - GraphQL semantics: operations execute against the schema with store-backed resolvers;
create*/update*/delete*mutations change the store. - Overrides (
override('POST /api/checkout', fn)), faults for one visit (the__lockstep_faultsstorage key orPOST /__lockstep/faults, statuses 400-599) and response validation against the contract in development. - Deterministic: no
Math.random(), no wall clock (FROZEN_EPOCH_MSis the crawler's frozen instant), UUID v5 ids.
Entry points:
| Import | What |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| @junejuly-lockstep/mock-runtime | everything below plus createHandlers (MSW v2, a peer dependency), with WebSocket and Server-Sent Events handlers for ws/sse operations |
| @junejuly-lockstep/mock-runtime/core | engine, store, faults, PRNG, ids, validation, stream wire formats; no msw |
| @junejuly-lockstep/mock-runtime/node | server-side use: createMockServer (lockstep mock-server), startServerMocks (in-process setupServer with the persona per request and the reset endpoint), handleFetchRequest |
| @junejuly-lockstep/mock-runtime/freeze-clock | a Node preload (NODE_OPTIONS=--import ...) freezing Date at LOCKSTEP_FROZEN_EPOCH_MS; timers keep running |
| @junejuly-lockstep/mock-runtime/externals | a Node preload that blocks and records outbound requests to non-local hosts during a check (LOCKSTEP_EGRESS_LOG, LOCKSTEP_EGRESS_ALLOW) |
import { createHandlers, override } from '@junejuly-lockstep/mock-runtime';
import model from './lockstep/mocks/model.json';
import typical from './lockstep/mocks/generated/seeds/typical.json';
export const handlers = createHandlers({
model,
seeds: { typical },
overrides: [override('GET /api/me', async ({ next }) => next())],
});Persona states in hand-written mocks
Fixtures that serve their own world (instead of the generated seed files) read a persona's
states with personaState, which turns them into the row counts and dates the generated seeds
use, relative to the frozen clock:
import { personaState } from '@junejuly-lockstep/mock-runtime';
const s = personaState(currentPersona()); // any object with `states`
s.count('PastVisit'); // PastVisit.collection: empty 0, one 1, many 3, lots 12, max a page + 1; 3 when unset
s.date('Appointment.startsAt', { direction: 'upcoming' }); // ISO string of the bucket; null, or undefined when unset
s.epoch('PastVisit.startsAt', { direction: 'past' }); // the same in milliseconds; `today` is an hour before the clock
s.is('Subscription.status', 'past_due'); // also has, get, oneOf
s.unread(); // pinned keys nothing has read: states these mocks ignoreDate buckets: overdue (3 days ago), past (30 days ago), in-progress (started half an hour
ago; spanEnd gives its end: null when the end may be null, else half an hour ahead), today
(the clock; an hour before it with direction: 'past', an hour after with 'upcoming'),
upcoming (in 3 days), far-future (in 400 days). collectionCount, bucketEpoch,
COLLECTION_SIZES and DATE_BUCKETS are exported for code that reads the values itself.
lockstep doctor reports state keys no mock reads.
Worlds in hand-written mocks
A world (lockstep/worlds/<name>.json at the repository root) is one set of fictional records
every app of the repository shares; a persona names its world and the actor it signs in as.
Generated seeds read worlds by themselves. Hand-written fixtures read them with createWorlds:
import { base64url, createWorlds, sha256 } from '@junejuly-lockstep/mock-runtime';
import clinicWeek from '../../../lockstep/worlds/clinic-week.json';
import { personas } from '../personas';
export const worlds = createWorlds({ worlds: [clinicWeek], personas });
worlds.world(); // the active persona's world (a ResolvedWorld), or null without one
worlds.actor(); // { key: 'patient:ava', role: 'patient', ref: 'Patient/ava', entity, id, record }
worlds.records('Appointment'); // every appointment of the world, in file order
worlds.records('Appointment', { mine: true }); // the actor's: its own record and records naming it
worlds.record('Provider', 'dr-test'); // one record (also `worlds.record('Provider/dr-test')`)
// A key the backend derives from records (here SHA-256 of two ids, base64url): `key()` returns it
// and remembers which world record it stands for, so a check can tell both apps showed one visit.
const id = worlds.key('Appointment', appt, base64url(sha256(`${patient.healthieId}:${appt.id}`)));The active persona comes from the query, storage, cookie or LOCKSTEP_PERSONA, as for
currentPersona(); pass persona: () => name to choose it yourself, or a persona name as the
last argument of each call. A record names another by a field holding its id whose name, without
an Id, _id or Ids ending, is the other entity (patient, providerId, appointment_ids) or
ends in it (primaryProvider), or by a ref ("Patient/ava"). Every mocked JSON response that
carries a key registered with key() gets an x-lockstep-world header naming their records; the
check reads it to tag those values as world records. resolveWorld(json) gives the same lookups for one world
without personas (entities(), records(), record(), actors(), actor(), references(),
naming(), concerns(), problems()), and sha256, base64url, sha1 and uuidv5 compute
keys the way a backend does, synchronously and in plain JavaScript.
The main and core entries are browser-safe: they never import Playwright or Node built-ins (the
Node-only code lives behind ./node and the preloads), import only types from
@junejuly-lockstep/model, and loads graphql only when the app uses GraphQL. See the engineering
guide's "Generated mocks" section.
