Repository navigation
v0.3.0
A device can now survive being killed. Client state is durable, and a local
outbox owns the ordering and retry semantics the protocol requires.
Durable client state
Views\MultiViewClient kept everything in eight private arrays committed by
clone-and-publish, so a process that died lost all of it and had to bootstrap its
whole dataset again over whatever connection it had. Its knowledge now lives
behind Client\Contracts\ClientState: InMemoryClientState by default,
Client\Pdo\PdoClientState for real. The whole view suite runs against both from
the same fixtures.
Each page is applied in one state transaction — records, version watermarks,
tombstone watermarks, memberships and the cursor move together, because a cursor
that advanced without its records would claim progress the local data does not
have. A view reset deliberately keeps the canonical watermarks: that tombstone
watermark is what stops a stale page from resurrecting a record the client
already saw deleted.
The outbox, and the bug it exposed
Client\Outbox owns the four outcomes every client has to get right: done,
retry this exact identity, resume from here, give up on this one. Getting any
wrong is silent data loss or a queue that never moves again, so it is
implemented once rather than in every application.
A mutation's sequence is assigned when it is sent, not when it is queued.
Numbering at queue time looks harmless and is not: a write the transport refuses
has already consumed a number the server never receives, so the server waits for
it forever, every later write comes back as a gap for it, and the device is
wedged permanently with no way out. This was found by running a real client
against a real server — neither half's own tests could have shown it.
Also
FieldState's version and origin are now optional, with a documented meaning: a
record that reached a client through a projection carries neither, because the
origin names an actor the reader may not be allowed to see. The engine always
sets both on canonical state.
Breaking: MultiViewClient::__construct() takes an optional ClientState.
Verification
104 tests against the in-memory store, a store that shares no objects across
commits, SQLite, and both client states, on PHP 8.4 and 8.5; MySQL and
PostgreSQL in CI; plus the deterministic simulator and the concurrent-writer
experiment.