feat(shard)!: declare wire protocol 3 #117

Merged
whitlocktech merged 1 commits from chore/protocol-3-cutover into edge 2026-07-30 03:02:26 +00:00
Member

What & why

Order 6 of the Protocol 3.0 plan (v3.md §4) — the website half of the cutover, paired with link #20.

The site's declared version is the admin-set uo_link_config.protocol column, so it has to move with the sidecar's PROTOCOL_VERSION 2 → 3 or every REST call 409s and uoLinkSocket closes the WS on the ws.hello mismatch. Five places carry the number and all five move together:

Where What
server/db/schema.sql column default 1 → 3, plus the boot migration below
uoLinkConfig.model.js DEFAULT_PROTOCOL — what a site with nothing saved yet declares
uoLinkClient.js, uoLinkSocket.js the config.protocol || 1 fallbacks
ShardAdmin.jsx, .env.example the admin form's initial value, the documented env default

The two fallbacks are unreachable today (toSafe() always fills protocol), but an unset value quietly sending 1 is exactly the confusing 409 the version check exists to prevent, so they move too rather than sitting there two bumps stale.

The migration is one-shot, and that is the only subtle part

schema.sql is re-run on every boot, and every other statement in its migration block is an idempotent ADD COLUMN IF NOT EXISTS / MODIFY. A bare UPDATE uo_link_config SET protocol = 3 would not be idempotent in the sense that matters: protocol is admin-editable, so an operator who deliberately pinned an older sidecar in Admin → Shard would be silently un-pinned on their next restart. So it is gated on a marker row in settings, written after the UPDATE:

ALTER TABLE uo_link_config MODIFY COLUMN protocol INT NOT NULL DEFAULT 3;
UPDATE uo_link_config SET protocol = 3
  WHERE id = 1 AND protocol < 3
    AND NOT EXISTS (SELECT 1 FROM settings WHERE `key` = 'uo_link_protocol_3_migrated');
INSERT IGNORE INTO settings (`key`, value) VALUES ('uo_link_protocol_3_migrated', '1');

protocol < 3 rather than = 2 also carries across an install still on the old default of 1, which could not have been talking to a v2 sidecar anyway. A fresh install has no row to update and simply gets the marker plus the new column default. UOLINK_PROTOCOL still overrides for anyone who wants to pin.

Merge order matters. This lands on edge; then the four edgemain PRs merge together, and that merge is the cutover.

How it was tested

Against the local MariaDB, through ensureSchema() — the same code path the server runs on boot, not hand-applied SQL:

  • Starting state protocol = 2, column default 1, no marker → after one run: protocol = 3, default 3, marker written.
  • Then pinned back to 2 by hand and re-ran ensureSchema() → it stayed 2. The one-shot property holds, so an operator's deliberate pin survives restarts.

673 server tests, 47 client tests, and npm run build all green. No route changed, so the Swagger spec and route manifest are untouched.

Checklist

  • I have read CONTRIBUTING.md.
  • The change builds and existing tests/checks pass locally.
  • I have added or updated tests/docs where it makes sense.
  • My commits are reasonably scoped with clear messages.

AI-assisted contributions (required)

  • No AI tools were used to produce this contribution.
  • AI tools were used. Tool(s): Claude Code. I have reviewed and understand
    every change, and take responsibility for it. AI-authored commits are
    marked with a Co-Authored-By / Assisted-By trailer.

License

  • I agree that my contribution is licensed under this project's license
    (GNU GPL v3.0 or later), and I have the right to contribute it.
## What & why **Order 6 of the Protocol 3.0 plan ([`v3.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/edge/link/v3.md) §4) — the website half of the cutover**, paired with link [#20](https://gitea.whitlocktech.com/RunicGateway/link/pulls/20). The site's declared version is the admin-set `uo_link_config.protocol` column, so it has to move with the sidecar's `PROTOCOL_VERSION` 2 → 3 or every REST call 409s and `uoLinkSocket` closes the WS on the `ws.hello` mismatch. Five places carry the number and all five move together: | Where | What | |---|---| | `server/db/schema.sql` | column default 1 → 3, plus the boot migration below | | `uoLinkConfig.model.js` | `DEFAULT_PROTOCOL` — what a site with nothing saved yet declares | | `uoLinkClient.js`, `uoLinkSocket.js` | the `config.protocol \|\| 1` fallbacks | | `ShardAdmin.jsx`, `.env.example` | the admin form's initial value, the documented env default | The two fallbacks are unreachable today (`toSafe()` always fills `protocol`), but an unset value quietly sending `1` is exactly the confusing 409 the version check exists to prevent, so they move too rather than sitting there two bumps stale. ### The migration is one-shot, and that is the only subtle part `schema.sql` is re-run on **every** boot, and every other statement in its migration block is an idempotent `ADD COLUMN IF NOT EXISTS` / `MODIFY`. A bare `UPDATE uo_link_config SET protocol = 3` would not be idempotent in the sense that matters: `protocol` is **admin-editable**, so an operator who deliberately pinned an older sidecar in Admin → Shard would be silently un-pinned on their next restart. So it is gated on a marker row in `settings`, written *after* the UPDATE: ```sql ALTER TABLE uo_link_config MODIFY COLUMN protocol INT NOT NULL DEFAULT 3; UPDATE uo_link_config SET protocol = 3 WHERE id = 1 AND protocol < 3 AND NOT EXISTS (SELECT 1 FROM settings WHERE `key` = 'uo_link_protocol_3_migrated'); INSERT IGNORE INTO settings (`key`, value) VALUES ('uo_link_protocol_3_migrated', '1'); ``` `protocol < 3` rather than `= 2` also carries across an install still on the old default of `1`, which could not have been talking to a v2 sidecar anyway. A fresh install has no row to update and simply gets the marker plus the new column default. `UOLINK_PROTOCOL` still overrides for anyone who wants to pin. **Merge order matters.** This lands on `edge`; then the four `edge` → `main` PRs merge together, and *that* merge is the cutover. ## How it was tested Against the local MariaDB, through `ensureSchema()` — the same code path the server runs on boot, not hand-applied SQL: - Starting state `protocol = 2`, column default `1`, no marker → after one run: **`protocol = 3`**, default `3`, marker written. - Then pinned back to `2` by hand and re-ran `ensureSchema()` → it **stayed 2**. The one-shot property holds, so an operator's deliberate pin survives restarts. `673` server tests, `47` client tests, and `npm run build` all green. No route changed, so the Swagger spec and route manifest are untouched. ## Checklist - [x] I have read [CONTRIBUTING.md](CONTRIBUTING.md). - [x] The change builds and existing tests/checks pass locally. - [x] I have added or updated tests/docs where it makes sense. - [x] My commits are reasonably scoped with clear messages. ## AI-assisted contributions (required) - [ ] No AI tools were used to produce this contribution. - [x] AI tools were used. Tool(s): `Claude Code`. I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with a `Co-Authored-By` / `Assisted-By` trailer. ## License - [x] I agree that my contribution is licensed under this project's license (**GNU GPL v3.0 or later**), and I have the right to contribute it.
wtclaude added 1 commit 2026-07-29 23:05:39 +00:00
The site's declared version is the admin-set uo_link_config.protocol column, so
the sidecar's PROTOCOL_VERSION 2 -> 3 bump has to be matched here or every REST
call 409s and uoLinkSocket closes the WS on the ws.hello mismatch. Five places
carry the number and all five move together: the column default, the model's
DEFAULT_PROTOCOL (what a site with nothing saved yet declares), the two
`config.protocol || 1` fallbacks in uoLinkClient/uoLinkSocket -- unreachable
today, but an unset value quietly sending 1 is exactly the confusing 409 the
version check exists to prevent -- the admin form's initial value, and the
documented env default.

The boot migration is the only subtle part. schema.sql is re-run on EVERY boot,
and `protocol` is admin-editable, so a bare UPDATE would silently un-pin an
operator who had deliberately pinned an older sidecar in Admin -> Shard. It is
therefore gated on a marker row in `settings`, written after the UPDATE: the
first boot on this build migrates, every later boot is a no-op. `protocol < 3`
rather than `= 2` picks up an install still on the old default of 1, which could
not have been talking to a v2 sidecar anyway. A fresh install has no row to
update and just gets the marker plus the new column default.

Verified against the local MariaDB through ensureSchema (the production path):
2 -> 3 with the marker written and the column default now 3; pinned back to 2 by
hand, re-ran, and it STAYED 2 -- the one-shot property holds. 673 server tests,
47 client tests, client build green.

Co-Authored-By: Claude <noreply@anthropic.com>
whitlocktech merged commit e50fab241f into edge 2026-07-30 03:02:26 +00:00
whitlocktech deleted branch chore/protocol-3-cutover 2026-07-30 03:02:27 +00:00
Sign in to join this conversation.
No description provided.