Skip to content

The inventory is as of the last world save; say so, and let a player ask (#117) - #124

Merged
CaYatur merged 2 commits into
mainfrom
feat/inventory-refresh
Jul 29, 2026
Merged

The inventory is as of the last world save; say so, and let a player ask (#117)#124
CaYatur merged 2 commits into
mainfrom
feat/inventory-refresh

Conversation

@CaYatur

@CaYatur CaYatur commented Jul 29, 2026

Copy link
Copy Markdown
Owner

The inventory is as of the last world save; say so, and let a player ask (#117)

Two water buckets in game showing as one, in the app and on the website alike,
looked like a counting bug. It is not. players.ts reads
playerdata/<uuid>.dat, and Minecraft writes that file when the world saves or
the player disconnects — so everything MSMS shows about an inventory is as of
the last save, which on a default server is up to five minutes ago. The number
was not wrong, it was old, and nothing said so.

So the profile now carries dataAt — when that file was actually written — and
the inventory heading says it. A number with its age next to it is data; the
same number alone is a bug report.

And there is a refresh, for your own inventory only. A button that re-read the
same stale file would be theatre, so it asks the server to save-all first and
then drops the cached roster. Flushing is per-world rather than per-player, so
concurrent refreshes share one within a five-second window — otherwise ten
players at three a minute each becomes thirty saves a minute on one world.

The budget is the two windows from the report: 3 a minute and 100 an hour,
both of which have to hold. One without the other is no limit — a per-minute cap
alone allows three every minute all day, an hourly cap alone allows a hundred in
the first ten seconds.

A refusal costs nothing, which is deliberately the opposite of the two-bucket
rule in the account-verification limiter. There the buckets are different
dimensions — a name and an address — and spending both stops one budget
shielding the other. Here both windows govern the same person, so charging the
hourly budget for a request the per-minute window already refused would let
someone clicking a dead-looking button burn their whole hour without a single
refresh happening.

Own-only and session-gated: an anonymous request and an admin panel token both
get 401, because the flush is a real cost and one visitor must not be able to
spend it on everybody's behalf.

Asserted: three then refused, the minute window rolls, refusals consume nothing
across fifty attempts, exactly 100 grants in an hour of 30-second attempts with
the hour refusing rather than the minute, and — since the window slides — the
invariant that no 60-minute span ever holds more than 100 grants. Over HTTP:
401 for anonymous and for an admin token, three 200s then a 429 carrying
Retry-After.

…ask (#117)

Two water buckets in game showing as one, in the app and on the website alike,
looked like a counting bug. It is not. `players.ts` reads
`playerdata/<uuid>.dat`, and Minecraft writes that file when the world saves or
the player disconnects — so everything MSMS shows about an inventory is as of
the last save, which on a default server is up to five minutes ago. The number
was not wrong, it was old, and nothing said so.

So the profile now carries `dataAt` — when that file was actually written — and
the inventory heading says it. A number with its age next to it is data; the
same number alone is a bug report.

And there is a refresh, for your own inventory only. A button that re-read the
same stale file would be theatre, so it asks the server to `save-all` first and
then drops the cached roster. Flushing is per-world rather than per-player, so
concurrent refreshes share one within a five-second window — otherwise ten
players at three a minute each becomes thirty saves a minute on one world.

The budget is the two windows from the report: **3 a minute and 100 an hour**,
both of which have to hold. One without the other is no limit — a per-minute cap
alone allows three every minute all day, an hourly cap alone allows a hundred in
the first ten seconds.

A refusal costs nothing, which is deliberately the opposite of the two-bucket
rule in the account-verification limiter. There the buckets are different
dimensions — a name and an address — and spending both stops one budget
shielding the other. Here both windows govern the same person, so charging the
hourly budget for a request the per-minute window already refused would let
someone clicking a dead-looking button burn their whole hour without a single
refresh happening.

Own-only and session-gated: an anonymous request and an admin panel token both
get 401, because the flush is a real cost and one visitor must not be able to
spend it on everybody's behalf.

Asserted: three then refused, the minute window rolls, refusals consume nothing
across fifty attempts, exactly 100 grants in an hour of 30-second attempts with
the *hour* refusing rather than the minute, and — since the window slides — the
invariant that no 60-minute span ever holds more than 100 grants. Over HTTP:
401 for anonymous and for an admin token, three 200s then a 429 carrying
Retry-After.
Copilot AI review requested due to automatic review settings July 29, 2026 00:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

I built the limiter around "a refusal costs nothing" and then broke that rule
one layer above it. The route spent a token and only afterwards discovered
whether a flush could happen at all — so a player on a running server with no
RCON burned three a minute on requests that did nothing, and the refresh they
were actually waiting for was refused by a budget spent on failures.

Feasibility is checked first: a stopped server is refreshable (its player data
was written on shutdown, so re-reading is the whole job), a running one needs a
channel to ask for the save, and anything else is a 409 that costs nothing.

The per-actor state map is also bounded now. It is keyed by a name a stranger
chooses, and an actor with no live hits is indistinguishable from one who has
never asked, so dropping them is free.

Also removed an assertion I had just written that asserted nothing — an
`if (!ok) {}` with an empty body. The three 200s above it already prove
feasibility is checked before the budget, which is what I was reaching for.
@CaYatur

CaYatur commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

Self-review: check the flush is possible before spending the budget

I built the limiter around "a refusal costs nothing" and then broke that rule
one layer above it. The route spent a token and only afterwards discovered
whether a flush could happen at all — so a player on a running server with no
RCON burned three a minute on requests that did nothing, and the refresh they
were actually waiting for was refused by a budget spent on failures.

Feasibility is checked first: a stopped server is refreshable (its player data
was written on shutdown, so re-reading is the whole job), a running one needs a
channel to ask for the save, and anything else is a 409 that costs nothing.

The per-actor state map is also bounded now. It is keyed by a name a stranger
chooses, and an actor with no live hits is indistinguishable from one who has
never asked, so dropping them is free.

Also removed an assertion I had just written that asserted nothing — an
if (!ok) {} with an empty body. The three 200s above it already prove
feasibility is checked before the budget, which is what I was reaching for.

@CaYatur
CaYatur merged commit f8f3b68 into main Jul 29, 2026
1 check passed
@CaYatur
CaYatur deleted the feat/inventory-refresh branch July 29, 2026 00:19
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.

2 participants