@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.
@@ -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 };