src/ws/userSet.ts
v0.3.0 · 2.6 KB
// Planning which tracked users get WebSocket slots and which fall back to REST polling.
import { isValidAddress } from './subscriptionKey.js';
export interface UserSlotPlan {
/** Users that get a WS subscription: sorted, lowercase, unique. */
ws: string[];
/** Overflow users for REST polling, in priority order. */
poll: string[];
/** Inputs that are not valid addresses (never subscribe placeholders). */
invalid: string[];
}
/**
* Splits users into WS slots and REST polling.
*
* - Input ORDER is the priority: slots go to the first users, overflow goes to REST. Slots on an IP
* are handed out first-come by the server, not by importance: a process that starts earlier can
* take every slot. Put the most important subscriptions first, then accounts with open positions,
* then the rest.
* - Addresses are deduplicated in lowercase and the WS set is sorted, so the same membership always
* yields the same set: an order that "breathes" between ticks causes needless unsubscribe/subscribe
* churn and ghost slots.
* - `capacity` should stay BELOW the server limit (documented 10 per IP) to leave room for ghost
* slots; shards filled to the brim produce a steady stream of `Cannot track` errors and REST 429s.
*/
export function planUserSlots(users: Iterable<string>, capacity: number): UserSlotPlan {
const seen = new Set<string>();
const ordered: string[] = [];
const invalid: string[] = [];
for (const raw of users) {
const user = String(raw).trim().toLowerCase();
if (!isValidAddress(user)) { invalid.push(String(raw)); continue; }
if (seen.has(user)) continue;
seen.add(user);
ordered.push(user);
}
const n = Math.max(0, Math.floor(capacity));
return { ws: ordered.slice(0, n).sort(), poll: ordered.slice(n), invalid };
}
export interface UserSetDiff {
add: string[];
remove: string[];
/** True when nothing changes: do not send anything and do not log a "degraded" line again. */
unchanged: boolean;
}
/**
* Difference between the current and the desired WS user sets (lowercase, sorted output). Apply
* removals on the connection that holds the user (same-connection unsubscribe frees the slot at once)
* and log degradation only when the set actually changes.
*/
export function diffUserSets(current: Iterable<string>, next: Iterable<string>): UserSetDiff {
const a = new Set([...current].map((u) => u.toLowerCase()));
const b = new Set([...next].map((u) => u.toLowerCase()));
const add = [...b].filter((u) => !a.has(u)).sort();
const remove = [...a].filter((u) => !b.has(u)).sort();
return { add, remove, unchanged: add.length === 0 && remove.length === 0 };
}