Skip to content

docs: add problem guides for races, rooms, and offline state - #70

Merged
cardmagic merged 5 commits into
mainfrom
docs/problem-guides
Oct 9, 2026
Merged

cardmagic merged 5 commits into
mainfrom
docs/problem-guides

Conversation

@cardmagic

Copy link
Copy Markdown
Owner

Why

Developers search for a problem ("Node.js race condition concurrent requests", "persistent Socket.IO rooms", "SQLite WASM multiple tabs"), not for "virtual actors". These guides meet that search. Each one reproduces a failure, shows the simplest adequate fix without an actor, and only then shows a tested Solid Objects version with its limits. The site will publish them at /js/race-conditions, /js/persistent-rooms, and /browser/offline-first.

Guides

Guide Failure it reproduces Native fix Solid Objects part
docs/guides/race-conditions.md A read-modify-write across an await takes the last seat twice; a mutex in each process still oversells Conditional UPDATE in a BEGIN IMMEDIATE transaction EventSeats: holds, payment confirmation, and keyed expiry reminders on one event identity; stale-form revision check; a policy by role
docs/guides/persistent-rooms.md A Map and setTimeout lose the room and its timer on restart A row with a version column and a deadline sweep TurnRoom: ordered moves, stale_turn for a second tab, a durable turn timer, authorized reconnect by revision
docs/guides/offline-first.md An outbox in page memory loses edits on reload IndexedDB outbox, client ids, Web Locks (prose) A module worker on sharedSqliteWasm with transmit; a server route that authenticates and applies each write once

Competitor and platform facts carry primary sources checked October 9, 2026: SQLite BEGIN IMMEDIATE, Socket.IO connection state recovery (including "will not always be successful" and the adapter table), MDN Background Synchronization ("Limited availability") and Web Locks, Yjs, and Automerge.

Tests

  • test/guide-race-conditions.test.ts (10): the race and the per-process mutex race both reproduce; the atomic fix holds across two connections; two real processes hold seats at the same time and only the capacity is held (the second runtime runs in a child process, because two synchronous SQLite connections in one event loop block each other); a repeated hold applies once; a hold expires after a restart; a late expiry after confirmation changes nothing; a confirmation after expiry rejects; a stale form rejects; a buyer cannot change capacity.
  • test/guide-persistent-rooms.test.ts (10): memory loss on restart, the version-column fix and sweep, one move per turn across duplicate submissions, wrong player, a move for another player, a timer that skips a turn after restart, a timer that does not fire after the turn ended, no caller can run the timer, reconnect returns the current room to a member and nothing to a stranger.
  • test/guide-offline-first.test.ts (4): the sync route refuses an unauthorized device, applies a write, applies a repeated write once, and answers 422 for a write that can never apply.
  • test/browser/offline-first.browser.ts (3, Chromium): two tabs record three findings offline and the server applies them once, in order, after the network returns; local state survives a reload; the page-memory outbox loses its edits on reload.

Supporting changes

  • scripts/check-documentation.mjs link-checks docs/guides/, requires each guide to embed its current example files, and rejects an em dash in a guide.
  • vitest.config.ts maps solid-objects, solid-objects/core, and solid-objects/database/sqlite to src/, so examples that import the package name run under test before a build. The guide tests type-check under tsconfig.examples.json, which already maps them; tsconfig.json excludes them so the build configs stay unchanged.
  • test/support/source-hooks.mjs lets the child process run the example TypeScript from src/.
  • test/browser-server.mjs serves the guide example and adds a test-only sync route with a network switch.
  • README "Guides" section and a pointer in docs/agents.md.
  • Version 0.17.6 and its changelog section.

Edits to the drafted copy

  • race-conditions.md: "The available field" changed to "The available getter" (it is a getter, which the runtime treats as a query).
  • All three guides: the example files replace placeholder lines verbatim, and Prettier formatted the Markdown.

Validation

pnpm run format:check, pnpm run check, pnpm test (613 passed, 32 database skips), pnpm run test:coverage, pnpm run build, pnpm run pack:check (the tarball contains the 3 guides and 15 example files), pnpm run test:package, pnpm run test:recovery, pnpm run test:at-least-once, pnpm run test:browser (16 passed), pnpm audit --audit-level=high (no known vulnerabilities). The race-conditions, rooms, and offline-first tests also passed in repeated runs.

Developers search for a problem, not for "virtual actors". Three guides
start from a failure they can reproduce, show the simplest fix without an
actor, and then show a tested Solid Objects version with its limits:

- Prevent race conditions in Node.js: a lost update across an await, a
  mutex in each process that still oversells, the conditional UPDATE
  fix, and an EventSeats actor whose holds, payment confirmations, and
  expiry reminders race.
- Keep turn-based room state through restarts: process memory loses the
  room and its timer, a version column fixes the write, and a TurnRoom
  actor keeps turn order, a durable timer, and authorized reconnect.
- Offline-first state in the browser with SQLite WASM: a page-memory
  outbox loses edits on reload, and a module worker queues writes with
  transmit that the server applies once, in order.

Every factual claim was checked against the source and the cited
primary sources. Every code block is an example file that a test runs; the
race-conditions test holds seats from two real processes. The
documentation check now requires each guide to embed its current example
files and rejects an em dash in a guide. Vitest maps solid-objects to src
so examples that import the package name run before a build; the guide
tests type-check under tsconfig.examples.json, which already maps it.
@greptile-apps

greptile-apps Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Low impact] The latest commit appears safe to merge; no blocking issue was found.

Summary

The PR adds three problem guides with examples and tests. The latest commit only switches PostgreSQL, MySQL, and Redis CI images to ECR Public.

  • The race guide shows how event seat holds avoid overselling.
  • Turn-based rooms keep their state and timers through restarts.
  • Offline inspection edits stay saved and sync in order.
  • The guide links and test setup make the new examples easier to find and run.

Reviews (4) · Last reviewed commit: "ci: pull database images from the ECR Pu..." · Reviewed by Greptile

Comment thread examples/guides/race-conditions/event-seats.ts
Comment thread examples/guides/offline-first/sync-route.ts Outdated
Comment thread examples/guides/persistent-rooms/sql-room.ts Outdated
Comment thread test/guide-race-conditions.test.ts
- EventSeats rejects a capacity that is not a whole, non-negative number
  with invalid_capacity, and hold stops at available <= 0. A capacity of
  1.5 let holds drive availability below zero.
- The sync route answers 422 for UnknownOperation, so a device stops
  retrying a queued write for an operation that a server deploy removed.
- The SQL room store uses a RoomState type and guards the stored JSON;
  loadRoom throws for an unexpected shape instead of returning it.
- The cross-process race test kills its child process after each test,
  so a failed run leaves no waiting process.

Each fix started from a failing test. The guides embed the new example
files and describe the new behavior.
@cardmagic

Copy link
Copy Markdown
Owner Author

@greptileai Please review the latest commit, 4268a54. It addresses each finding, each one test first:

  1. Fractional capacity oversells seats: setCapacity rejects a capacity that is not a whole, non-negative number (invalid_capacity), and hold stops at available <= 0. New test: 1.5 and -1 both reject.
  2. Old writes keep retrying: the sync route answers 422 for UnknownOperation as well. New test: an envelope for a removed operation receives 422.
  3. Room helpers hide their types: saveRoom takes a RoomState, and loadRoom narrows the stored JSON with a guard and throws on an unexpected shape. New test for a corrupt row.
  4. Failed tests leave children waiting: the race test records each child process and kills it in afterEach.

The guides embed the updated example files and describe the new behavior. Validation: format, check, pnpm test (616 passed), build, pack, package smoke, and Chromium (16 passed).


Delivery is at least once. Write each operation so that it can run again. One room runs one write at a time. This fits turn-based play, not a high-frequency game loop or fast real-time physics.

The application owns the WebSocket server, authentication, and the display of the room. There are no transactions across two rooms. The package requires Node.js 24.4 or newer and ESM only. The package is pre-1.0.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grammar and spelling: 'and ESM only' lacks a verb; write 'and is ESM only'.

@context7

context7 Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Docs7 for cardmagic/solid-objects-js

Result Status Action
Deployment ➖ Not used —
Content review ➖ Did not run. This site has no agent runs available this month. Wait for the monthly reset or check your Docs7 plan. —

Commit 0eefeca

A device can queue a write for an actor type that a later server deploy
removes. receiveTransmitEnvelope throws UnknownActorType at enqueue, and
the route rethrew it as a 500, so the device retried a write that can
never apply. The route now answers 422 for UnknownActorType as it does for
UnknownOperation. A new route test failed with the 500 first.
@cardmagic

Copy link
Copy Markdown
Owner Author

@greptileai Please review the latest commit, c6ca93a. Old writes keep retrying: UnknownOperation already returned 422 in 4268a54; this commit also maps UnknownActorType to 422, as you suggested. New route test: an envelope for a removed actor type receives 422 (it failed with the rethrown error first). There is also a route test for a nonempty unknown operation (removedOperation). The offline guide embeds the updated route and names both errors. pnpm test: 617 passed.

Docker Hub's unauthenticated pull limit failed every database and Redis
job on this branch three times in a row. The same official images are
mirrored at public.ecr.aws/docker/library, which has no such limit for
GitHub runners. The tag run publishes to npm only after these jobs pass,
so a rate-limited run would also block a release.
@cardmagic

Copy link
Copy Markdown
Owner Author

@greptileai Please review the latest commit, 0eefeca. It only switches the CI service images (postgres, mysql, redis) to the same official images on the ECR Public mirror, because Docker Hub's unauthenticated pull limit failed the database and Redis jobs three times in a row.

@cardmagic
cardmagic merged commit 151ef1c into main Oct 9, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant