@colyseus/core 0.17.0 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -4
- package/src/Debug.ts +37 -0
- package/src/IPC.ts +124 -0
- package/src/Logger.ts +30 -0
- package/src/MatchMaker.ts +1119 -0
- package/src/Protocol.ts +160 -0
- package/src/Room.ts +1797 -0
- package/src/Server.ts +325 -0
- package/src/Stats.ts +107 -0
- package/src/Transport.ts +207 -0
- package/src/errors/RoomExceptions.ts +141 -0
- package/src/errors/SeatReservationError.ts +5 -0
- package/src/errors/ServerError.ts +17 -0
- package/src/index.ts +81 -0
- package/src/matchmaker/Lobby.ts +68 -0
- package/src/matchmaker/LocalDriver/LocalDriver.ts +92 -0
- package/src/matchmaker/LocalDriver/Query.ts +94 -0
- package/src/matchmaker/RegisteredHandler.ts +172 -0
- package/src/matchmaker/controller.ts +64 -0
- package/src/matchmaker/driver.ts +191 -0
- package/src/presence/LocalPresence.ts +331 -0
- package/src/presence/Presence.ts +263 -0
- package/src/rooms/LobbyRoom.ts +135 -0
- package/src/rooms/RankedQueueRoom.ts +425 -0
- package/src/rooms/RelayRoom.ts +90 -0
- package/src/router/default_routes.ts +58 -0
- package/src/router/index.ts +43 -0
- package/src/serializer/NoneSerializer.ts +16 -0
- package/src/serializer/SchemaSerializer.ts +194 -0
- package/src/serializer/SchemaSerializerDebug.ts +148 -0
- package/src/serializer/Serializer.ts +9 -0
- package/src/utils/DevMode.ts +133 -0
- package/src/utils/StandardSchema.ts +20 -0
- package/src/utils/Utils.ts +169 -0
- package/src/utils/nanoevents.ts +20 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Matchmaking controller
|
|
3
|
+
* (for interoperability between different http frameworks, e.g. express, uWebSockets.js, etc)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ErrorCode } from '../Protocol.ts';
|
|
7
|
+
import { ServerError } from '../errors/ServerError.ts';
|
|
8
|
+
import * as matchMaker from '../MatchMaker.ts';
|
|
9
|
+
import type { AuthContext } from '../Transport.ts';
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
DEFAULT_CORS_HEADERS: {
|
|
13
|
+
'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, Authorization',
|
|
14
|
+
'Access-Control-Allow-Methods': 'OPTIONS, POST, GET',
|
|
15
|
+
'Access-Control-Allow-Credentials': 'true',
|
|
16
|
+
'Access-Control-Allow-Origin': '*',
|
|
17
|
+
'Access-Control-Max-Age': '2592000',
|
|
18
|
+
// ...
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
exposedMethods: ['joinOrCreate', 'create', 'join', 'joinById', 'reconnect'],
|
|
22
|
+
allowedRoomNameChars: /([a-zA-Z_\-0-9]+)/gi,
|
|
23
|
+
matchmakeRoute: 'matchmake',
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* You can manually change the default corsHeaders by overwriting the `getCorsHeaders()` method:
|
|
27
|
+
* ```
|
|
28
|
+
* import { matchMaker } from "@colyseus/core";
|
|
29
|
+
* matchMaker.controller.getCorsHeaders = function(headers) {
|
|
30
|
+
* if (headers.get('referer') !== "xxx") {
|
|
31
|
+
* }
|
|
32
|
+
*
|
|
33
|
+
* return {
|
|
34
|
+
* 'Access-Control-Allow-Origin': 'safedomain.com',
|
|
35
|
+
* }
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
getCorsHeaders(headers: Headers): { [header: string]: string } {
|
|
40
|
+
return {
|
|
41
|
+
['Access-Control-Allow-Origin']: headers.get("origin") || "*",
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
async invokeMethod(
|
|
46
|
+
method: string,
|
|
47
|
+
roomName: string,
|
|
48
|
+
clientOptions: matchMaker.ClientOptions = {},
|
|
49
|
+
authOptions?: AuthContext,
|
|
50
|
+
) {
|
|
51
|
+
if (this.exposedMethods.indexOf(method) === -1) {
|
|
52
|
+
throw new ServerError(ErrorCode.MATCHMAKE_NO_HANDLER, `invalid method "${method}"`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
return await matchMaker[method](roomName, clientOptions, authOptions);
|
|
57
|
+
|
|
58
|
+
} catch (e: any) {
|
|
59
|
+
throw new ServerError(e.code || ErrorCode.MATCHMAKE_UNHANDLED, e.message);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
}
|
|
64
|
+
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { Room } from "@colyseus/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sort options for room queries.
|
|
5
|
+
*/
|
|
6
|
+
export interface SortOptions {
|
|
7
|
+
[fieldName: string]: 1 | -1 | 'asc' | 'desc' | 'ascending' | 'descending';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Built-in room cache fields that can be used for sorting.
|
|
12
|
+
*/
|
|
13
|
+
export type IRoomCacheSortByKeys = 'clients' | 'maxClients' | 'createdAt';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Built-in room cache fields that can be used for filtering.
|
|
17
|
+
*/
|
|
18
|
+
export type IRoomCacheFilterByKeys = 'clients' | 'maxClients' | 'processId';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Extract metadata type from Room type
|
|
22
|
+
*/
|
|
23
|
+
export type ExtractMetadata<RoomType extends Room> = RoomType['~metadata'];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Generates a unique lock ID based on filter options.
|
|
27
|
+
*/
|
|
28
|
+
export function getLockId(filterOptions: any) {
|
|
29
|
+
return Object.keys(filterOptions).map((key) => `${key}:${filterOptions[key]}`).join("-");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Initialize a room cache which contains CRUD operations for room listings.
|
|
34
|
+
*
|
|
35
|
+
* @internal
|
|
36
|
+
* @param initialValues - Predefined room properties.
|
|
37
|
+
* @returns RoomData - New room cache.
|
|
38
|
+
*/
|
|
39
|
+
export function initializeRoomCache(initialValues: Partial<IRoomCache> = {}): IRoomCache {
|
|
40
|
+
return {
|
|
41
|
+
clients: 0,
|
|
42
|
+
maxClients: Infinity,
|
|
43
|
+
locked: false,
|
|
44
|
+
private: false,
|
|
45
|
+
metadata: undefined,
|
|
46
|
+
createdAt: (initialValues && initialValues.createdAt) ? new Date(initialValues.createdAt) : new Date(),
|
|
47
|
+
unlisted: false,
|
|
48
|
+
...initialValues,
|
|
49
|
+
} as IRoomCache;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface IRoomCache<Metadata = any> {
|
|
53
|
+
/**
|
|
54
|
+
* Room name.
|
|
55
|
+
*/
|
|
56
|
+
name: string;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Unique identifier for the room.
|
|
60
|
+
*/
|
|
61
|
+
roomId: string;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Process id where the room is running.
|
|
65
|
+
*/
|
|
66
|
+
processId: string;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Number of clients connected to this room.
|
|
70
|
+
*/
|
|
71
|
+
clients: number;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Maximum number of clients allowed to join the room.
|
|
75
|
+
*/
|
|
76
|
+
maxClients: number;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Indicates if the room is locked (i.e. join requests are rejected).
|
|
80
|
+
*/
|
|
81
|
+
locked?: boolean;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Indicates if the room is private
|
|
85
|
+
* Private rooms can't be joined via `join()` or `joinOrCreate()`.
|
|
86
|
+
*/
|
|
87
|
+
private?: boolean;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Public address of the server.
|
|
91
|
+
*/
|
|
92
|
+
publicAddress?: string;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Do not show this room in lobby listing.
|
|
96
|
+
*/
|
|
97
|
+
unlisted?: boolean;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Metadata associated with the room.
|
|
101
|
+
*/
|
|
102
|
+
metadata?: Metadata;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* When the room was created.
|
|
106
|
+
*/
|
|
107
|
+
createdAt?: Date;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface MatchMakerDriver {
|
|
111
|
+
/**
|
|
112
|
+
* Check if a room exists in room cache.
|
|
113
|
+
*
|
|
114
|
+
* @param roomId - The room id.
|
|
115
|
+
*
|
|
116
|
+
* @returns Promise<boolean> | boolean - A promise or a boolean value indicating if the room exists.
|
|
117
|
+
*/
|
|
118
|
+
has(roomId: string): Promise<boolean> | boolean;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Query rooms in room cache for given conditions.
|
|
122
|
+
*
|
|
123
|
+
* @param conditions - Filtering conditions.
|
|
124
|
+
*
|
|
125
|
+
* @returns Promise<IRoomCache[]> | IRoomCache[] - A promise or an object contaning room metadata list.
|
|
126
|
+
*/
|
|
127
|
+
query<T extends Room = any>(
|
|
128
|
+
conditions: Partial<IRoomCache & ExtractMetadata<T>>,
|
|
129
|
+
sortOptions?: SortOptions
|
|
130
|
+
): Promise<Array<IRoomCache<ExtractMetadata<T>>>> | Array<IRoomCache<ExtractMetadata<T>>>;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Clean up rooms in room cache by process id.
|
|
134
|
+
* @param processId - The process id.
|
|
135
|
+
*/
|
|
136
|
+
cleanup?(processId: string): Promise<void>;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Query for a room in room cache for given conditions.
|
|
140
|
+
*
|
|
141
|
+
* @param conditions - Filtering conditions.
|
|
142
|
+
*
|
|
143
|
+
* @returns `IRoomCache` - An object contaning filtered room metadata.
|
|
144
|
+
*/
|
|
145
|
+
findOne<T extends Room = any>(
|
|
146
|
+
conditions: Partial<IRoomCache & ExtractMetadata<T>>,
|
|
147
|
+
sortOptions?: SortOptions
|
|
148
|
+
): Promise<IRoomCache<ExtractMetadata<T>>>;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Remove a room from room cache.
|
|
152
|
+
*
|
|
153
|
+
* @param roomId - The room id.
|
|
154
|
+
*/
|
|
155
|
+
remove(roomId: string): Promise<boolean> | boolean;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Update a room in room cache.
|
|
159
|
+
*
|
|
160
|
+
* @param IRoomCache - The room to update.
|
|
161
|
+
* @param operations - The operations to update the room.
|
|
162
|
+
*/
|
|
163
|
+
update(
|
|
164
|
+
room: IRoomCache,
|
|
165
|
+
operations: Partial<{ $set: Partial<IRoomCache>, $inc: Partial<IRoomCache> }>
|
|
166
|
+
): Promise<boolean> | boolean;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Persist a room in room cache.
|
|
170
|
+
*
|
|
171
|
+
* @param room - The room to persist.
|
|
172
|
+
* @param create - If true, create a new record. If false (default), update existing record.
|
|
173
|
+
*/
|
|
174
|
+
persist(room: IRoomCache, create?: boolean): Promise<boolean> | boolean;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Empty the room cache. Used for testing purposes only.
|
|
178
|
+
* @internal Do not call this method yourself.
|
|
179
|
+
*/
|
|
180
|
+
clear(): void;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Boot the room cache medium (if available).
|
|
184
|
+
*/
|
|
185
|
+
boot?(): Promise<void>;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Dispose the connection of the room cache medium.
|
|
189
|
+
*/
|
|
190
|
+
shutdown(): void;
|
|
191
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
import { spliceOne } from '../utils/Utils.ts';
|
|
4
|
+
import type { Presence } from './Presence.ts';
|
|
5
|
+
|
|
6
|
+
import { hasDevModeCache, isDevMode, getDevModeCache, writeDevModeCache } from '../utils/DevMode.ts';
|
|
7
|
+
|
|
8
|
+
type Callback = (...args: any[]) => void;
|
|
9
|
+
|
|
10
|
+
export class LocalPresence implements Presence {
|
|
11
|
+
public subscriptions = new EventEmitter();
|
|
12
|
+
|
|
13
|
+
public data: {[roomName: string]: string[]} = {};
|
|
14
|
+
public hash: {[roomName: string]: {[key: string]: string}} = {};
|
|
15
|
+
|
|
16
|
+
public keys: {[name: string]: string | number} = {};
|
|
17
|
+
|
|
18
|
+
private timeouts: {[name: string]: NodeJS.Timeout} = {};
|
|
19
|
+
|
|
20
|
+
constructor() {
|
|
21
|
+
//
|
|
22
|
+
// reload from local cache on devMode
|
|
23
|
+
//
|
|
24
|
+
if (
|
|
25
|
+
isDevMode &&
|
|
26
|
+
hasDevModeCache()
|
|
27
|
+
) {
|
|
28
|
+
const cache = getDevModeCache();
|
|
29
|
+
if (cache.data) { this.data = cache.data; }
|
|
30
|
+
if (cache.hash) { this.hash = cache.hash; }
|
|
31
|
+
if (cache.keys) { this.keys = cache.keys; }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public subscribe(topic: string, callback: (...args: any[]) => void) {
|
|
36
|
+
this.subscriptions.on(topic, callback);
|
|
37
|
+
return Promise.resolve(this);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public unsubscribe(topic: string, callback?: Callback) {
|
|
41
|
+
if (callback) {
|
|
42
|
+
this.subscriptions.removeListener(topic, callback);
|
|
43
|
+
|
|
44
|
+
} else {
|
|
45
|
+
this.subscriptions.removeAllListeners(topic);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public publish(topic: string, data: any) {
|
|
52
|
+
this.subscriptions.emit(topic, data);
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public async channels (pattern?: string) {
|
|
57
|
+
let eventNames = this.subscriptions.eventNames() as string[];
|
|
58
|
+
if (pattern) {
|
|
59
|
+
//
|
|
60
|
+
// This is a limited glob pattern to regexp implementation.
|
|
61
|
+
// If needed, we can use a full implementation like picomatch: https://github.com/micromatch/picomatch/
|
|
62
|
+
//
|
|
63
|
+
const regexp = new RegExp(
|
|
64
|
+
pattern.
|
|
65
|
+
replaceAll(".", "\\.").
|
|
66
|
+
replaceAll("$", "\\$").
|
|
67
|
+
replaceAll("*", ".*").
|
|
68
|
+
replaceAll("?", "."),
|
|
69
|
+
"i"
|
|
70
|
+
);
|
|
71
|
+
eventNames = eventNames.filter((eventName) => regexp.test(eventName));
|
|
72
|
+
}
|
|
73
|
+
return eventNames;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public async exists(key: string): Promise<boolean> {
|
|
77
|
+
return (
|
|
78
|
+
this.keys[key] !== undefined ||
|
|
79
|
+
this.data[key] !== undefined ||
|
|
80
|
+
this.hash[key] !== undefined
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public set(key: string, value: string) {
|
|
85
|
+
this.keys[key] = value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public setex(key: string, value: string, seconds: number) {
|
|
89
|
+
this.keys[key] = value;
|
|
90
|
+
this.expire(key, seconds);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public expire(key: string, seconds: number) {
|
|
94
|
+
// ensure previous timeout is clear before setting another one.
|
|
95
|
+
if (this.timeouts[key]) {
|
|
96
|
+
clearTimeout(this.timeouts[key]);
|
|
97
|
+
}
|
|
98
|
+
this.timeouts[key] = setTimeout(() => {
|
|
99
|
+
delete this.keys[key];
|
|
100
|
+
delete this.timeouts[key];
|
|
101
|
+
}, seconds * 1000);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public get(key: string) {
|
|
105
|
+
return this.keys[key];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public del(key: string) {
|
|
109
|
+
delete this.keys[key];
|
|
110
|
+
delete this.data[key];
|
|
111
|
+
delete this.hash[key];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public sadd(key: string, value: any) {
|
|
115
|
+
if (!this.data[key]) {
|
|
116
|
+
this.data[key] = [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (this.data[key].indexOf(value) === -1) {
|
|
120
|
+
this.data[key].push(value);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
public async smembers(key: string): Promise<string[]> {
|
|
125
|
+
return this.data[key] || [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
public async sismember(key: string, field: string) {
|
|
129
|
+
return this.data[key] && this.data[key].includes(field) ? 1 : 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public srem(key: string, value: any) {
|
|
133
|
+
if (this.data[key]) {
|
|
134
|
+
spliceOne(this.data[key], this.data[key].indexOf(value));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public scard(key: string) {
|
|
139
|
+
return (this.data[key] || []).length;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public async sinter(...keys: string[]) {
|
|
143
|
+
const intersection: {[value: string]: number} = {};
|
|
144
|
+
|
|
145
|
+
for (let i = 0, l = keys.length; i < l; i++) {
|
|
146
|
+
(await this.smembers(keys[i])).forEach((member) => {
|
|
147
|
+
if (!intersection[member]) {
|
|
148
|
+
intersection[member] = 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
intersection[member]++;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return Object.keys(intersection).reduce((prev, curr) => {
|
|
156
|
+
if (intersection[curr] > 1) {
|
|
157
|
+
prev.push(curr);
|
|
158
|
+
}
|
|
159
|
+
return prev;
|
|
160
|
+
}, []);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
public hset(key: string, field: string, value: string) {
|
|
164
|
+
if (!this.hash[key]) { this.hash[key] = {}; }
|
|
165
|
+
this.hash[key][field] = value;
|
|
166
|
+
return Promise.resolve(true);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
public hincrby(key: string, field: string, incrBy: number) {
|
|
170
|
+
if (!this.hash[key]) { this.hash[key] = {}; }
|
|
171
|
+
let value = Number(this.hash[key][field] || '0');
|
|
172
|
+
value += incrBy;
|
|
173
|
+
this.hash[key][field] = value.toString();
|
|
174
|
+
return Promise.resolve(value);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
public hincrbyex(key: string, field: string, incrBy: number, expireInSeconds: number) {
|
|
178
|
+
if (!this.hash[key]) { this.hash[key] = {}; }
|
|
179
|
+
let value = Number(this.hash[key][field] || '0');
|
|
180
|
+
value += incrBy;
|
|
181
|
+
this.hash[key][field] = value.toString();
|
|
182
|
+
|
|
183
|
+
//
|
|
184
|
+
// FIXME: delete only hash[key][field]
|
|
185
|
+
// (we can't use "HEXPIRE" in Redis because it's only available since Redis version 7.4.0+)
|
|
186
|
+
//
|
|
187
|
+
if (this.timeouts[key]) {
|
|
188
|
+
clearTimeout(this.timeouts[key]);
|
|
189
|
+
}
|
|
190
|
+
this.timeouts[key] = setTimeout(() => {
|
|
191
|
+
delete this.hash[key];
|
|
192
|
+
delete this.timeouts[key];
|
|
193
|
+
}, expireInSeconds * 1000);
|
|
194
|
+
|
|
195
|
+
return Promise.resolve(value);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
public async hget(key: string, field: string) {
|
|
199
|
+
return (typeof(this.hash[key]) === 'object')
|
|
200
|
+
? this.hash[key][field] ?? null
|
|
201
|
+
: null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
public async hgetall(key: string) {
|
|
205
|
+
return this.hash[key] || {};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
public hdel(key: string, field: any) {
|
|
209
|
+
const success = this.hash?.[key]?.[field] !== undefined;
|
|
210
|
+
if (success) {
|
|
211
|
+
delete this.hash[key][field];
|
|
212
|
+
}
|
|
213
|
+
return Promise.resolve(success);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
public async hlen(key: string) {
|
|
217
|
+
return this.hash[key] && Object.keys(this.hash[key]).length || 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
public async incr(key: string) {
|
|
221
|
+
if (!this.keys[key]) {
|
|
222
|
+
this.keys[key] = 0;
|
|
223
|
+
}
|
|
224
|
+
(this.keys[key] as number)++;
|
|
225
|
+
return Promise.resolve(this.keys[key] as number);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
public async decr(key: string) {
|
|
229
|
+
if (!this.keys[key]) {
|
|
230
|
+
this.keys[key] = 0;
|
|
231
|
+
}
|
|
232
|
+
(this.keys[key] as number)--;
|
|
233
|
+
return Promise.resolve(this.keys[key] as number);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
public llen(key: string) {
|
|
237
|
+
return Promise.resolve((this.data[key] && this.data[key].length) || 0);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
public rpush(key: string, ...values: string[]): Promise<number> {
|
|
241
|
+
if (!this.data[key]) { this.data[key] = []; }
|
|
242
|
+
|
|
243
|
+
let lastLength: number = 0;
|
|
244
|
+
|
|
245
|
+
values.forEach(value => {
|
|
246
|
+
lastLength = this.data[key].push(value);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return Promise.resolve(lastLength);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
public lpush(key: string, ...values: string[]): Promise<number> {
|
|
253
|
+
if (!this.data[key]) { this.data[key] = []; }
|
|
254
|
+
|
|
255
|
+
let lastLength: number = 0;
|
|
256
|
+
|
|
257
|
+
values.forEach(value => {
|
|
258
|
+
lastLength = this.data[key].unshift(value);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
return Promise.resolve(lastLength);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
public lpop(key: string): Promise<string> {
|
|
265
|
+
return Promise.resolve(Array.isArray(this.data[key])
|
|
266
|
+
? this.data[key].shift()
|
|
267
|
+
: null);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
public rpop(key: string): Promise<string | null> {
|
|
271
|
+
return Promise.resolve(this.data[key].pop());
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
public brpop(...args: [...keys: string[], timeoutInSeconds: number]): Promise<[string, string] | null> {
|
|
275
|
+
const keys = args.slice(0, -1) as string[];
|
|
276
|
+
const timeoutInSeconds = args[args.length - 1] as number;
|
|
277
|
+
|
|
278
|
+
const getFirstPopulated = (): [string, string] | null => {
|
|
279
|
+
const keyWithValue = keys.find(key => this.data[key] && this.data[key].length > 0);
|
|
280
|
+
if (keyWithValue) {
|
|
281
|
+
return [keyWithValue, this.data[keyWithValue].pop()];
|
|
282
|
+
} else {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const firstPopulated = getFirstPopulated();
|
|
288
|
+
|
|
289
|
+
if (firstPopulated) {
|
|
290
|
+
// return first populated key + item
|
|
291
|
+
return Promise.resolve(firstPopulated);
|
|
292
|
+
|
|
293
|
+
} else {
|
|
294
|
+
// 8 retries per second
|
|
295
|
+
const maxRetries = timeoutInSeconds * 8;
|
|
296
|
+
|
|
297
|
+
let tries = 0;
|
|
298
|
+
return new Promise((resolve) => {
|
|
299
|
+
const interval = setInterval(() => {
|
|
300
|
+
tries++;
|
|
301
|
+
|
|
302
|
+
const firstPopulated = getFirstPopulated();
|
|
303
|
+
if (firstPopulated) {
|
|
304
|
+
clearInterval(interval);
|
|
305
|
+
return resolve(firstPopulated);
|
|
306
|
+
|
|
307
|
+
} else if (tries >= maxRetries) {
|
|
308
|
+
clearInterval(interval);
|
|
309
|
+
return resolve(null);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
}, (timeoutInSeconds * 1000) / maxRetries);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
public setMaxListeners(number: number) {
|
|
318
|
+
this.subscriptions.setMaxListeners(number);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
public shutdown() {
|
|
322
|
+
if (isDevMode) {
|
|
323
|
+
writeDevModeCache({
|
|
324
|
+
data: this.data,
|
|
325
|
+
hash: this.hash,
|
|
326
|
+
keys: this.keys
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
}
|