@nekodb/client 1.0.0
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/LICENSE +21 -0
- package/README.md +557 -0
- package/auth/index.js +7 -0
- package/auth/key-derivation.js +7 -0
- package/auth/manager.js +45 -0
- package/connection/index.js +8 -0
- package/connection/manager.js +135 -0
- package/connection/status.js +17 -0
- package/errors/base.js +19 -0
- package/errors/classifier.js +15 -0
- package/errors/database.js +63 -0
- package/errors/index.js +16 -0
- package/errors/network.js +23 -0
- package/events/constants.js +10 -0
- package/events/emitter.js +106 -0
- package/events/index.js +9 -0
- package/events/subscriber.js +21 -0
- package/helpers/collection.js +151 -0
- package/helpers/document.js +49 -0
- package/helpers/index.js +9 -0
- package/helpers/pagination.js +59 -0
- package/index.js +392 -0
- package/middleware/batch.js +49 -0
- package/middleware/buffer.js +33 -0
- package/middleware/cache.js +55 -0
- package/middleware/index.js +15 -0
- package/middleware/logger.js +35 -0
- package/middleware/ping.js +29 -0
- package/middleware/reconnect.js +36 -0
- package/package.json +46 -0
- package/query/builder.js +181 -0
- package/query/field.js +21 -0
- package/query/index.js +9 -0
- package/query/operators.js +19 -0
- package/schema/index.js +7 -0
- package/schema/schema.js +70 -0
- package/schema/types.js +31 -0
- package/schema/validator.js +60 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
class CursorNavigator {
|
|
2
|
+
#db;
|
|
3
|
+
#collection;
|
|
4
|
+
#query;
|
|
5
|
+
#limit;
|
|
6
|
+
#nextCursor;
|
|
7
|
+
#prevCursor;
|
|
8
|
+
|
|
9
|
+
constructor(db, collectionName, options = {}) {
|
|
10
|
+
this.#db = db;
|
|
11
|
+
this.#collection = collectionName;
|
|
12
|
+
this.#query = options.query || null;
|
|
13
|
+
this.#limit = options.limit || 20;
|
|
14
|
+
this.#nextCursor = '';
|
|
15
|
+
this.#prevCursor = '';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async next() {
|
|
19
|
+
const result = await this.#db.listPaginated(this.#collection, {
|
|
20
|
+
limit: this.#limit,
|
|
21
|
+
cursor: this.#nextCursor,
|
|
22
|
+
filter: this.#query,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (result?.page_info) {
|
|
26
|
+
this.#nextCursor = result.page_info.next_cursor || '';
|
|
27
|
+
this.#prevCursor = result.page_info.prev_cursor || '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return result?.data || [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async prev() {
|
|
34
|
+
if (!this.#prevCursor) return [];
|
|
35
|
+
|
|
36
|
+
const result = await this.#db.listPaginated(this.#collection, {
|
|
37
|
+
limit: this.#limit,
|
|
38
|
+
cursor: this.#prevCursor,
|
|
39
|
+
filter: this.#query,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (result?.page_info) {
|
|
43
|
+
this.#nextCursor = result.page_info.next_cursor || '';
|
|
44
|
+
this.#prevCursor = result.page_info.prev_cursor || '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return result?.data || [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get hasNext() {
|
|
51
|
+
return !!this.#nextCursor;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get hasPrev() {
|
|
55
|
+
return !!this.#prevCursor;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { CursorNavigator };
|
package/index.js
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
const WebSocket = require('ws');
|
|
2
|
+
const { AuthManager } = require('./auth');
|
|
3
|
+
const { BufferManager, PingManager, ReconnectManager, ResponseCache, BatchCollector, Logger } = require('./middleware');
|
|
4
|
+
const { QueryBuilder } = require('./query');
|
|
5
|
+
const { Schema } = require('./schema');
|
|
6
|
+
const { EventBus } = require('./events');
|
|
7
|
+
const { CollectionHelper } = require('./helpers');
|
|
8
|
+
const { ConnectionManager } = require('./connection');
|
|
9
|
+
const errors = require('./errors');
|
|
10
|
+
|
|
11
|
+
class NekoDB {
|
|
12
|
+
#host;
|
|
13
|
+
#auth;
|
|
14
|
+
#ws;
|
|
15
|
+
#buffer;
|
|
16
|
+
#ping;
|
|
17
|
+
#reconnect;
|
|
18
|
+
#cache;
|
|
19
|
+
#batch;
|
|
20
|
+
#log;
|
|
21
|
+
#connectedPromise;
|
|
22
|
+
#resolveConnected;
|
|
23
|
+
#rejectConnected;
|
|
24
|
+
#shouldReconnect;
|
|
25
|
+
#connected;
|
|
26
|
+
#events;
|
|
27
|
+
|
|
28
|
+
constructor({ host, username, password, key, cache, logging }) {
|
|
29
|
+
this.#host = host;
|
|
30
|
+
this.#auth = new AuthManager(username, password, key);
|
|
31
|
+
this.#ws = null;
|
|
32
|
+
this.#buffer = new BufferManager();
|
|
33
|
+
this.#reconnect = new ReconnectManager();
|
|
34
|
+
this.#cache = new ResponseCache(cache?.ttl, cache?.maxSize);
|
|
35
|
+
this.#log = new Logger('[NekoDB]', logging || 'none');
|
|
36
|
+
this.#batch = new BatchCollector((ops) => this.bulkExecute(ops));
|
|
37
|
+
this.#connectedPromise = null;
|
|
38
|
+
this.#resolveConnected = null;
|
|
39
|
+
this.#rejectConnected = null;
|
|
40
|
+
this.#shouldReconnect = true;
|
|
41
|
+
this.#connected = false;
|
|
42
|
+
this.#events = {};
|
|
43
|
+
|
|
44
|
+
this.#init();
|
|
45
|
+
|
|
46
|
+
process.once('SIGINT', () => { this.close(); process.exit(); });
|
|
47
|
+
process.once('SIGTERM', () => { this.close(); process.exit(); });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#init() {
|
|
51
|
+
try {
|
|
52
|
+
this.#ws = new WebSocket(`ws://${this.#host}`);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
this.#log.error('Connection failed:', err.message);
|
|
55
|
+
if (this.#rejectConnected) this.#rejectConnected(err);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.#connectedPromise = new Promise((resolve, reject) => {
|
|
60
|
+
this.#resolveConnected = resolve;
|
|
61
|
+
this.#rejectConnected = reject;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
this.#ws.on('open', () => {
|
|
65
|
+
this.#connected = true;
|
|
66
|
+
this.#reconnect.reset();
|
|
67
|
+
|
|
68
|
+
this.#ping = new PingManager(this.#ws);
|
|
69
|
+
this.#ping.start();
|
|
70
|
+
|
|
71
|
+
this.#log.info('Connected to', this.#host);
|
|
72
|
+
this.#resolveConnected(true);
|
|
73
|
+
this.#emit('connected');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
this.#ws.on('message', (msg) => {
|
|
77
|
+
this.#buffer.append(msg);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
this.#ws.on('error', (err) => {
|
|
81
|
+
this.#log.error('WebSocket error:', err.message);
|
|
82
|
+
this.#emit('error', err);
|
|
83
|
+
if (!this.#connected && this.#rejectConnected) {
|
|
84
|
+
this.#rejectConnected(err);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
this.#ws.on('close', () => {
|
|
89
|
+
this.#connected = false;
|
|
90
|
+
if (this.#ping) this.#ping.stop();
|
|
91
|
+
this.#ping = null;
|
|
92
|
+
this.#ws = null;
|
|
93
|
+
this.#log.warn('Disconnected');
|
|
94
|
+
this.#emit('disconnected');
|
|
95
|
+
|
|
96
|
+
if (this.#shouldReconnect) {
|
|
97
|
+
this.#reconnect.schedule(() => {
|
|
98
|
+
this.#log.info('Reconnecting...');
|
|
99
|
+
this.#init();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async #ensureConnected() {
|
|
106
|
+
if (this.#connected && this.#ws?.readyState === WebSocket.OPEN) return;
|
|
107
|
+
await this.#connectedPromise;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async #send(payload) {
|
|
111
|
+
await this.#ensureConnected();
|
|
112
|
+
|
|
113
|
+
this.#buffer.clear();
|
|
114
|
+
this.#ws.send(Buffer.from(JSON.stringify(payload)));
|
|
115
|
+
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
const start = Date.now();
|
|
118
|
+
const poll = () => {
|
|
119
|
+
if (Date.now() - start > 30000) {
|
|
120
|
+
reject(new Error('Request timeout'));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (!this.#buffer.hasComplete()) {
|
|
124
|
+
return setTimeout(poll, 5);
|
|
125
|
+
}
|
|
126
|
+
const data = this.#buffer.extract();
|
|
127
|
+
try { resolve(JSON.parse(data)); }
|
|
128
|
+
catch { resolve(data); }
|
|
129
|
+
};
|
|
130
|
+
poll();
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#request(action, collection, document, documentId, query) {
|
|
135
|
+
const payload = { auth: this.#auth.getCredentials(), action };
|
|
136
|
+
if (collection) payload.collection = collection;
|
|
137
|
+
if (document) payload.document = document;
|
|
138
|
+
if (documentId) payload.document_id = documentId;
|
|
139
|
+
if (query) payload.query = query;
|
|
140
|
+
return this.#send(payload);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
on(event, handler) {
|
|
144
|
+
if (!this.#events[event]) this.#events[event] = [];
|
|
145
|
+
this.#events[event].push(handler);
|
|
146
|
+
return this;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#emit(event, data) {
|
|
150
|
+
const handlers = this.#events[event];
|
|
151
|
+
if (handlers) handlers.forEach(h => { try { h(data); } catch { } });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
get connected() { return this.#connected; }
|
|
155
|
+
get ready() { return this.#connectedPromise; }
|
|
156
|
+
|
|
157
|
+
insert(collection, data) {
|
|
158
|
+
this.#cache.invalidatePrefix(`list:${collection}`);
|
|
159
|
+
this.#cache.invalidatePrefix(`count:${collection}`);
|
|
160
|
+
this.#log.debug('insert', collection);
|
|
161
|
+
return this.#request('insert', collection, data);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
get(collection, id) {
|
|
165
|
+
const cacheKey = `get:${collection}:${id}`;
|
|
166
|
+
const cached = this.#cache.get(cacheKey);
|
|
167
|
+
if (cached) {
|
|
168
|
+
this.#log.debug('cache hit', cacheKey);
|
|
169
|
+
return Promise.resolve(cached);
|
|
170
|
+
}
|
|
171
|
+
return this.#request('get', collection, null, id).then(result => {
|
|
172
|
+
this.#cache.set(cacheKey, result);
|
|
173
|
+
return result;
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
list(collection) {
|
|
178
|
+
const cacheKey = `list:${collection}`;
|
|
179
|
+
const cached = this.#cache.get(cacheKey);
|
|
180
|
+
if (cached) return Promise.resolve(cached);
|
|
181
|
+
return this.#request('list', collection).then(result => {
|
|
182
|
+
this.#cache.set(cacheKey, result);
|
|
183
|
+
return result;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
search(collection, query) {
|
|
188
|
+
return this.#request('search', collection, null, null, query);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
update(collection, id, data) {
|
|
192
|
+
this.#cache.invalidate(`get:${collection}:${id}`);
|
|
193
|
+
this.#cache.invalidatePrefix(`list:${collection}`);
|
|
194
|
+
this.#log.debug('update', collection, id);
|
|
195
|
+
return this.#request('update', collection, data, id);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
delete(collection, id) {
|
|
199
|
+
this.#cache.invalidate(`get:${collection}:${id}`);
|
|
200
|
+
this.#cache.invalidatePrefix(`list:${collection}`);
|
|
201
|
+
this.#cache.invalidatePrefix(`count:${collection}`);
|
|
202
|
+
this.#log.debug('delete', collection, id);
|
|
203
|
+
return this.#request('delete', collection, null, id);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
count(collection) {
|
|
207
|
+
const cacheKey = `count:${collection}`;
|
|
208
|
+
const cached = this.#cache.get(cacheKey);
|
|
209
|
+
if (cached) return Promise.resolve(cached);
|
|
210
|
+
return this.#request('count', collection).then(result => {
|
|
211
|
+
this.#cache.set(cacheKey, result);
|
|
212
|
+
return result;
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
listCollections() {
|
|
217
|
+
return this.#request('list-collections');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
deleteCollection(collection) {
|
|
221
|
+
this.#cache.invalidatePrefix(collection);
|
|
222
|
+
return this.#request('delete-collection', collection);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
listPaginated(collection, options = {}) {
|
|
226
|
+
return this.#request('list-paginated', collection, {
|
|
227
|
+
limit: options.limit || 20,
|
|
228
|
+
offset: options.offset || 0,
|
|
229
|
+
page: options.page || 0,
|
|
230
|
+
cursor: options.cursor || '',
|
|
231
|
+
sort: options.sort || [],
|
|
232
|
+
}, null, options.filter || null);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
searchPaginated(collection, query, options = {}) {
|
|
236
|
+
return this.#request('search-paginated', collection, {
|
|
237
|
+
limit: options.limit || 20,
|
|
238
|
+
offset: options.offset || 0,
|
|
239
|
+
page: options.page || 0,
|
|
240
|
+
}, null, query);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
aggregate(collection, stages) {
|
|
244
|
+
return this.#request('aggregate', collection, { stages });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
bulkExecute(operations, options = {}) {
|
|
248
|
+
return this.#request('bulk-execute', null, {
|
|
249
|
+
operations,
|
|
250
|
+
stop_on_error: options.stopOnError || false,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
bulkInsert(collection, documents) {
|
|
255
|
+
this.#cache.invalidatePrefix(`list:${collection}`);
|
|
256
|
+
this.#cache.invalidatePrefix(`count:${collection}`);
|
|
257
|
+
const ops = documents.map(doc => ({ type: 'insert', collection, document: doc }));
|
|
258
|
+
return this.bulkExecute(ops);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
bulkUpdate(collection, updates) {
|
|
262
|
+
this.#cache.invalidatePrefix(collection);
|
|
263
|
+
const ops = Object.entries(updates).map(([id, doc]) => ({
|
|
264
|
+
type: 'update', collection, document_id: id, document: doc,
|
|
265
|
+
}));
|
|
266
|
+
return this.bulkExecute(ops);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
bulkDelete(collection, documentIds) {
|
|
270
|
+
this.#cache.invalidatePrefix(collection);
|
|
271
|
+
const ops = documentIds.map(id => ({ type: 'delete', collection, document_id: id }));
|
|
272
|
+
return this.bulkExecute(ops);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
getProjected(collection, id, projection) {
|
|
276
|
+
return this.#request('get-projected', collection, projection, id);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
searchProjected(collection, query, projection) {
|
|
280
|
+
return this.#request('search-projected', collection, projection, null, query);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
createIndex(collection, field, type = 'hash') {
|
|
284
|
+
return this.#request('create-index', collection, { field, type });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
listIndexes(collection) {
|
|
288
|
+
return this.#request('list-indexes', collection);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
queueInsert(collection, data) {
|
|
292
|
+
this.#batch.add({ type: 'insert', collection, document: data });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
queueUpdate(collection, id, data) {
|
|
296
|
+
this.#batch.add({ type: 'update', collection, document_id: id, document: data });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
queueDelete(collection, id) {
|
|
300
|
+
this.#batch.add({ type: 'delete', collection, document_id: id });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async flushQueue() {
|
|
304
|
+
await this.#batch.flush();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
clearCache() {
|
|
308
|
+
this.#cache.clear();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
collection(name) {
|
|
312
|
+
return new Collection(this, name);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
helper(name) {
|
|
316
|
+
return new CollectionHelper(this, name);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
query(collection) {
|
|
320
|
+
return new QueryBuilder(this, collection);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
close() {
|
|
324
|
+
this.#shouldReconnect = false;
|
|
325
|
+
this.#reconnect.disable();
|
|
326
|
+
this.#batch.destroy();
|
|
327
|
+
this.#cache.clear();
|
|
328
|
+
if (this.#ping) this.#ping.stop();
|
|
329
|
+
this.#ping = null;
|
|
330
|
+
try { this.#ws?.close(1000, 'Client disconnect'); } catch { }
|
|
331
|
+
this.#ws = null;
|
|
332
|
+
this.#connected = false;
|
|
333
|
+
this.#log.info('Connection closed');
|
|
334
|
+
this.#emit('closed');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
static fromEnv(host) {
|
|
338
|
+
return new NekoDB({
|
|
339
|
+
host: host || process.env.NEKODB_HOST,
|
|
340
|
+
username: process.env.NEKODB_USERNAME,
|
|
341
|
+
password: process.env.NEKODB_PASSWORD,
|
|
342
|
+
key: process.env.NEKODB_KEY,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
class Collection {
|
|
348
|
+
#db;
|
|
349
|
+
#name;
|
|
350
|
+
|
|
351
|
+
constructor(db, name) {
|
|
352
|
+
this.#db = db;
|
|
353
|
+
this.#name = name;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
get name() { return this.#name; }
|
|
357
|
+
|
|
358
|
+
insert(document) { return this.#db.insert(this.#name, document); }
|
|
359
|
+
get(id) { return this.#db.get(this.#name, id); }
|
|
360
|
+
list() { return this.#db.list(this.#name); }
|
|
361
|
+
search(query) { return this.#db.search(this.#name, query); }
|
|
362
|
+
update(id, data) { return this.#db.update(this.#name, id, data); }
|
|
363
|
+
delete(id) { return this.#db.delete(this.#name, id); }
|
|
364
|
+
count() { return this.#db.count(this.#name); }
|
|
365
|
+
drop() { return this.#db.deleteCollection(this.#name); }
|
|
366
|
+
listPaginated(options) { return this.#db.listPaginated(this.#name, options); }
|
|
367
|
+
searchPaginated(query, options) { return this.#db.searchPaginated(this.#name, query, options); }
|
|
368
|
+
aggregate(stages) { return this.#db.aggregate(this.#name, stages); }
|
|
369
|
+
bulkInsert(documents) { return this.#db.bulkInsert(this.#name, documents); }
|
|
370
|
+
bulkUpdate(updates) { return this.#db.bulkUpdate(this.#name, updates); }
|
|
371
|
+
bulkDelete(ids) { return this.#db.bulkDelete(this.#name, ids); }
|
|
372
|
+
getProjected(id, projection) { return this.#db.getProjected(this.#name, id, projection); }
|
|
373
|
+
searchProjected(query, projection) { return this.#db.searchProjected(this.#name, query, projection); }
|
|
374
|
+
createIndex(field, type) { return this.#db.createIndex(this.#name, field, type); }
|
|
375
|
+
listIndexes() { return this.#db.listIndexes(this.#name); }
|
|
376
|
+
queueInsert(data) { this.#db.queueInsert(this.#name, data); }
|
|
377
|
+
queueUpdate(id, data) { this.#db.queueUpdate(this.#name, id, data); }
|
|
378
|
+
queueDelete(id) { this.#db.queueDelete(this.#name, id); }
|
|
379
|
+
query() { return new QueryBuilder(this.#db, this.#name); }
|
|
380
|
+
helper() { return new CollectionHelper(this.#db, this.#name); }
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
module.exports = NekoDB;
|
|
384
|
+
module.exports.NekoDB = NekoDB;
|
|
385
|
+
module.exports.Collection = Collection;
|
|
386
|
+
module.exports.AuthManager = AuthManager;
|
|
387
|
+
module.exports.QueryBuilder = QueryBuilder;
|
|
388
|
+
module.exports.Schema = Schema;
|
|
389
|
+
module.exports.EventBus = EventBus;
|
|
390
|
+
module.exports.CollectionHelper = CollectionHelper;
|
|
391
|
+
module.exports.ConnectionManager = ConnectionManager;
|
|
392
|
+
module.exports.errors = errors;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
class BatchCollector {
|
|
2
|
+
#queue;
|
|
3
|
+
#maxSize;
|
|
4
|
+
#flushInterval;
|
|
5
|
+
#timer;
|
|
6
|
+
#onFlush;
|
|
7
|
+
|
|
8
|
+
constructor(onFlush, maxSize = 50, flushInterval = 3000) {
|
|
9
|
+
this.#queue = [];
|
|
10
|
+
this.#maxSize = maxSize;
|
|
11
|
+
this.#flushInterval = flushInterval;
|
|
12
|
+
this.#timer = null;
|
|
13
|
+
this.#onFlush = onFlush;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
add(operation) {
|
|
17
|
+
this.#queue.push(operation);
|
|
18
|
+
if (this.#queue.length >= this.#maxSize) {
|
|
19
|
+
this.flush();
|
|
20
|
+
} else if (!this.#timer) {
|
|
21
|
+
this.#timer = setTimeout(() => this.flush(), this.#flushInterval);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async flush() {
|
|
26
|
+
if (this.#timer) {
|
|
27
|
+
clearTimeout(this.#timer);
|
|
28
|
+
this.#timer = null;
|
|
29
|
+
}
|
|
30
|
+
if (this.#queue.length === 0) return;
|
|
31
|
+
const ops = [...this.#queue];
|
|
32
|
+
this.#queue = [];
|
|
33
|
+
if (this.#onFlush) await this.#onFlush(ops);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get pending() {
|
|
37
|
+
return this.#queue.length;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
destroy() {
|
|
41
|
+
if (this.#timer) {
|
|
42
|
+
clearTimeout(this.#timer);
|
|
43
|
+
this.#timer = null;
|
|
44
|
+
}
|
|
45
|
+
this.#queue = [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { BatchCollector };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
class BufferManager {
|
|
2
|
+
#buffer;
|
|
3
|
+
#delimiter;
|
|
4
|
+
|
|
5
|
+
constructor(delimiter = '\n') {
|
|
6
|
+
this.#buffer = '';
|
|
7
|
+
this.#delimiter = delimiter;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
append(data) {
|
|
11
|
+
this.#buffer += data.toString();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
hasComplete() {
|
|
15
|
+
return this.#buffer.includes(this.#delimiter);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
extract() {
|
|
19
|
+
const data = this.#buffer.trim();
|
|
20
|
+
this.#buffer = '';
|
|
21
|
+
return data;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
clear() {
|
|
25
|
+
this.#buffer = '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get length() {
|
|
29
|
+
return this.#buffer.length;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { BufferManager };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
class ResponseCache {
|
|
2
|
+
#cache;
|
|
3
|
+
#ttl;
|
|
4
|
+
#maxSize;
|
|
5
|
+
|
|
6
|
+
constructor(ttl = 10000, maxSize = 200) {
|
|
7
|
+
this.#cache = new Map();
|
|
8
|
+
this.#ttl = ttl;
|
|
9
|
+
this.#maxSize = maxSize;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
set(key, value) {
|
|
13
|
+
if (this.#cache.size >= this.#maxSize) {
|
|
14
|
+
const oldest = this.#cache.keys().next().value;
|
|
15
|
+
this.#cache.delete(oldest);
|
|
16
|
+
}
|
|
17
|
+
this.#cache.set(key, { value, expires: Date.now() + this.#ttl });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get(key) {
|
|
21
|
+
const entry = this.#cache.get(key);
|
|
22
|
+
if (!entry) return null;
|
|
23
|
+
if (Date.now() > entry.expires) {
|
|
24
|
+
this.#cache.delete(key);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return entry.value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
has(key) {
|
|
31
|
+
return this.get(key) !== null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
invalidate(key) {
|
|
35
|
+
this.#cache.delete(key);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
invalidatePrefix(prefix) {
|
|
39
|
+
for (const key of this.#cache.keys()) {
|
|
40
|
+
if (key.startsWith(prefix)) {
|
|
41
|
+
this.#cache.delete(key);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
clear() {
|
|
47
|
+
this.#cache.clear();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get size() {
|
|
51
|
+
return this.#cache.size;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { ResponseCache };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const { BufferManager } = require('./buffer');
|
|
2
|
+
const { PingManager } = require('./ping');
|
|
3
|
+
const { ReconnectManager } = require('./reconnect');
|
|
4
|
+
const { ResponseCache } = require('./cache');
|
|
5
|
+
const { BatchCollector } = require('./batch');
|
|
6
|
+
const { Logger } = require('./logger');
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
BufferManager,
|
|
10
|
+
PingManager,
|
|
11
|
+
ReconnectManager,
|
|
12
|
+
ResponseCache,
|
|
13
|
+
BatchCollector,
|
|
14
|
+
Logger,
|
|
15
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
class Logger {
|
|
2
|
+
#enabled;
|
|
3
|
+
#prefix;
|
|
4
|
+
#level;
|
|
5
|
+
#levels;
|
|
6
|
+
|
|
7
|
+
constructor(prefix = '[NekoDB]', level = 'info') {
|
|
8
|
+
this.#enabled = true;
|
|
9
|
+
this.#prefix = prefix;
|
|
10
|
+
this.#levels = { debug: 0, info: 1, warn: 2, error: 3, none: 4 };
|
|
11
|
+
this.#level = this.#levels[level] || 1;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
debug(...args) {
|
|
15
|
+
if (this.#enabled && this.#level <= 0) console.log(this.#prefix, '[DEBUG]', ...args);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
info(...args) {
|
|
19
|
+
if (this.#enabled && this.#level <= 1) console.log(this.#prefix, '[INFO]', ...args);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
warn(...args) {
|
|
23
|
+
if (this.#enabled && this.#level <= 2) console.warn(this.#prefix, '[WARN]', ...args);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
error(...args) {
|
|
27
|
+
if (this.#enabled && this.#level <= 3) console.error(this.#prefix, '[ERROR]', ...args);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
enable() { this.#enabled = true; }
|
|
31
|
+
disable() { this.#enabled = false; }
|
|
32
|
+
setLevel(level) { this.#level = this.#levels[level] || 1; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { Logger };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
class PingManager {
|
|
2
|
+
#ws;
|
|
3
|
+
#interval;
|
|
4
|
+
#timer;
|
|
5
|
+
|
|
6
|
+
constructor(ws, interval = 25000) {
|
|
7
|
+
this.#ws = ws;
|
|
8
|
+
this.#interval = interval;
|
|
9
|
+
this.#timer = null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
start() {
|
|
13
|
+
if (this.#timer) return;
|
|
14
|
+
this.#timer = setInterval(() => {
|
|
15
|
+
if (this.#ws && this.#ws.readyState === 1) {
|
|
16
|
+
try { this.#ws.ping(); } catch { }
|
|
17
|
+
}
|
|
18
|
+
}, this.#interval);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
stop() {
|
|
22
|
+
if (this.#timer) {
|
|
23
|
+
clearInterval(this.#timer);
|
|
24
|
+
this.#timer = null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = { PingManager };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
class ReconnectManager {
|
|
2
|
+
#delay;
|
|
3
|
+
#maxDelay;
|
|
4
|
+
#timer;
|
|
5
|
+
#enabled;
|
|
6
|
+
|
|
7
|
+
constructor(delay = 1500, maxDelay = 30000) {
|
|
8
|
+
this.#delay = delay;
|
|
9
|
+
this.#maxDelay = maxDelay;
|
|
10
|
+
this.#timer = null;
|
|
11
|
+
this.#enabled = true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
schedule(fn) {
|
|
15
|
+
if (!this.#enabled) return;
|
|
16
|
+
const wait = Math.min(this.#delay, this.#maxDelay);
|
|
17
|
+
this.#timer = setTimeout(() => {
|
|
18
|
+
fn();
|
|
19
|
+
this.#delay = Math.min(this.#delay * 2, this.#maxDelay);
|
|
20
|
+
}, wait);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
reset() {
|
|
24
|
+
this.#delay = 1500;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
disable() {
|
|
28
|
+
this.#enabled = false;
|
|
29
|
+
if (this.#timer) {
|
|
30
|
+
clearTimeout(this.#timer);
|
|
31
|
+
this.#timer = null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { ReconnectManager };
|