[www] token drop performance improvement

This commit is contained in:
Devin Zuczek
2026-10-05 13:10:05 -04:00
parent 8ea72fcdc5
commit 0900bcac25
9 changed files with 645 additions and 78 deletions
+49
View File
@@ -99,6 +99,13 @@ export interface HubState {
pending: Array<{ playerId: number; count: number; latest: string }>
}
/** One entry of a {@link NotificationsHub.notifyPlayers} call: a `notifyPlayer`'s arguments. */
export interface PlayerNotification {
playerId: number
notificationType: string | number
data?: Record<string, unknown>
}
/**
* How many queued notifications one offline player may accumulate. Past this the OLDEST
* are dropped, newest kept.
@@ -464,6 +471,48 @@ export class NotificationsHub extends DurableObject<Env> {
return { delivered, queued: false }
}
/**
* {@link notifyPlayer} for MANY notifications in ONE RPC, each with its own player and
* payload, delivered (or queued, when that player is offline) in the order given. The
* server-wide token drop pays a few hundred players at once, and each needs two frames of
* their own — a balance that differs per player, then a box whose id does — so sent
* through `notifyPlayer` it was two round trips per player, in series, from a request
* already short of its subrequest budget. One call carries the lot.
*
* `notifyPlayersEphemeral` is the other bulk send: one identical payload to many, never
* queued. This one keeps `notifyPlayer`'s durability, player by player. The "nobody to
* deliver to" warning is logged ONCE for the whole call, naming the players, rather than
* once per frame: a drop reaches everyone with an unexpired presence row, and a good
* share of them are between sessions at any moment.
*/
async notifyPlayers(
notifications: readonly PlayerNotification[]
): Promise<{ delivered: number; queued: number }> {
let delivered = 0
let queued = 0
const unreachable = new Set<number>()
for (const { playerId, notificationType, data } of notifications) {
const payload = this.buildNotificationPayload(notificationType, data)
const sent = this.deliverToPlayer(playerId, payload)
if (sent === 0) {
this.queuePending(playerId, payload)
queued++
unreachable.add(playerId)
} else {
delivered += sent
}
}
if (queued > 0) {
console.warn('hub: notifications queued, nobody to deliver to', {
playerIds: [...unreachable],
queued,
notifications: notifications.length,
liveSockets: this.ctx.getWebSockets().length,
})
}
return { delivered, queued }
}
/**
* Queue a notification for a player who wasn't reachable, keeping the queue to
* {@link MAX_PENDING_PER_PLAYER}. Trimming is oldest-first and happens on the write, so