feat: initialize sunrise with 100-player shared rooms
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { MongoClient, type Collection } from 'mongodb';
|
||||
|
||||
export type EndReason = 'disconnect' | 'shutdown' | 'interrupted';
|
||||
export interface VisitRecord {
|
||||
_id: string;
|
||||
visitorId: string;
|
||||
nickname: string;
|
||||
locationId: string;
|
||||
roomId: string;
|
||||
joinedAt: Date;
|
||||
lastSeenAt: Date;
|
||||
leftAt: Date | null;
|
||||
endReason: EndReason | null;
|
||||
serverInstanceId: string;
|
||||
}
|
||||
/** Only visit lifecycle metadata is persisted; visitorId is anonymous, not authentication. */
|
||||
export interface VisitStore {
|
||||
insert(record: VisitRecord): Promise<void>;
|
||||
heartbeat(sessionIds: string[], at: Date): Promise<void>;
|
||||
finish(sessionId: string, at: Date, reason: EndReason): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
/** Explicit test/development fixture. Never selected as a fallback. */
|
||||
export class MemoryVisitStore implements VisitStore {
|
||||
readonly records = new Map<string, VisitRecord>();
|
||||
async insert(record: VisitRecord) {
|
||||
if (this.records.has(record._id)) throw new Error('Duplicate visit');
|
||||
this.records.set(record._id, structuredClone(record));
|
||||
}
|
||||
async heartbeat(ids: string[], at: Date) {
|
||||
for (const id of ids) { const r = this.records.get(id); if (r && r.endReason === null) r.lastSeenAt = at; }
|
||||
}
|
||||
async finish(id: string, at: Date, reason: EndReason) {
|
||||
const r = this.records.get(id);
|
||||
if (r && r.endReason === null) { r.lastSeenAt = at; r.leftAt = at; r.endReason = reason; }
|
||||
}
|
||||
async close() {}
|
||||
}
|
||||
|
||||
export class MongoVisitStore implements VisitStore {
|
||||
private client: MongoClient;
|
||||
private records: Collection<VisitRecord>;
|
||||
private constructor(client: MongoClient, database: string) {
|
||||
this.client = client;
|
||||
this.records = client.db(database).collection<VisitRecord>('visitSessions');
|
||||
}
|
||||
static async connect(uri: string, database: string): Promise<MongoVisitStore> {
|
||||
const client = new MongoClient(uri, { serverSelectionTimeoutMS: 5000, connectTimeoutMS: 5000, socketTimeoutMS: 10000, writeConcern: { w: 'majority', wtimeoutMS: 5000 } });
|
||||
try {
|
||||
await client.connect();
|
||||
const store = new MongoVisitStore(client, database);
|
||||
await store.records.createIndex({ endReason: 1, lastSeenAt: 1 });
|
||||
// Never rewrite historical visits on connection. A crashed session retains
|
||||
// its lastSeenAt and unknown leftAt; this process owns only its new sessions.
|
||||
return store;
|
||||
} catch (error) { await client.close(); throw error; }
|
||||
}
|
||||
async insert(record: VisitRecord) { await this.records.insertOne(record); }
|
||||
async heartbeat(ids: string[], at: Date) {
|
||||
if (ids.length) await this.records.updateMany({ _id: { $in: ids }, endReason: null }, { $max: { lastSeenAt: at } });
|
||||
}
|
||||
async finish(id: string, at: Date, reason: EndReason) {
|
||||
await this.records.updateOne({ _id: id, endReason: null }, { $set: { leftAt: at, endReason: reason }, $max: { lastSeenAt: at } });
|
||||
}
|
||||
async close() { await this.client.close(); }
|
||||
}
|
||||
Reference in New Issue
Block a user