-
Notifications
You must be signed in to change notification settings - Fork 742
/
Copy pathmongo.js
162 lines (142 loc) · 3.82 KB
/
mongo.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
/*
* Copyright 2017 The boardgame.io Authors
*
* Use of this source code is governed by a MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
const LRU = require('lru-cache');
/**
* MongoDB connector.
*/
export class Mongo {
/**
* Creates a new Mongo connector object.
*/
constructor({ url, dbname, cacheSize, mockClient }) {
if (cacheSize === undefined) cacheSize = 1000;
if (dbname === undefined) dbname = 'bgio';
this.client = mockClient || require('mongodb').MongoClient;
this.url = url;
this.dbname = dbname;
this.cache = new LRU({ max: cacheSize });
}
/**
* Connect to the instance.
*/
async connect() {
const c = await this.client.connect(this.url, { useNewUrlParser: true });
this.db = c.db(this.dbname);
return;
}
/**
* Write the game state.
* @param {string} id - The game id.
* @param {object} store - A game state to persist.
*/
async set(id, state) {
// Don't set a value if the cache has a more recent version.
// This can occur due a race condition.
//
// For example:
//
// A --sync--> server | DB => 0 --+
// |
// A <--sync-- server | DB => 0 --+
//
// B --sync--> server | DB => 0 ----+
// |
// A --move--> server | DB <= 1 --+ |
// | |
// A <--sync-- server | DB => 1 --+ |
// |
// B <--sync-- server | DB => 0 ----+
//
const cacheValue = this.cache.get(id);
if (cacheValue && cacheValue._stateID >= state._stateID) {
return;
}
this.cache.set(id, state);
const col = this.db.collection(id);
delete state._id;
await col.insertOne(state);
return;
}
/**
* Read the game state.
* @param {string} id - The game id.
* @returns {object} - A game state, or undefined
* if no game is found with this id.
*/
async get(id) {
let cacheValue = this.cache.get(id);
if (cacheValue !== undefined) {
return cacheValue;
}
const col = this.db.collection(id);
const docs = await col
.find()
.sort({ _id: -1 })
.limit(1)
.toArray();
let oldStateID = 0;
cacheValue = this.cache.get(id);
/* istanbul ignore next line */
if (cacheValue !== undefined) {
/* istanbul ignore next line */
oldStateID = cacheValue._stateID;
}
let newStateID = -1;
if (docs.length > 0) {
newStateID = docs[0]._stateID;
}
// Update the cache, but only if the read
// value is newer than the value already in it.
// A race condition might overwrite the
// cache with an older value, so we need this.
if (newStateID >= oldStateID) {
this.cache.set(id, docs[0]);
}
return docs[0];
}
/**
* Check if a particular game exists.
* @param {string} id - The game id.
* @returns {boolean} - True if a game with this id exists.
*/
async has(id) {
const cacheValue = this.cache.get(id);
if (cacheValue !== undefined) {
return true;
}
const col = this.db.collection(id);
const docs = await col
.find()
.limit(1)
.toArray();
return docs.length > 0;
}
/**
* Remove the game state from the DB.
* @param {string} id - The game id.
*/
async remove(id) {
if (!(await this.has(id))) return;
function _dropCollection(db, id) {
return new Promise(function(ok) {
db.dropCollection(id, ok);
});
}
await _dropCollection(this.db, id);
// Update the cache
this.cache.del(id);
}
/**
* Return all keys.
* @returns {array} - Array of keys (strings)
*/
async list() {
const keys = await this.db.listCollections().toArray();
return keys.map(r => r.name);
}
}