Skip to content

v0.4.0

Choose a tag to compare

@sylvesterdamgaard sylvesterdamgaard released this 18 Sep 19:28
· 61 commits to main since this release

Thirteen defects from an external review, each reproduced as a failing test
before it was fixed. Several lose or strand writes; upgrade before shipping a
client.

The outbox was keyed by the wrong thing (breaking)

It was keyed by replica alone. Two consequences:

  • A push naming one entity type drained the entire queue and submitted every
    other type's writes as that type. At best the server refuses the fields and
    the write is discarded silently; at worst the field names overlap and it
    creates a wrong record.
  • The server keys an acknowledgement stream by space and replica, so a
    device writing to a second space offered it a number that space had never
    seen, was told it was a gap, and could not lower its own counter to recover.

OutboxStore::head(), acknowledged(), setAcknowledged() and pending()
changed signature; Outbox::resumeAfter() now takes the mutation whose answer
it is acting on.

The two stores disagreed

PdoStore::scanRecords() could not match a field that was never set — the SQL
asserted a sync_fields row exists, and a never-set field has none, while the
in-memory store matched it. A bootstrap that omits a record still advances its
cursor, so the delta never repaired the omission either.

The in-memory watermark was derived from retained commits, so pruning all
history rewound it to zero and the next mutation reused a sequence a client had
already consumed.

Capabilities that existed but could not be reached

Outbox::queue() had no dependency argument, so an offline write chain could
not be expressed and two edits to the same field conflicted with each other. A
bootstrap interrupted part-way could only be restarted, which the replica
correctly refuses as out of order; the pending continuation token is now
exposed so it can be resumed.

Pruning

prune() is atomic, and a read re-checks the retention horizon afterwards. A
prune landing between a reader's horizon check and its query removed commits the
reader never saw, and its cursor advanced past them as though they had been
delivered.

Verification

125 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.