docs: lead with the case a lock cannot cover - #29
Merged
Conversation
Two comments on r/rails, both negative, and the first one was right: a counter is the worst possible lead example, because one statement of SQL does it. The README opened with a shopping cart that pushed a string onto an array, which reads the same way. The first thing a skeptical reader saw was the argument against installing anything. The lead is now the ticket sale from the homepage: 100 seats, a hold, a ten-minute expiry that frees the seat, and a published count. It is the smallest example that needs three things from one number, and the three things are the actual argument. A cart append needs one. The objection now gets answered where it is asked. A new section concedes the transaction and the row lock first, including navigator.locks in the browser, then makes the case on scope rather than discipline: a lock is scoped to one transaction, on one connection, in one process, and any expiresAt or scheduledAt column is evidence the critical section already outlived it. What follows that column is a sweeper, and then a race between the sweeper and the next writer. Arguing that someone might forget the lock would lose, because the reader answers "so remember". The transactions callout already existed and asserted the limit without showing where it bites, so it now points at that section. A short worth-it and not-worth-it section moves into the first screen, and the realtime section concedes send-on-your-own-socket before explaining where push-after-write drops an update.
Greptile SummaryThe PR reframes the README around coordination that outlives a transaction and synchronizes the package version for the documentation release.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains. Important Files Changed
Reviews (2): Last reviewed commit: "release: use 0.14.3, not 0.15.0" | Re-trigger Greptile |
The realtime paragraph claimed the value is published once per change from the saving turn. The saving turn records the publication in the same transaction as the state change, which is the part that answers push-after-write. Delivery is a separate worker and is at least once, so "published once" promised something the runtime does not. The claim now matches docs/correctness.md: rows are claimed in actor revision order, subscribers reject a duplicate or stale revision, and the guarantee is that a subscriber cannot end up on an older value. That distinction is what makes the argument against push-after-write work. The point was never that a socket send is unreliable. It is that the send is not part of the write and its ordering comes from arrival rather than revision.
The framing pass added the argument the README was missing, but it added it to a page that already repeated itself. The single-row-transaction point appeared in five places. Three sections answered "is this for me" with overlapping tables. Length is not thoroughness: a reader who bounces at screen three never reaches the reference material that justified it. Removed what docs/ owns outright. Measured behavior restated docs/benchmarks.md number for number, so it keeps the four rows that change a decision and links the rest. How it works, Delivery boundaries, Requirements, and Operations restated correctness, support, and operations docs, so each keeps the claims a reader needs before clicking and drops the rest. The Comparison table restated docs/comparisons.md, so the celld paragraph stays, because it is the only place that says what to choose and why, and the table goes. Removed what the README said three times. "Good and poor fits" is gone, with its unique rows folded into "Is it worth installing here?": per document reminders, realtime projections, edge placement, and the identity-splitting rule. The pattern table survives because its "one identity per" column exists nowhere else. Fixed a claim that was not true. Design provenance said the API was redesigned around Web Components. There is no customElements or HTMLElement anywhere in src; the browser surface is a framework-neutral component registry.
The standard-readme spec requires a table of contents above 100 lines. This file is 537 with 17 sections and never had one, so a reader landing from the npm page had no map of what the page covers. An audit against that spec, the Prana et al. content categories, and the popularity correlation study found nothing else missing here: this README already carries contribution and security links, uses lists, and links out to docs. The Ruby sibling had the larger gaps.
The section answering the lock objection was titled after the mechanism, "a transaction and a row lock", which is longer than the question a reader actually asks. The heading now matches the Ruby sibling and says transactions. The body still names SELECT ... FOR UPDATE and navigator.locks, because those are what a reader reaches for. "What an actor replaces" was ambiguous in a document that also discusses Durable Objects, Rivet actors, and celld. It now says a Solid Objects actor. The not-worth-it list ended at hot identities and global counters without saying where such a reader should go. Solid Objects Pro is the commercial performance layer for the family, so the list now names it and what it adds. It says the Rails gem ships today and the Node build is in development, which is what the product page states, so a reader here does not expect something they cannot buy yet.
The README is what npmjs.com renders, and docs/ ships inside the tarball, so this work does not reach a reader until a release goes out. Published 0.14.2 still leads with the cart example and answers the lock objection nowhere. Minor rather than patch: the page a reader lands on is different, not corrected, and 0.15.0 puts the package back in step with the gem after the two drifted apart at 0.14.1 and 0.14.2.
Nothing in this release changes behavior. The runtime is untouched and every change is prose, so semver calls this a patch. The earlier bump reached for minor to resync the version numbers with the gem, which is not a reason to spend a minor: the parity ledger tracks capability, not the number.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Framing fix to the first screen and the fit sections. No API documentation was
rewritten, and no runtime code changed.
The lead example
A cart append needs one thing from one value. The ticket sale needs three:
The first is one UPDATE statement. The second is an
expiresAtcolumn plus asweeper. The third is a push on every path that changes the number. The
combination is the argument, and the example now shows all three in one class:
the guard,
this.schedule({ at, key }), andbroadcastValue(this.remaining).Answering the objection where it is asked
## Why not just use a transaction and a row lock?now sits in the firstscreen. It concedes first, in the first sentence, because a concession is what
buys the right to be believed in the paragraph after it. Then it argues scope
rather than discipline:
Arguing "you might forget the lock" would lose that exchange, because the reader
answers "so remember" and they are right.
Also
## Is it worth installing here?moves into the first screen, and says whoshould not use this, specifically.
limit without showing where it bites. It now names the cases and links to the
section that argues them.
SQL transaction or row lockrow, soit is unchanged.
before explaining the two gaps: two sends can arrive out of order, and a send
is not part of the write.
Validation
The lead example was executed against this branch as a temporary test, not read
for plausibility: three concurrent reserves serialize to
remaining97, arepeat reserve for the same buyer returns
false, andexpirereturns the seatto 98. The scratch test was removed before committing.
pnpm run checkclean,pnpm test343 passed and 13 skipped.The Ruby counterpart is cardmagic/solid-objects-ruby#54. Verifying its example
turned up a bug in the shipped homepage copy: it stores
Time.currentin actorstate, which Ruby state serialization rejects, so that
reservenever commits.The JS homepage example stores
Date.now()and is unaffected.