feat(games): derive the clock instead of ticking it - #203
Open
Utzig26 wants to merge 1 commit into
Open
Conversation
Implements RFC-004. No timer, no cron, no worker: the remaining time is a function of Date.now() evaluated whenever the game is read. The mechanism is the one worth keeping from the old code. It is stateless, scales horizontally without coordination and survives a process restart. The part not worth keeping was the side effect: findOne called clock, which called save, so every read of a game was a write. It violated the semantics of GET, multiplied the load on mongo per read, and raced whenever several clients looked at the same game. projectGame is a pure function of the document and an instant. It returns the discounted clocks and, when the flag has fallen, a finished status with the win on time, without touching the document. GameResponseDto calls it in its constructor, so no read path can forget to project and none of them write. The loss is only persisted on a mutation. Between the flag falling and the next move, the stored document still reads A while every read already reports F with the result. That is the trade the old design paid a write per read to avoid, for a state nobody queried directly. The first white move is charged now. The old code, seeing a single move in the history, only set turnTime and returned without debiting the time white had spent, so the opening move was free. The reference for the first move is startedAt, recorded when the second player joins. The 409 for an expired clock does not carry the result, which the RFC had said it would. The error envelope has no data field, and inventing one would break the contract precisely in the layer that exists to keep it uniform. The mutation persists the end and answers Time is up; the next read shows the finished game. 25 unit tests, all of them passing the instant as a parameter so none of them sleep, and 8 end-to-end that rewind the recorded timestamps to move the clock. The one that closes the debt compares updatedAt before and after three reads.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements RFC-004. No timer, no cron, no worker.
The mechanism, kept
The lazy clock is the best idea the old code had and it survives almost intact: remaining time is a function of
Date.now()evaluated whenever the game is read. Stateless, scales horizontally without coordination, survives a process restart.The side effect, removed
What did not survive is that
findOne()calledclock(), which calledgame.save(). Every read of a game was a write. It violated the semantics ofGET, multiplied the mongo load per read, and raced whenever several clients looked at the same game.projectGame(game, now)is a pure function returning the discounted clocks and, when the flag has fallen, a finished status — without touching the document.GameResponseDtocalls it in its constructor, so no read path can forget to project, and none of them write.The test that closes the debt:
The trade, stated
The loss is persisted only on a mutation. Between the flag falling and the next move the stored document still reads
A, while every read already reportsFwith the result. Both sides have a test.That is exactly what the old design paid a write per read to avoid — for a state nobody queried directly.
The free first move is gone
Seeing a single move in the history, the old code only set
turnTimeand returned without debiting the time white had spent. The opening move cost nothing. The reference for the first move is nowstartedAt, recorded when the second player joins.One deviation from the RFC
The RFC said the 409 for an expired clock would carry the result. It does not. The error envelope has no
datafield, and inventing one would break the contract precisely in the layer that exists to keep it uniform. The mutation persists the end and answersTime is up; the next read shows the finished game. The RFC is updated with the reason.Verification
245 tests green, clean
stricttypecheck and lint.updatedAtuntouched across reads, a finished-on-read game still stored asA, and the loss persisted once a move is attempted.