@colyseus/redis-driver
Version:
<div align="center"> <a href="https://github.com/colyseus/colyseus"> <img src="media/logo.svg?raw=true" width="60%" height="300" /> </a> <br> <br> <a href="https://npmjs.com/package/colyseus"> <img src="https://img.shields.io/npm/dm/coly
174 lines (139 loc) • 5.19 kB
text/typescript
import {
Redis,
Cluster,
type ClusterNode,
type ClusterOptions,
type RedisOptions
} from 'ioredis';
import {
type IRoomCache,
type MatchMakerDriver,
type SortOptions,
debugMatchMaking,
initializeRoomCache,
} from '@colyseus/core';
import { Query } from './Query.ts';
const ROOMCACHES_KEY = 'roomcaches';
export class RedisDriver implements MatchMakerDriver {
private readonly _client: Redis | Cluster;
constructor(options?: number | string | RedisOptions | ClusterNode[] | Redis | Cluster, clusterOptions?: ClusterOptions) {
this._client = (options instanceof Redis || options instanceof Cluster)
? options
: (Array.isArray(options))
? new Cluster(options, clusterOptions)
: new Redis(options as RedisOptions);
}
public async has(roomId: string) {
return await this._client.hexists(ROOMCACHES_KEY, roomId) === 1;
}
public async query(conditions: Partial<IRoomCache>, sortOptions?: SortOptions) {
const query = new Query<IRoomCache>(this.getRooms(), conditions);
if (sortOptions) {
query.sort(sortOptions);
}
return query.all();
}
public async cleanup(processId: string) {
const cachedRooms = await this.query({ processId });
debugMatchMaking("removing stale rooms by processId %s (%s rooms found)", processId, cachedRooms.length);
const itemsPerCommand = 500;
// remove rooms in batches of 500
for (let i = 0; i < cachedRooms.length; i += itemsPerCommand) {
await this._client.hdel(ROOMCACHES_KEY, ...cachedRooms.slice(i, i + itemsPerCommand).map((room) => room.roomId));
}
}
public findOne(conditions: Partial<IRoomCache>, sortOptions?: SortOptions): Promise<IRoomCache> {
if (typeof conditions.roomId !== 'undefined') {
// get room by roomId
//
// TODO: refactor driver APIs.
// the API here is legacy from MongooseDriver which made sense on versions <= 0.14.0
//
return new Promise<IRoomCache>((resolve, reject) => {
this._client.hget(ROOMCACHES_KEY, conditions.roomId).then((roomcache) => {
if (roomcache) {
resolve(initializeRoomCache(JSON.parse(roomcache)));
} else {
resolve(undefined);
}
}).catch(reject);
});
} else {
// filter list by other conditions
const query = new Query<IRoomCache>(this.getRooms(conditions['name']), conditions);
if (sortOptions) {
query.sort(sortOptions);
}
return query as any as Promise<IRoomCache>;
}
}
// gets recent room caches w/o making multiple simultaneous reads to REDIS
private _concurrentRoomCacheRequest?: Promise<Record<string, string>>;
private _roomCacheRequestByName: {[roomName: string]: Promise<IRoomCache[]>} = {};
private getRooms(roomName?: string) {
// if there's a shared request, return it
if (this._roomCacheRequestByName[roomName] !== undefined) {
return this._roomCacheRequestByName[roomName];
}
const roomCacheRequest = this._concurrentRoomCacheRequest || this._client.hgetall(ROOMCACHES_KEY);
this._concurrentRoomCacheRequest = roomCacheRequest;
this._roomCacheRequestByName[roomName] = roomCacheRequest.then((result) => {
// clear shared promises so we can read it again
this._concurrentRoomCacheRequest = undefined;
delete this._roomCacheRequestByName[roomName];
let roomcaches = Object.entries(result ?? {});
//
// micro optimization:
// filter rooms by name before parsing JSON
//
if (roomName !== undefined) {
const roomNameField = `"name":"${roomName}"`;
roomcaches = roomcaches.filter(([, roomcache]) => (roomcache as string).includes(roomNameField));
}
return roomcaches.map(
([, roomcache]) => initializeRoomCache(JSON.parse(roomcache as string))
);
});
return this._roomCacheRequestByName[roomName];
}
public async update(room: IRoomCache, operations: Partial<{ $set: Partial<IRoomCache>, $inc: Partial<IRoomCache> }>) {
if (operations.$set) {
for (const field in operations.$set) {
if (operations.$set.hasOwnProperty(field)) {
room[field] = operations.$set[field];
}
}
}
if (operations.$inc) {
for (const field in operations.$inc) {
if (operations.$inc.hasOwnProperty(field)) {
room[field] += operations.$inc[field];
}
}
}
await this._client.hset(ROOMCACHES_KEY, room.roomId, JSON.stringify(room));
return true;
}
public async persist(room: IRoomCache, _: boolean = false) {
if (!room.roomId) {
debugMatchMaking("RedisDriver: can't .persist() without a `roomId`");
return false;
}
await this._client.hset(ROOMCACHES_KEY, room.roomId, JSON.stringify(room));
return true;
}
public async remove(roomId: string) {
const result = await this._client.hdel(ROOMCACHES_KEY, roomId);
return result > 0;
}
public async shutdown() {
await this._client.quit();
}
//
// only relevant for the test-suite.
// not used during runtime.
//
public clear() {
this._client.del(ROOMCACHES_KEY);
}
}