mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-25 23:18:51 -07:00
[msc] make playersettings more efficient
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
"test": "run-vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/domain": "workspace:*",
|
||||
"@repo/hono-helpers": "workspace:*",
|
||||
"@repo/jwt": "workspace:*",
|
||||
"@standard-community/standard-json": "0.3.5",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Hono } from 'hono'
|
||||
import { describeRoute, openAPIRouteHandler } from 'hono-openapi'
|
||||
import { useWorkersLogger } from 'workers-tagged-logger'
|
||||
|
||||
import { mergePlayerSettings } from '@repo/domain'
|
||||
import { withCleanSpec, withNotFound, withOnError } from '@repo/hono-helpers'
|
||||
import { validateAndGetAccountId } from '@repo/jwt'
|
||||
|
||||
@@ -181,7 +182,9 @@ const app = new Hono<App>()
|
||||
|
||||
// Upsert player settings into KV, keyed by the authenticated player id.
|
||||
// A full replace would overwrite the player's entire set; we merge so individual key PUTs
|
||||
// (e.g. `key=PlayerSessionCount&value=1`) don't wipe the rest.
|
||||
// (e.g. `key=PlayerSessionCount&value=1`) don't wipe the rest. The client re-posts values
|
||||
// it already has at every login and menu change, so a PUT that changes nothing is a
|
||||
// read only — KV writes are what cost money here.
|
||||
.put(
|
||||
'/playersettings',
|
||||
describeRoute({
|
||||
@@ -192,7 +195,8 @@ const app = new Hono<App>()
|
||||
'key PUT (`key=PlayerSessionCount&value=1`, which is what the client sends) leaves the',
|
||||
'player’s other settings alone. A JSON body is also accepted, as one object or an',
|
||||
'array, in either `key`/`value` or `Key`/`Value` casing; entries with an empty key are',
|
||||
'dropped. An unparseable or empty body is a no-op 200, not a 400. Empty body on success.',
|
||||
'dropped. An unparseable or empty body is a no-op 200, not a 400, and so is a PUT whose',
|
||||
'values are already stored — nothing is written to KV. Empty body on success.',
|
||||
].join(' '),
|
||||
security: AUTHED,
|
||||
requestBody: formOrJson(SettingFormWrite, SettingJsonWrite, 'The setting(s) to write'),
|
||||
@@ -208,15 +212,10 @@ const app = new Hono<App>()
|
||||
const incoming = await parseSettings(c)
|
||||
if (incoming.length === 0) return c.body(null, 200)
|
||||
|
||||
const kvKey = `player:${id}`
|
||||
const existing = await c.env.RECFLARE_PLAYER_SETTINGS.get<Record<string, string>>(
|
||||
kvKey,
|
||||
'json'
|
||||
)
|
||||
const merged: Record<string, string> = { ...existing }
|
||||
for (const { key, value } of incoming) merged[key] = value
|
||||
const patch: Record<string, string> = {}
|
||||
for (const { key, value } of incoming) patch[key] = value
|
||||
|
||||
await c.env.RECFLARE_PLAYER_SETTINGS.put(kvKey, JSON.stringify(merged))
|
||||
await mergePlayerSettings(c.env.RECFLARE_PLAYER_SETTINGS, id, patch)
|
||||
return c.body(null, 200)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -140,6 +140,30 @@ describe('playersettings endpoints', () => {
|
||||
expect(stored).toEqual({ A: '1', B: '2' })
|
||||
})
|
||||
|
||||
// KV writes are the cost here, and the client re-posts settings it already has at every
|
||||
// login. The raw value is seeded with whitespace the worker's own JSON.stringify would
|
||||
// never produce: if it survives the PUT, nothing was written.
|
||||
it('PUT /playersettings does not write KV when the value is already stored', async () => {
|
||||
const padded = '{ "PlayerSessionCount": "5", "Recroom.OOBE": "77" }'
|
||||
await env.RECFLARE_PLAYER_SETTINGS.put('player:10', padded)
|
||||
|
||||
const same = await SELF.fetch(
|
||||
`${ORIGIN}/playersettings`,
|
||||
putForm({ key: 'PlayerSessionCount', value: '5' }, await bearer('10'))
|
||||
)
|
||||
expect(same.status).toBe(200)
|
||||
expect(await env.RECFLARE_PLAYER_SETTINGS.get('player:10', 'text')).toBe(padded)
|
||||
|
||||
// A real change is still written — and compacted, which is how we know.
|
||||
await SELF.fetch(
|
||||
`${ORIGIN}/playersettings`,
|
||||
putForm({ key: 'PlayerSessionCount', value: '6' }, await bearer('10'))
|
||||
)
|
||||
expect(await env.RECFLARE_PLAYER_SETTINGS.get('player:10', 'text')).toBe(
|
||||
JSON.stringify({ PlayerSessionCount: '6', 'Recroom.OOBE': '77' })
|
||||
)
|
||||
})
|
||||
|
||||
it('PUT /playersettings 200s with no parseable settings', async () => {
|
||||
const res = await SELF.fetch(`${ORIGIN}/playersettings`, putForm({}, await bearer('9')))
|
||||
expect(res.status).toBe(200)
|
||||
|
||||
Reference in New Issue
Block a user