The local-first set is complete
Three libraries, one registry, and every piece consumed by a shipped sample before its tag settled.
When the library ecosystem opened, we sketched the set a local-first developer actually needs: the machinery of working offline, the machinery of working with rows, and the machinery of working with money. All three are now published, and the claim this post exists to make is not “they exist” — it is that every one of them is consumed by a shipped sample application, which is the only review a library API respects.
| library | what it decides | consumed by |
|---|---|---|
pwa 2.0.0 |
what a queued unit of work is, when it counts as sent, what a degraded mode is called | Route Orders, Stock Count |
table 1.2.0 |
filtering, sorting, paging, grouping rows on the device — with the index that keeps a sorted read from going quadratic | the Ledger |
money 1.0.0 |
what an amount is, where a rounding decision happens, how a total splits without losing a centime | Route Orders |
The outbox grew up: partition-tolerant by default
The big change since the ecosystem post is pwa 2.0, and it
was not designed at a whiteboard. It was designed against
field evidence from a real restaurant: on 15 August a faulty
refrigerator kept tripping a breaker in a restaurant in France, every
router reboot changed the server’s address, and because browsers bind
storage to the origin, two hours of the owner’s notes became
unreachable. The design that came out of that incident is written up in
PARTITION-FOUNDATIONS.md,
and 2.0 is that document executed:
Replay is ordered and idempotent. Entries transmit strictly in queue order, one request in flight, and a failure stops the replay — a later entry must never overtake an earlier one’s retry. The entry id is minted on the device before the first send, so a server that sees it twice returns the original verdict instead of creating a second order.
A full store refuses loudly. queue() persists the
entry before answering ok; a store that cannot hold it says
storage-full by name instead of accepting work it cannot keep.
1.x could lose that quietly. 2.0 cannot.
The degraded mode has a name, and Ring can read it. The library
maintains a rung — alone, streaming,
unreachable — and pushes it into the VM, because
a rule like “card payment needs the server; pay cash, or wait”
is a business rule, and business rules live in Ring, not in UI glue. The
connection-lost banner fires only after eight seconds of true silence
— the exact number a restaurant owner’s complaint tuned.
And it is tested the way outages actually happen: a harness drives a real
world, on the real VM, through a real TCP proxy whose sever()
destroys live sockets mid-stream. Twenty checks in 0.7 seconds, including
the one the first run failed — which found a genuine design gap about
what a reconnecting device may forget. The fix is a rule now, not a
patch.
Money, because doubles are not an accounting policy
The newest library is the smallest and the most opinionated. An amount is
integer minor units plus a currency —
{"m":1250,"c":"EUR"} — and four rules carry the rest:
a typed price is parsed exactly while a computed value is
rounded on purpose; there is exactly one rounding function in the
whole library; currencies never mix silently; and an allocation always sums
to the whole — 100 F split three ways is 34 + 33 + 33,
and no centime has ever gone missing between three people who can count.
Route Orders now prices with it, and the one visible change is the honest
one. The tax on a 137 750 F order used to be
26 172.4999… in a double, passing its test on tolerance; it is
now 26 173 F, by a half-up rule with a name, applied where
you can see it. It knows XOF has no subdivision and Tunisian dinars carry
three decimals, and it will refuse "12.50" as an XOF amount
rather than round somebody’s typed price.
mLine = MoneyMul(mPrice, 10 * 0.95) # a case of ten, 5% off: rounds ONCE
mTax = MoneyPercent(mLine, 19) # integer arithmetic, half-up at the end
mTotal = MoneyAdd(mLine, mTax)
aParts = MoneySplit(mTotal, 3) # the parts sum EXACTLY
The habit that built all three
Publish, then immediately consume, then let the tag rest. Every release in this set moved a real sample onto itself before anything downstream could pin it, and the habit has paid three times: the second application found the duplicate-send bug that became pwa 1.0.1; moving Route Orders onto 2.0 caught a batch API the refactor had dropped, while the tag was still cheap to re-cut; and the partition harness’s first run corrected the reconcile contract itself. A library with no consumer moving against it is a library whose regressions are all still in the future.
The other half of the habit is that the installer keeps the
promises. Every download is verified against the registry’s sha256
before a byte is unpacked; update fetches and verifies the new
version before deleting the old, so a failed download leaves the project
exactly as it was; remove unwires every page an install ever
touched and leaves index.html byte-identical. The second
project on a laptop installs from the package cache with no network at all
— which, for tooling that exists to build offline-capable
applications, is not a nice-to-have.
Try the set
From any folder with an index.html, using the binary
already in the starter kit:
ringscript add pwa
ringscript add table
ringscript add money
The format and the tooling are documented in LIBRARIES.md, the hands-on tutorial in using-libraries.md, and the partition design — the storage survey, the outbox contract, the rung, the test story — in PARTITION-FOUNDATIONS.md. The registry is still one JSON file, and a pull request is still the whole of the review.