feat: Guest history, a Guest holds the Seat and owns its decided Games #515
No reviewers
Labels
No labels
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
spec
wayfinder:grilling
wayfinder:map
wayfinder:prototype
wayfinder:research
wayfinder:task
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
amaurycoudr/chess-project!515
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "claude/guest-history-spec-355-385bb1"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
A browser's Guest replaces the Seat token as the only proof of a Seat. Each decided Online game is stored once, owned by the Guest in each Seat, listed in "My games", readable by link and exportable as PGN.
{ mintedGuest?, snapshot }; a refused take mints nothing.game.getanswersyou: owner of a Color | reader.RoomOutcomenow sharesGameEnding(resignation,drawwith reasonagreed).20261013144201_decided_games:guests(unique hash),games(a nullable Guest per Color, CHECKs on winner and draw reason, an index per owner column withended_at).Game.pgntakes a caller'sResulttag;Game.boardEnding()is the one place that reads an ending off the board.Evidence
After: the gate is green: lint, typecheck, test (engine, API on the in-process Postgres, front), build and format:check. e2e passes 36/36, including the new
my-games.spec.ts:Story 12 (read-after-decide) is pinned by holding the database's transactions: the test fails once
game.liststops waiting on writes still in flight.Merge Danger
Door: two-way for the code; the migration only adds tables.
Blast Radius: wire
RoomOutcome's variants change on the wire. Front and API ship together.Rule changes
Made here, at the owner's request (#393 and review threads on this MR):
apps/api/CODING_STANDARDS.md: the layout paragraph and rules 1, 3 and 6 nameRoomService,PlayService,games/, and the globalDatabaseServiceandGuestServiceincommon/. Rule 2 now reads: "only that option's default may callDate.now,Math.randomorrandomBytes", with no list of services..oxlintrc.json:import/max-dependenciesis off, and its two per-file caps are gone.apps/api/.oxlintrc.json:typescript/parameter-propertiesallowsprivate readonly;common/*.module.tsandcommon/*.service.tsmay import Nest.docs/adr/README.md: row 0021 points atroom.service.tsandgame/lib/replay.ts; row 0027 follows the client's move tolib/api-client.ts.games/declared in both apps (withgames.controller.tsbesideroom.controller.tsin the API's errors override);unicorn/no-nullmoved toroom.service.ts.Proposed, for a dedicated PR (also: the now-unused
Optionalexception inapps/api/.oxlintrc.json'snew-cap):apps/api/CODING_STANDARDS.mdrule 5: "The Guest arrives in the RPC context from thex-guestheader, which the context hashes into the caller's occupant; it is never in a procedure's input (ADR 0034). [review]"apps/e2e/CODING_STANDARDS.mdrule 5: "…each Player is a separate browser context, so each holds its own Guest." Rule 7's helper list could namestranger.packages/contracts/CODING_STANDARDS.mdrule 3: "Identifiers (RoomCode,Guest) are branded strings with no format check: an unknown value is a domain answer (ROOM_NOT_FOUND, a Guest with nothing to its name), not a validation error."apps/front/CODING_STANDARDS.mdrule 1 and ADR 0011's list of verticals gaingames/.Closes #355
Closes #372
Closes #373
Closes #374
Closes #375
Closes #376
Closes #377
Closes #393
added 3 commits
f3153324- feat(contracts): define once how a Game ended, and let the Room's outcome share it57d73a71- chore(api): declare the games folder in the layer order and framework-import rulesffe0ddb2- feat(api): store each decided Online game once, owned by the Guest in each SeatCompare with previous version
added 5 commits
10df84db- refactor(api): keep the games module's storage in its service, with no side file43149e45- refactor(api): hand a decided Game to an onDecided option, not to a games argument5adf62ba- feat(contracts): add the game router, listing the caller's Games and reading one by ide9f281c4- refactor(api): name the oRPC module where the RPC context is typed, for every feature to importe4a70f56- feat(api): read stored Games over the wire: the caller's list and one Game by idCompare with previous version
added 4 commits
c0c83659- refactor(front): read how a Game ended from the contracts, not a second copy of it81190c5b- refactor(front): replay the server's line in game/, for every vertical that reads one536c1b57- chore(front): declare the games vertical in the per-vertical import rulesa47684a9- feat(front): read a stored Game by its link, oriented to its owner, and copy the linkCompare with previous version
why do we need to import useLocal at each screen ?
should we handle at the loader state the not found case it seems mor tanstack router mind
it seems a bad idea to do that it is not a big issue to load oRPC client not that we add the account featur we will use it every where (we need to do this change at multple place)
this could be simplifed
left review comments
added 2 commits
559f1c01- feat(engine): take the PGN result from the caller for an ending Status cannot expresse56f3670- feat(front): export a stored Game as PGN, its result taken from how it endedCompare with previous version
changed the description
added 4 commits
e400720d- refactor(front): name how a decided Game ended in game/, for every reader of itfa03a05c- feat(front): list this browser's decided Games in My games, reachable from the navigation0629dbca- test(e2e): both Players find a resigned Game in My games, and a stranger reads it by link97d67ee2- Merge branch 'claude/guest-history-spec-355-385bb1' into spec-355/376-my-gamesCompare with previous version
added 2 commits
762ca9dc- refactor(front): share the copy-link helper between Room and stored Game pagese7e86a79- refactor(front): read refusal codes in lib and map only Room refusals to Room textCompare with previous version
added 3 commits
448e4b87- refactor(api): fold the Room into RoomService, keyed by Room coded6bf9899- docs(api): describe the Room living in RoomService084e96e1- Merge branch 'claude/guest-history-spec-355-385bb1' into spec-355/393-room-foldCompare with previous version
added 2 commits
07d0e617- refactor(api): split the Game played in a Room into PlayServicedb5853c5- docs(api): name RoomService and PlayService in the standardsCompare with previous version
changed this line in version 10 of the diff
changed this line in version 10 of the diff
changed this line in version 10 of the diff
changed this line in version 10 of the diff
added 6 commits
269a090e- refactor(front): load the oRPC routes with the default code split0d41f227- refactor(front): find the caller's Color once in the fake Games server71bd517b- refactor(front): throw notFound from the Game loader for an unknown Gamedb2af629- refactor(front): follow the language from the router, not from each route1ed89e96- Merge remote-tracking branch 'origin/claude/guest-history-spec-355-385bb1'...c393b4a8- refactor(front): hand the unknown Game's notFound back as the Room loader hands its redirectCompare with previous version
Done in
269a090e:codeSplitGroupingsand its comment are gone fromgames.$id.tsx,games.index.tsxandroom.$code.tsx, so they use the default split. No test or config pinned it.Done in
71bd517bandc393b4a8: the loader (ensureGameingames/services/game-query.ts) turnsGAME_NOT_FOUNDintonotFound(), and the route rendersnotFoundComponent: GameNotFoundView. The query no longer caches a not-found answer, andGamePageViewhas no match. The not-found tests (unknown id, malformed id, French) moved toapp/game-page-route.test.tsx, since the router owns that page now. Type-aware lint rejectsthrow notFound(), so the error is returned and then thrown, the wayenterRoomhands back itsredirect.Done in
0d41f227: oneheldBy({ owners, guest })holds the guard (an absent Guest must not match an ownerless side), andviewedByandlistedForboth use it.listedForalso lost its early return.Tried twice to drive it from the root, and neither works:
useShownLocalein__root.tsx), but TanStack Router'sMatchInnermemoizes each route's element on the route's component, so a root re-render stops at<Outlet />;router.invalidate()on a switch reloads the matches but hits the same memo: 8 of 16 language-switch tests stayed red;<Outlet key={locale} />would work but remounts the page, losing the selection, the review position and the Room's stream;route.update(db2af629) worked but leans on TanStack internals.So, as discussed, back to one
useLocale()per route part (21e0973c,179eef15), the/games/$idnot-found page included, with the reason in the comment onuseLocaleinlib/locale.ts.resolved all threads
added 3 commits
21e0973c- revert: follow the language from each route again, not from the router179eef15- fix(front): have the unknown Game's page follow the language too13c662a0- test(front): count the Room's streams from once the screen follows itCompare with previous version
added 12 commits
ffe9363e- refactor(front): read the Room's refusal codes without nesting derived types1855d8cc- refactor(engine): tell the ending a Game's board decided2eab2914- refactor(api): read the board's ending from the engine and name each ending by its source5d55a45e- refactor(front): read the board's ending from the engine5528fdf0- refactor(api): have a run inside a Room return only its refusalfbee4321- refactor(api): name the stored Games' helpers for what they do12c91d3f- refactor(api): seat a pair through one wire fixtured94acc98- fix(api): check a Game id's shape through the encoding that mints it813d0986- refactor(front): serve both fake servers through one MSW adapter5bac90bc- refactor(front): derive a Player's result from an ending onceCompare with previous version
marked this merge request as ready
changed the description
added 52 commits
main6ba5de11- refactor(front): read the Room's refusal codes without nesting derived types99ea300c- refactor(engine): tell the ending a Game's board decideddb3c8f1c- refactor(api): read the board's ending from the engine and name each ending by its source8fb0ac5a- refactor(front): read the board's ending from the engined9018869- refactor(api): have a run inside a Room return only its refusal05795a3b- refactor(api): name the stored Games' helpers for what they do2f370e3f- refactor(api): seat a pair through one wire fixture49d4c0e2- fix(api): check a Game id's shape through the encoding that mints it2ab3bdc3- refactor(front): serve both fake servers through one MSW adaptercc48e5be- refactor(front): derive a Player's result from an ending onceCompare with previous version
Screenshots
Before is production, after is this merge request's review app at
548839b.Desktop
Mobile
je comprned aps pourquoi ce fichier existe ca devrait etre dans un service je pense
pourquoi le @optional ?
ca serait sans dout emieux de créer un service non N
un point non nest traidtionnnel : normalmeent on defini tout dans le constructor pour un element (le fait qu'il est readonly
je pense qu'on doit abandonner les # pour le privé
on veut pas defnir des fonction en dehors du service comme ca
je comprens pas ca
c'est fonciton est etonnante
ca parrait compliqué
left review comments
changed this file in version 14 of the diff
changed this line in version 14 of the diff
changed this line in version 14 of the diff
changed this line in version 14 of the diff
changed this line in version 14 of the diff
added 8 commits
4fd863ea- refactor(api): own the database in a global DatabaseService9d3abb08- refactor(api): require each service's options token4723cf5e- refactor(api): mark private members with private readonly, not #0eb73421- refactor(api): keep the services' helpers inside their classes5b9be94a- refactor(api): mint and hash Guests in a global GuestServiceda65949a- refactor(api): forget a stored Game's write with finally77a90074- refactor(api): read a stored Game's ending with one match on its row65358f25- refactor(api): write a stored Game's ending columns in its insertCompare with previous version
Fait dans
5b9be94a:occupant.tsest supprimé. UnGuestServiceglobal (common/guest.service.ts) génère le Guest (mint()), le hache (occupantOf()) et compare deux occupants ; il exporte les typesOccupantetGuestHash. RoomService l'injecte, et le contexte RPC l'obtient parORPCModule.forRootAsync({ inject: [GuestService] }), donc le hachage reste fait à l'arrivée, synchrone et sans I/O.Il ne servait à rien : chaque module fournit toujours son jeton d'options. Supprimé avec les
= {}dans9d3abb08.On a traité le vrai problème : la base n'était pas un service. Dans
4fd863ea, unDatabaseModuleglobal avec unDatabaseServicepossède le client drizzle : il le vérifie dansonModuleInit(une stack sans base ne devient jamais saine), le ferme dansonApplicationShutdownet exposedb. Le symboleDATABASEdisparaît,main.tsne passe plus qu'une URL, etAppModule/GamesModulene transportent plus la base. Le script de migration garde ses fonctions simples, hors Nest. GamesService reste un service Nest, qui injecteDatabaseService.Fait dans
0eb73421: les helpers des trois services (GamesService, RoomService, PlayService) sont des méthodes privées. Ceux que PlayService importait de room.service.ts (holderOf,lineOf,roomEndingOf,isOver,seatHolding,resolve,endOffer) sont des méthodes publiques de RoomService.Fait dans
77a90074:present()est supprimé. Un seulmatch(row)lit les deux formes valides avecP.nonNullable, et une branche{ drawReason: P.nullish }, { winner: P.nullish }lève une erreur pour une ligne qui viole les contraintes de la table.Fait dans
65358f25: l'insert écrit directementending,winneretdrawReason, sansendingColumns.Fait dans
da65949a:#forgetOnceSettledest remplacé parvoid write.finally(() => this.inFlight.delete(write)). La règle lintpromise/prefer-await-to-thenrefuse.finally, donc la ligne porte un commentaire de désactivation avec sa raison.Les
#sont remplacés parprivate readonlydans toute l'API (4723cf5e). Les propriétés déclarées dans le constructeur (constructor(@Inject(X) private readonly x: X)) ne sont pas faites : la règle linttypescript/parameter-propertiesde la config racine les refuse. Les dépendances restent donc des champsprivate readonlyassignés dans le constructeur. Je laisse ce fil ouvert : on change la règle lint ici, ou dans une PR dédiée ?added 1 commit
5e29aaaf- docs(api): state the non-determinism rule without listing servicesCompare with previous version
Fait :
typescript/parameter-propertiesautoriseprivate readonlypour l'API seule (a2ef0718, dansapps/api/.oxlintrc.json), et les dépendances injectées sont maintenant@Inject(X) private readonly x: X(fe98c748). Les objets d'options restent des paramètres simples, puisqu'ils alimentent des valeurs par défaut (options.now ?? Date.now).resolved all threads
added 8 commits
a2ef0718- chore(api): allow private readonly parameter propertiesfe98c748- refactor(api): inject Nest dependencies as private readonly parameter propertiesc3c89696- chore(repo): drop the import/max-dependencies capdc67edfb- refactor(api): declare AppOptions back in the app modulefaecb5ae- refactor(front): render My games from the games harness, beside the Game pagee2262bd7- refactor(front): keep a Player's result from an ending in game-over.ts237ebbf9- refactor(api): hash a Guest in the wire fixtures, not a fixture file of its ownb6b2172a- Merge remote-tracking branch 'origin/claude/guest-history-spec-355-385bb1' into spec-355/lint-rulesCompare with previous version
changed the description
added 68 commits
mainc8aceba5- refactor(api): read a stored Game's ending with one match on its row7fcf6015- refactor(api): write a stored Game's ending columns in its insert8dfe0ba4- chore(api): allow private readonly parameter propertiesad3ec9b4- refactor(api): inject Nest dependencies as private readonly parameter properties4bfcccbd- chore(repo): drop the import/max-dependencies capb24f5e8c- refactor(api): declare AppOptions back in the app modulee2113736- refactor(front): render My games from the games harness, beside the Game pagec6dffb64- refactor(front): keep a Player's result from an ending in game-over.ts9858629e- refactor(api): hash a Guest in the wire fixtures, not a fixture file of its own548839b7- docs(api): state the non-determinism rule without listing servicesCompare with previous version
mentioned in merge request !537