docs(link): the Protocol 3.0 cutover (v3.md order 6) #72
@@ -31,31 +31,31 @@ Missing or wrong token → **401** `{"error":"missing or invalid auth token"}`.
|
|||||||
|
|
||||||
The wire protocol is versioned so a mismatch is caught immediately instead of failing weirdly.
|
The wire protocol is versioned so a mismatch is caught immediately instead of failing weirdly.
|
||||||
|
|
||||||
- Every response carries an **`X-UOLink-Version: 2`** header.
|
- Every response carries an **`X-UOLink-Version: 3`** header.
|
||||||
- `GET /health` and the WebSocket `ws.hello` frame include `"protocol": 2`.
|
- `GET /health` and the WebSocket `ws.hello` frame include `"protocol": 3`.
|
||||||
- **Optionally**, send `X-UOLink-Version: 2` on your requests. If it disagrees with the sidecar, the request is rejected **409 Conflict**:
|
- **Optionally**, send `X-UOLink-Version: 3` on your requests. If it disagrees with the sidecar, the request is rejected **409 Conflict**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "error": "protocol version mismatch", "sidecar_protocol": 2, "client_protocol": "1" }
|
{ "error": "protocol version mismatch", "sidecar_protocol": 3, "client_protocol": "2" }
|
||||||
```
|
```
|
||||||
|
|
||||||
Pin the version you built against and compare it to the header (or `/health.protocol`) at startup.
|
Pin the version you built against and compare it to the header (or `/health.protocol`) at startup.
|
||||||
|
|
||||||
**v2 (Protocol 2.0)** added the account-provisioning surface (§6.x: `POST /accounts/create`, `DELETE /link/{account}`) and the `account.*` events. Outbound event kinds are **additive** — a v1 client that ignores unknown kinds keeps working against the live feed — but the new *endpoints* require a v2 sidecar. If you send `X-UOLink-Version: 1`, calls to the new endpoints are refused with the 409 above.
|
**v2 (Protocol 2.0)** added the account-provisioning surface (§6.x: `POST /accounts/create`, `DELETE /link/{account}`) and the `account.*` events. Outbound event kinds are **additive** — a v1 client that ignores unknown kinds keeps working against the live feed — but the new *endpoints* require a v2 sidecar. If you send `X-UOLink-Version: 1`, calls to the new endpoints are refused with the 409 above.
|
||||||
|
|
||||||
**v3 (Protocol 3.0) is being built and the version has not been bumped yet.** It is defined as *adds
|
**v3 (Protocol 3.0)** adds `world.ruleset`, `points.board` and `vendor.listing` /
|
||||||
`world.ruleset`, `points.board`, `vendor.listing` / `vendor.listing.remove`*, and the bump to
|
`vendor.listing.remove`, with the `GET /ruleset`, `/points` and `/market` reads that serve them from
|
||||||
`X-UOLink-Version: 3` happens **exactly once**, at the end, when [`v3.md`](v3.md) §4's `edge` → `main`
|
the sidecar's store. Same shape as the v2 bump: the event kinds are additive, so a v2 client that
|
||||||
cutover lands — because a bump is an operator-visible hard break (409 on every protected route, and
|
ignores unknown kinds keeps working against the live feed, but the three new endpoints require a v3
|
||||||
the website's WS closes on the `ws.hello` mismatch), so doing it per phase would break the site
|
sidecar. There is deliberately **no feature-negotiation array** — v3 implies all three kinds, so the
|
||||||
repeatedly.
|
version number alone tells you what is available.
|
||||||
|
|
||||||
Until then, sidecars on `edge` still report `2` while already carrying some v3 kinds and endpoints.
|
**Upgrading a v2 integration.** The bump is an operator-visible hard break in one direction only: a
|
||||||
That is safe in the direction that matters: event kinds are additive, and a client that ignores
|
client still declaring `2` gets a 409 on every protected route and, on the WebSocket, a closed
|
||||||
unknown kinds and tolerates a `404` on a not-yet-present endpoint keeps working. What you must **not**
|
connection on the `ws.hello` mismatch. So update the pinned version at the same time you deploy the
|
||||||
do is infer feature availability from the version number during this window — probe the endpoint, or
|
v3 sidecar. Nothing that existed in v2 changed shape, so that is the whole migration — the website
|
||||||
treat a missing `world.ruleset` as "this shard hasn't published one". There is deliberately **no
|
does it with a one-shot boot migration of its `uo_link_config.protocol` row ([`v3.md`](v3.md) §4.1);
|
||||||
feature-negotiation array**: v3 implies all three kinds.
|
a third-party client changes the constant it sends.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ GET /health (no auth)
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"status": "ok", // "ok" when plugin connected AND db reachable, else "degraded"
|
"status": "ok", // "ok" when plugin connected AND db reachable, else "degraded"
|
||||||
"protocol": 1,
|
"protocol": 3,
|
||||||
"plugin_connected": true, // is the shard link up right now?
|
"plugin_connected": true, // is the shard link up right now?
|
||||||
"database": "ok", // "ok" | "error"
|
"database": "ok", // "ok" | "error"
|
||||||
"uptime": "3d 12h",
|
"uptime": "3d 12h",
|
||||||
@@ -91,7 +91,7 @@ A push-only stream of game events as they happen. You do **not** send commands o
|
|||||||
**On connect**, the first frame is:
|
**On connect**, the first frame is:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "kind": "ws.hello", "protocol": 1 }
|
{ "kind": "ws.hello", "protocol": 3 }
|
||||||
```
|
```
|
||||||
|
|
||||||
**Then** a continuous stream of event frames, each with at least `t` (epoch ms) and `kind`. Route on `kind`.
|
**Then** a continuous stream of event frames, each with at least `t` (epoch ms) and `kind`. Route on `kind`.
|
||||||
@@ -955,7 +955,7 @@ sidecar defines no audiences. Deciding who may see what is the consuming site's
|
|||||||
A typical character page:
|
A typical character page:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const H = { "Authorization": `Bearer ${TOKEN}`, "X-UOLink-Version": "2" };
|
const H = { "Authorization": `Bearer ${TOKEN}`, "X-UOLink-Version": "3" };
|
||||||
|
|
||||||
// 1. render the roster
|
// 1. render the roster
|
||||||
const roster = await fetch(`${BASE}/roster/${account}`, { headers: H }).then(r => r.json());
|
const roster = await fetch(`${BASE}/roster/${account}`, { headers: H }).then(r => r.json());
|
||||||
|
|||||||
@@ -347,6 +347,12 @@ of 25 vendors x 40 listings and **0.3 ms** in steady state (the per-vendor diff)
|
|||||||
items / 43k mobiles. It is also the first stream to honour a per-player privacy toggle: ServUO's own
|
items / 43k mobiles. It is also the first stream to honour a per-player privacy toggle: ServUO's own
|
||||||
`PlayerVendor.VendorSearch` flag, so a shop hidden in game is hidden on the site.
|
`PlayerVendor.VendorSearch` flag, so a shop hidden in game is hidden on the site.
|
||||||
|
|
||||||
|
That completes 3.0's feature work, so the last step is the version itself: `PROTOCOL_VERSION` **2 →
|
||||||
|
3** and the coordinated `edge` → `main` merge across all four repos ([`v3.md`](v3.md) §4 and §4.1).
|
||||||
|
The bump is deliberately the *only* thing that happens at that moment — v3 adds kinds and endpoints
|
||||||
|
but changes nothing that already existed in v2 — so the operator-visible break is limited to
|
||||||
|
re-pinning the version, which the website does for itself in a one-shot boot migration.
|
||||||
|
|
||||||
### Config keys (`Config/Bridge.cfg`)
|
### Config keys (`Config/Bridge.cfg`)
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
|
|||||||
43
link/v3.md
43
link/v3.md
@@ -1,6 +1,6 @@
|
|||||||
# Protocol 3.0 — Shard content, standings & the visibility framework
|
# Protocol 3.0 — Shard content, standings & the visibility framework
|
||||||
|
|
||||||
**Status:** In progress. All work lands on an `edge` branch in each repo; `edge` → `main` is the v3 cutover.
|
**Status:** Feature-complete on `edge`; the cutover (order 6) is in review. All work lands on an `edge` branch in each repo; `edge` → `main` is the v3 cutover.
|
||||||
**Date:** 2026-07-28
|
**Date:** 2026-07-28
|
||||||
**Codebase:** ServUO 57.4, `<servuo>`, net48 / x64, Expansion **EJ**.
|
**Codebase:** ServUO 57.4, `<servuo>`, net48 / x64, Expansion **EJ**.
|
||||||
**Companion to** [`PLAN.md`](PLAN.md) (1.0 read/event plane), [`PROTOCOL_2.md`](PROTOCOL_2.md) (2.0 provisioning + world-state streams), [`ADMIN_CONTROLS.md`](ADMIN_CONTROLS.md) (staff write plane), [`INTEGRATION.md`](INTEGRATION.md) (website API).
|
**Companion to** [`PLAN.md`](PLAN.md) (1.0 read/event plane), [`PROTOCOL_2.md`](PROTOCOL_2.md) (2.0 provisioning + world-state streams), [`ADMIN_CONTROLS.md`](ADMIN_CONTROLS.md) (staff write plane), [`INTEGRATION.md`](INTEGRATION.md) (website API).
|
||||||
@@ -16,8 +16,8 @@ Each part is marked off here as it lands on `edge`. §9 carries the same state p
|
|||||||
| 3 | **C** — spawn atlas (§6) | ✅ **Done** | website [#112](https://gitea.whitlocktech.com/RunicGateway/website/pulls/112) (parsers + CLI + tables) + [#113](https://gitea.whitlocktech.com/RunicGateway/website/pulls/113) (API + pages + admin panel), docs [#67](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/67) + [#68](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/68) |
|
| 3 | **C** — spawn atlas (§6) | ✅ **Done** | website [#112](https://gitea.whitlocktech.com/RunicGateway/website/pulls/112) (parsers + CLI + tables) + [#113](https://gitea.whitlocktech.com/RunicGateway/website/pulls/113) (API + pages + admin panel), docs [#67](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/67) + [#68](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/68) |
|
||||||
| 4 | **B/2** — `points.board` (§7) | ✅ **Done** | servuo-plugins [#4](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/4), link [#18](https://gitea.whitlocktech.com/RunicGateway/link/pulls/18), website [#114](https://gitea.whitlocktech.com/RunicGateway/website/pulls/114), docs [#69](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/69) |
|
| 4 | **B/2** — `points.board` (§7) | ✅ **Done** | servuo-plugins [#4](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/4), link [#18](https://gitea.whitlocktech.com/RunicGateway/link/pulls/18), website [#114](https://gitea.whitlocktech.com/RunicGateway/website/pulls/114), docs [#69](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/69) |
|
||||||
| 5a | **B/3 dependency** — cliloc table (§8.6) | ✅ **Done** | website [#115](https://gitea.whitlocktech.com/RunicGateway/website/pulls/115), docs [#70](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/70) |
|
| 5a | **B/3 dependency** — cliloc table (§8.6) | ✅ **Done** | website [#115](https://gitea.whitlocktech.com/RunicGateway/website/pulls/115), docs [#70](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/70) |
|
||||||
| 5b | **B/3** — `vendor.listing` (§8) | 🟨 In review | servuo-plugins [#5](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/5), link [#19](https://gitea.whitlocktech.com/RunicGateway/link/pulls/19), website [#116](https://gitea.whitlocktech.com/RunicGateway/website/pulls/116), docs [#71](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/71) |
|
| 5b | **B/3** — `vendor.listing` (§8) | ✅ **Done** | servuo-plugins [#5](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/5), link [#19](https://gitea.whitlocktech.com/RunicGateway/link/pulls/19), website [#116](https://gitea.whitlocktech.com/RunicGateway/website/pulls/116), docs [#71](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/71) |
|
||||||
| 6 | **Cutover** — `PROTOCOL_VERSION` 2→3 (§4) | ⬜ Not started | — |
|
| 6 | **Cutover** — `PROTOCOL_VERSION` 2→3 (§4) | 🟨 In review | the bump: link [#PR_LINK_BUMP](https://gitea.whitlocktech.com/RunicGateway/link/pulls/PR_LINK_BUMP), website [#PR_SITE_BUMP](https://gitea.whitlocktech.com/RunicGateway/website/pulls/PR_SITE_BUMP), docs [#PR_DOCS_BUMP](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/PR_DOCS_BUMP) — then `edge` → `main`: servuo-plugins [#PR_PLUG_CUT](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/PR_PLUG_CUT), link [#PR_LINK_CUT](https://gitea.whitlocktech.com/RunicGateway/link/pulls/PR_LINK_CUT), website [#PR_SITE_CUT](https://gitea.whitlocktech.com/RunicGateway/website/pulls/PR_SITE_CUT), docs [#PR_DOCS_CUT](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/PR_DOCS_CUT) |
|
||||||
|
|
||||||
Order 5 split in two once §8.6's cliloc dependency turned out to be a client-format problem rather
|
Order 5 split in two once §8.6's cliloc dependency turned out to be a client-format problem rather
|
||||||
than a parser (see §8.6). 5a is website-only and lands first so the marketplace ships with real item
|
than a parser (see §8.6). 5a is website-only and lands first so the marketplace ships with real item
|
||||||
@@ -242,6 +242,39 @@ admin-set `uo_link_config.protocol` column — so it happens **exactly once**, a
|
|||||||
from 2 to 3, so the cutover doesn't require a manual admin edit. `UOLINK_PROTOCOL` still overrides.
|
from 2 to 3, so the cutover doesn't require a manual admin edit. `UOLINK_PROTOCOL` still overrides.
|
||||||
- No feature-negotiation array anywhere — v3 implies all three kinds.
|
- No feature-negotiation array anywhere — v3 implies all three kinds.
|
||||||
|
|
||||||
|
### 4.1 What the bump actually touches
|
||||||
|
|
||||||
|
The version lives in five places, and all five move together:
|
||||||
|
|
||||||
|
| Where | Change |
|
||||||
|
|---|---|
|
||||||
|
| `link/sidecar/src/main.rs` | `PROTOCOL_VERSION` 2 → 3 (with the v3 note beside the v2 one), plus the sidecar README's worked example |
|
||||||
|
| `website/server/db/schema.sql` | `uo_link_config.protocol` column default 1 → 3, plus the boot migration below |
|
||||||
|
| `website/server/src/model/uoLinkConfig/uoLinkConfig.model.js` | `DEFAULT_PROTOCOL` — what a site with nothing saved yet declares |
|
||||||
|
| `website/server/src/utils/uoLinkClient.js` + `uoLinkSocket.js` | the `config.protocol || …` fallbacks, so an unset value can never quietly send `1` and 409 with a confusing message |
|
||||||
|
| `website/client/.../ShardAdmin.jsx`, `website/.env.example` | the admin form's initial value and the documented env default |
|
||||||
|
|
||||||
|
**The migration has to be one-shot, and that is the only subtle part.** `schema.sql` is re-run on
|
||||||
|
*every* boot (`utils/db.js::ensureSchema`), 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 pins an older sidecar in Admin → Shard would silently be un-pinned on the next
|
||||||
|
restart. It is therefore gated on a marker row in `settings`:
|
||||||
|
|
||||||
|
```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');
|
||||||
|
```
|
||||||
|
|
||||||
|
The marker is written *after* the `UPDATE`, so the first boot on the new build migrates and every
|
||||||
|
later boot is a no-op. A fresh install has no `uo_link_config` row to update and simply gets the
|
||||||
|
marker plus the new column default. `protocol < 3` rather than `= 2` so an install that never left
|
||||||
|
the old default of `1` is carried across too — it could not have been talking to a v2 sidecar
|
||||||
|
anyway.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Part B/1 — `world.ruleset` ✅ Done
|
## 5. Part B/1 — `world.ruleset` ✅ Done
|
||||||
@@ -887,8 +920,8 @@ not a blocker here.)
|
|||||||
| 3 | **C** — spawn atlas (§6) | website, docs | none | ✅ Done |
|
| 3 | **C** — spawn atlas (§6) | website, docs | none | ✅ Done |
|
||||||
| 4 | **B/2** — `points.board` (§7) | all four | new kind + `char.profile` field | ✅ Done |
|
| 4 | **B/2** — `points.board` (§7) | all four | new kind + `char.profile` field | ✅ Done |
|
||||||
| 5a | **B/3 dependency** — cliloc table (§8.6) | website, docs | none | ✅ Done |
|
| 5a | **B/3 dependency** — cliloc table (§8.6) | website, docs | none | ✅ Done |
|
||||||
| 5b | **B/3** — `vendor.listing` (§8) | all four | new kinds | 🟨 In review |
|
| 5b | **B/3** — `vendor.listing` (§8) | all four | new kinds | ✅ Done |
|
||||||
| 6 | **Cutover** — `PROTOCOL_VERSION` 2→3, `edge` → `main` | all four | the bump | ⬜ |
|
| 6 | **Cutover** — `PROTOCOL_VERSION` 2→3, `edge` → `main` | all four | the bump | 🟨 In review |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user