Presence: count an account as soon as it's linked; accept an empty outfit
CI / test (push) Successful in 36s

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-10-29 05:07:28 -07:00
1 parent 7a06ba49c3
commit 339ba8e2d6
3 files changed
+45 -5

No files matched your search

+34 -4
View File
@@ -11,7 +11,8 @@ export type Item = { id: string; name: string };
export type Online = { userId: string; imvuId: number; since: Date; outfit: Item[] };
type Session = { userId: string; imvuId: number; since: Date; outfit: Item[]; close: () => void };
// requested: the IMVU account the client said it's using, even before it's linked (it counts once it is).
type Session = { userId: string; imvuId: number; requested: number; since: Date; outfit: Item[]; close: () => void };
export type Owners = (imvuId: number) => Promise<string | undefined>;
@@ -48,7 +49,7 @@ export class Realtime {
}
open(id: string, userId: string, close: () => void) {
this.sessions.set(id, { userId, imvuId: 0, since: new Date(), outfit: [], close });
this.sessions.set(id, { userId, imvuId: 0, requested: 0, since: new Date(), outfit: [], close });
}
close(id: string) {
@@ -57,6 +58,32 @@ export class Realtime {
if (s?.imvuId) this.changed();
}
/** A person just linked an IMVU account: their clients already using it count now. */
linked(userId: string, imvuId: number) {
let changed = false;
for (const s of this.sessions.values()) {
if (s.userId === userId && s.requested === imvuId && s.imvuId !== imvuId) {
s.imvuId = imvuId;
s.since = new Date();
changed = true;
}
}
if (changed) this.changed();
}
/** An IMVU account was unlinked (or moved): it no longer counts for that person's clients. */
unlinked(userId: string, imvuId: number) {
let changed = false;
for (const s of this.sessions.values()) {
if (s.userId === userId && s.imvuId === imvuId) {
s.imvuId = 0;
s.outfit = [];
changed = true;
}
}
if (changed) this.changed();
}
/** Closes a person's connections (banned, or signed out of their computers). */
disconnect(userId: string) {
for (const s of this.sessions.values()) if (s.userId === userId) s.close();
@@ -70,6 +97,7 @@ export class Realtime {
if (!m || typeof m !== "object") return { type: "error", message: "not a message" };
if (m.type === "session") {
const imvuId = Number(m.imvu_id);
s.requested = Number.isSafeInteger(imvuId) && imvuId > 0 ? imvuId : 0;
const owner = Number.isSafeInteger(imvuId) && imvuId > 0 ? await this.owner(imvuId) : undefined;
if (owner !== s.userId) {
// Not this person's account: no presence, no outfit for it.
@@ -85,8 +113,10 @@ export class Realtime {
return;
}
if (m.type === "outfit") {
if (!Array.isArray(m.items) || m.items.length > 64) return { type: "error", message: "too many items" };
s.outfit = m.items
const items = m.items ?? [];
if (!Array.isArray(items)) return { type: "error", message: "items must be a list" };
if (items.length > 64) return { type: "error", message: "too many items" };
s.outfit = items
.filter((it): it is Item => typeof it?.id === "string" && typeof it?.name === "string")
.filter((it) => it.id && it.id.length <= 128 && it.name.length <= 128)
.map((it) => ({ id: it.id, name: it.name }));