mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 14:41:28 -07:00
178d3b5b0e
* [auth][api] accept the 20250424.01 client * [2025] unstable * 20250718.0 * correct one this time * stubs * more stubs * more stubs * [lists] add worker * [ai] route stubs * [api] player photo setting * [econ] add roomEconConfig route * [infra] update worker generators * [worker] add cards/moderation/platformnotification workers * [lists] updates to some endpoints * [clubs] stub out announcement endpoint, for now * [econ] stub out season endpoints for now * [chat] apps/chat stub out party endpoint not sure the shape yet * [api] stub out statsig and lockeditems * [doc] new services * [lists] stub the bulk endpoint * [datacollection] add placeholder service until we can kill it * [api] set gifting to lvl5 * update lock * [cdn] enable cache * [match] matchmake v2 * [lists] stub some lists * [ai] stubs * [rooms] new subroom save endpoint * [econ] add bulk purchase endpoint * [discovery] update featured creator to 1 for fun * [api] add photo settings flag * [chat] fixup chat permissions (sorta) * [auth] restrictions endpoint * [rooms] contributed endpoint * [api] fix outfit endpoint * [discovery] attempt to fix store * [chat] privacy endpoints * [api] cheered images * [rooms] add xp endpoint (disbaled) * [rooms] add xp endpoint (disabled) * update images-db for cheers * [rooms] add autocomplete endpoint * [cdn/img] increase cache ttl for statics * [api] bulk route for images * [accounts] add banner image * [api] add misc missing endpoints * [discovery] remove AI tab * [platformnotifications] stub some endpoints * [lists] add some more lists * [rooms] additional endpoints * [chat] stub a few privacy endpoints * [econ] stub some endpoints * misc db fixes * [api] tweak shape for images v6 * [rooms] dont show trending RROs
12 KiB
12 KiB
- `just install` - Install dependencies
- `just dev` - Run development servers (uses `bun runx dev` - context-aware)
- `just test` - Run tests with vitest (uses `bun vitest`)
- `just build` - Build all workers (uses `bun turbo build`)
- `just check` - Check code quality - deps, lint, types, format (uses `bun runx check`)
- `just fix` - Fix code issues - deps, lint, format, workers-types (uses `bun runx fix`)
- `just deploy` - Deploy all workers (uses `bun turbo deploy`)
- `just preview` - Run Workers in preview mode
- `just new-worker` (alias: `just gen`) - Create a new Cloudflare Worker
- `just new-package` - Create a new shared package
- `just update deps` (alias: `just up deps`) - Update dependencies across the monorepo
- `just update pnpm` - Update pnpm version
- `just update turbo` - Update turbo version
- `bun turbo -F worker-name dev` - Start specific worker
- `bun turbo -F worker-name test` - Test specific worker
- `bun turbo -F worker-name deploy` - Deploy specific worker
- `bun vitest path/to/test.test.ts` - Run a single test file
- `pnpm -F @repo/package-name add dependency` - Add dependency to specific package
- Cloudflare Workers monorepo using pnpm workspaces and Turborepo
- `apps/` - Individual Cloudflare Worker applications
- `packages/` - Shared libraries and configurations
- `@repo/oxlint-config` - Shared oxlint configuration
- `@repo/typescript-config` - Shared TypeScript configuration
- `@repo/hono-helpers` - Hono framework utilities
- `@repo/tools` - Development tools and scripts
- Worker apps delegate scripts to `@repo/tools` for consistency
- Hono web framework with helpers in `@repo/hono-helpers`
- Vitest with `@cloudflare/vitest-pool-workers` for testing
- Syncpack ensures dependency version consistency
- Turborepo enables parallel task execution and caching
- Workers configured via `wrangler.jsonc` with environment variables
- Each worker has `context.ts` for typed environment bindings
- Integration tests in `src/test/integration/`
- Workers use `nodejs_compat` compatibility flag
- GitHub Actions deploy automatically on merge to main
- Changesets manage versions and changelogs
- Use tabs for indentation, spaces for alignment
- Type imports use `import type`
- Workspace imports use `@repo/` prefix
- Import order: Built-ins → Third-party → `@repo/` → Relative
- Prefix unused variables with `_`
- Prefer `const` over `let`
- Use `array-simple` notation
- Explicit function return types are optional
Response shapes the Rec Room client depends on. These were found by watching the live
client, not by reading a spec: when one is wrong the client renders nothing or hangs
rather than erroring, so tests won't catch a regression. Don't "clean up" an
inconsistency here without checking the client first.
- Player image lists (
api:/api/images/v5|v4/player/:id,/api/images/v3/feed/player/:id) must use thetoImagesPlayerprojection —Id→SavedImageId,Type→SavedImageType, noTaggedPlayerIds. Serving the rawSavedImagerenders blank thumbnails. - The room photo feed (
api:/api/images/v4/room/:roomId) serves the rawSavedImageand displays correctly. It is deliberately NOT projected — do not unify these two. - A club's
AdditionalImages(clubs) is an array of wholeSavedImagerecords, not image names — a bare string array fails the client's parser ("expected '{'"). The list is packed: removing an image shifts the rest up, never leaving a blank slot. - A room's
LoadScreens(rooms:PUT /rooms/:id/loadscreen) is an array — the client's parser wants one — but the client renders only the FIRST entry and only ever posts one. So the endpoint REPLACES the list rather than appending: an appended screen sits unreachable behind the old one and setting a load screen looks like it did nothing. Keep the array shape for eventual multi-screen support. - Endpoints the client re-renders from must return the updated entity, not
{ error, success, value: null }— e.g.clubsPUT /club/:id/clubhouseleft the old clubhouse on screen until it answered the full details envelope. - Every subroom mutation (
rooms: create, delete,/subrooms/:sid/clone,/subrooms/:sid/accessibility,/subrooms/:sid/publish_save) answers{ success, error, value }with the whole updated ROOM — the client re-renders the room fromvalue. Notablyvalueis the room even forclone, whose product is a new SUBROOM; only the room-levelPOST /rooms/:id/clonereturns the thing it created. - The room save (
rooms:POST /subrooms/:sid/data) is the ONE exception to that shape:valueis{ room, subRoomDataSave }, anderroris NULL rather than"". ThesubRoomDataSaveis camelCase with a different field set from the PascalCaseCurrentSaveembedded in the room (no persistence/OM/UGC versions, no moderation state, no asset arrays; butunityAsset/unityAssetHash). Don't unify the two projections. - A subroom's saved scene loads from
CurrentSave.DataBlob(rooms:GET /rooms/:id), NOT the flatDataBlobon the subroom — a subroom with noCurrentSavesilently loads nothing. The key must be present (null before the first publish); read it viasubRoomDataBlob()somatch/authinstance payloads resolve it the same way. - A room save (
rooms:POST …/subrooms/:sid/data) publishes only when the body saysAutoPublish: true; otherwise it STAGES ontoStagedSubRoomDataSaveIdand leavesCurrentSavealone, so players keep loading the last published version until the owner posts…/subrooms/:sid/publish_savewithsubRoomDataSaveId=<id>. DORMS always publish: no publish step exists in the client for them. Saves live in thesubroom_savetable with globally-unique ids (a bare id has to resolve —StagedSubRoomDataSaveIdcarries no subroom context), and nothing is overwritten, so…/savesis real history andpublish_savedoubles as restore-a-save. There is noGET …/subrooms/:sid/data; only the POST (the room save) exists on that path.GET …/saves/:saveIdis the detail behind a list row, under the same gate, but in the CAMELCASE projection the room save's response uses — not the PascalCase rows the list serves. Three shapes of one save; keep them straight. - Both save reads (
rooms:…/savesand…/saves/:saveId) are auth-gated and readable by the room's CREATOR or by anyone whose livepresencerow puts them in that room — not by co-owners as such (a co-owner passes only by standing there). They list unpublished staged saves, so they aren't public; but a visitor resolves which version an instance is running from this list, so creator-only locks them out of loading the room. The grant expires with the presence row. - A room save writes ONLY to the subroom and its save row — never to the room. Everything
the body carries describes that one revision:
Descriptionis the save comment shown in…/saves, andPersistenceVersion/InventionUsagedescribe the scene just saved (the latter lives on the SUBROOM). The room's public description isPUT /rooms/:id/description's alone; copying the save comment ontoroom.Description(as this once did) silently replaces the room's description every time someone saves. - Matchmaking (
match:/matchmake/room/:roomId/:subRoomId) always serves the PUBLISHEDCurrentSaveblob, creator included. Joining a private instance, the client itself asks the owner whether to load the latest or the published version and resolves it from the/subrooms/:sid/saveslist — the matchmake call is identical either way. Don't make this server-side: it would put two people in one instance on different versions. - A balance lives in a
(CurrencyType, Platform)BUCKET and the client shows the SUM of the buckets, soPlatformis a balance's identity, not a label. This server uses exactly one bucket per currency —ALL_PLATFORMS, -2NonPurchasedNotUsableInP2P— and every surface must name it: the balance DTO (econ:GET /api/storefronts/v4/balance/:type), theBalanceTypethe storefront bodies echo, and thePlatformon everyStorefrontBalance*socket frame. Two traps, which produced two "balance doubling" bugs that both looked like the frames being additive when they are not:- Each frame SETS the bucket it names to an absolute value —
Balanceis the RESULTING TOTAL, never the change (StorefrontBalancePurchase'sDelta/BalanceAddTypeare display-only; the client logs them and storesBalanceoutright). Send a change and the balance becomes that change. Being absolute, a frame is idempotent: re-sending one, or racing aGET /balance, cannot drift the total, so the player reading the HTTP response for the same change gets a frame too. - The bucket key on the wire is
Platform. The client's property is namedBalanceTypebut carries a[DataMember]rename, and its decoder drops unknown members silently, so a frame sayingBalanceTypelands inPlatform0 (SteamPurchased) and adds a phantom balance to the real one — 10,000 tokens + a 250 reward read 20,250. Sending a real-but- different platform does the same:Platform: RecNeton a buy showed 34,100 to a player who spent 900 of 17,500, then 33,200 once the body's -900 reached the true bucket. The payload shapes are recovered from the client's own decoder inapps/notify/src/notification-payloads.ts— build frames against those interfaces (econ does) so a renamed key fails the build instead of silently vanishing on the wire.
- Each frame SETS the bucket it names to an absolute value —
- Every matchmake response (
match:/matchmake/*, refusals and the ban middleware's included) must echo the request'sCorrelationIdback ascorrelationId— the client tags each attempt with a GUID and fails with "Unable to connect to game session" if it can't match the response to the attempt. A request that names none gets the all-zeroGuid.Emptyrather than null: the client's field is a non-nullable Guid. The code is served under TWO names,result(what the client reads) anderrorCode(what this server has always sent); they are the same number and must never disagree, which is why everything answers throughmatchmakeResultrather than building the envelope by hand. - Accessibility is sent as the
RoomAccessibilityenum NAME onroomsPUT /rooms/:id/subrooms/:sid/accessibility(accessibility=Private), not the ordinal the room-level/rooms/:id/accessibilitytakes. The enum has five members (Private, Public, Unlisted, Dev_only, Dev_Unlisted); parse viaparseAccessibility, which accepts either form.