feat(sidecar): store and serve the player-vendor market index #19

Merged
whitlocktech merged 1 commits from feat/vendor-listing into edge 2026-07-29 20:03:42 +00:00
Member

What & why

Protocol 3.0 §8 (docs/link/v3.md) — order 5b, the sidecar side. Plugin in servuo-plugins #5; website in website #116; docs in docs #71.

Ingests vendor.listing / vendor.listing.remove into a vendors table and serves GET /market.

Small change, three decisions worth review.

1. No vendor_items table — the items stay inside the blob

The obvious move is to normalize, since the website does exactly that. It would be the wrong seam here: the sidecar's job for the market is outage resilience (PROTOCOL_2.md §12.2) — hand the website back what the shard last said — not search. Search lives in MariaDB, where the query surface, the indexes and the cliloc-resolved display names already are; a second search implementation here would be one more thing to keep in step with that one, for no reader.

2. There IS a remove, unlike the other 3.0 boards

points.board and world.ruleset have no delete path — the shard's set of systems is fixed at startup and the ruleset is a singleton. A vendor is different: it is dismissed, it expires, or its owner switches off the in-game Vendor Search flag. That last one is a privacy control, so dropping the row promptly is the point of it, not housekeeping.

3. /market is the only PAGED read the sidecar serves

Because it is the only board that can be a whole world's inventory. limit clamps to 1..1000 (default 200), and total comes back alongside so a caller knows when to stop rather than paging until it sees a short page — which would race a concurrent sweep.

Ordering is by serial, not by shop name. The page is a snapshot cursor for the website's reconnect backfill; a serial is stable while a shop name is renameable, so a rename mid-backfill cannot make a vendor skip or repeat a page.

The route is /market, not /vendors — axum would route the latter fine, but /vendors/:account next door is the per-account RPC, and two routes a prefix apart meaning "this player's shops" and "every shop on the shard" is a readability trap nobody wins.

Frames are served verbatim, owner names and coordinates included. Not an oversight: the sidecar defines no audiences (v3.md §3.2). Deciding who may see a vendor's owner or whereabouts is the website's job and is admin-configurable there.

How it was tested

cargo build and cargo clippy --all-targets clean.

Run against the real shard with the plugin from servuo-plugins #5:

  • 27 vendors / 1,040 listings ingested off the live sweep and served back through GET /market, frames intact — nested location, owner fields, count/total/truncated, every item's cliloc.
  • Paging exercised (?limit=5, ?limit=27) with total correct.
  • Insert-then-remove driven over the loopback socket with the shard stopped: total=28, synthetic=truetotal=27, synthetic=false. This is the delete path the plugin cannot easily be made to exercise without a game client.
  • Rows survive a shard restart (they are in SQLite), which is the outage-resilience property the whole design leans on.
  • The website's reconnect backfill walked /market and logged snapshotted player-vendor market from /market {"count":27}.

X-UOLink-Version still reports 2 — the bump to 3 is order 6, deliberately once, at the edgemain cutover.

CI note: release.yml only runs on merge to main, so this PR (targeting edge) runs no CI — local verification above is the gate.

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 (Opus 5). I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with a Co-Authored-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 Protocol 3.0 **§8** ([`docs/link/v3.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/edge/link/v3.md)) — order 5b, the sidecar side. Plugin in servuo-plugins #5; website in website #116; docs in docs #71. Ingests `vendor.listing` / `vendor.listing.remove` into a `vendors` table and serves `GET /market`. Small change, three decisions worth review. ### 1. No `vendor_items` table — the items stay inside the blob The obvious move is to normalize, since the website does exactly that. It would be the wrong seam here: **the sidecar's job for the market is outage resilience** (`PROTOCOL_2.md` §12.2) — hand the website back what the shard last said — **not search.** Search lives in MariaDB, where the query surface, the indexes and the cliloc-resolved display names already are; a second search implementation here would be one more thing to keep in step with that one, for no reader. ### 2. There IS a remove, unlike the other 3.0 boards `points.board` and `world.ruleset` have no delete path — the shard's set of systems is fixed at startup and the ruleset is a singleton. A vendor is different: it is dismissed, it expires, or **its owner switches off the in-game Vendor Search flag**. That last one is a privacy control, so dropping the row promptly is the point of it, not housekeeping. ### 3. `/market` is the only PAGED read the sidecar serves Because it is the only board that can be a whole world's inventory. `limit` clamps to 1..1000 (default 200), and `total` comes back alongside so a caller knows when to stop rather than paging until it sees a short page — which would race a concurrent sweep. **Ordering is by `serial`, not by shop name.** The page is a snapshot cursor for the website's reconnect backfill; a serial is stable while a shop name is renameable, so a rename mid-backfill cannot make a vendor skip or repeat a page. The route is **`/market`, not `/vendors`** — axum would route the latter fine, but `/vendors/:account` next door is the per-account RPC, and two routes a prefix apart meaning "this player's shops" and "every shop on the shard" is a readability trap nobody wins. Frames are served **verbatim**, owner names and coordinates included. Not an oversight: the sidecar defines no audiences (`v3.md` §3.2). Deciding who may see a vendor's owner or whereabouts is the website's job and is admin-configurable there. ## How it was tested `cargo build` and `cargo clippy --all-targets` clean. Run against the **real shard** with the plugin from servuo-plugins #5: - **27 vendors / 1,040 listings** ingested off the live sweep and served back through `GET /market`, frames intact — nested `location`, owner fields, `count`/`total`/`truncated`, every item's `cliloc`. - Paging exercised (`?limit=5`, `?limit=27`) with `total` correct. - **Insert-then-remove** driven over the loopback socket with the shard stopped: `total=28, synthetic=true` → `total=27, synthetic=false`. This is the delete path the plugin cannot easily be made to exercise without a game client. - Rows survive a shard restart (they are in SQLite), which is the outage-resilience property the whole design leans on. - The website's reconnect backfill walked `/market` and logged `snapshotted player-vendor market from /market {"count":27}`. `X-UOLink-Version` still reports **2** — the bump to 3 is order 6, deliberately once, at the `edge` → `main` cutover. **CI note:** `release.yml` only runs on merge to `main`, so this PR (targeting `edge`) runs no CI — local verification above is the gate. ## 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 (Opus 5)`. I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with a `Co-Authored-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 14:54:39 +00:00
Protocol 3.0 §8. Ingests vendor.listing / vendor.listing.remove into a `vendors`
table and serves GET /market.

The frame is authoritative for one vendor, so the upsert is a whole-row
overwrite. Unlike the other 3.0 boards there IS a remove: a vendor is dismissed,
expires, or its owner switches off the in-game Vendor Search flag — the last of
those is a privacy control, so dropping the row promptly is the point.

Items ride inside the stored blob and are deliberately not normalized into a
vendor_items table. The sidecar's job for the market is outage resilience
(PROTOCOL_2.md §12.2), not search; search lives in MariaDB on the website side,
where the query surface, the indexes and the cliloc-resolved names already are.

/market is the only PAGED read the sidecar serves, because it is the only board
that can be a whole world's inventory. limit clamps to 1..1000 (default 200) and
`total` comes back so a caller knows when to stop rather than paging until it
sees a short page, which would race a concurrent sweep. Ordering is by SERIAL,
not shop name: a serial is stable while a shop name is renameable, so a rename
mid-walk cannot make a vendor skip or repeat a page.

The route is /market and not /vendors: /vendors/:account next door is the
per-account RPC, and two routes a prefix apart meaning "this player's shops" and
"every shop on the shard" is a readability trap.

Frames are served verbatim, owner names and coordinates included — the sidecar
defines no audiences (v3.md §3.2).

Verified against the live shard: 27 vendors / 1,040 listings ingested from the
plugin, plus a synthetic insert-then-remove confirming the delete path.

Co-Authored-By: Claude <noreply@anthropic.com>
whitlocktech merged commit 05e192ca70 into edge 2026-07-29 20:03:42 +00:00
whitlocktech deleted branch feat/vendor-listing 2026-07-29 20:03:42 +00:00
Sign in to join this conversation.
No description provided.