@push.rocks/smartdb 1.0.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.
Files changed (112) hide show
  1. package/.smartconfig.json +38 -0
  2. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  3. package/dist_ts/00_commitinfo_data.js +9 -0
  4. package/dist_ts/index.d.ts +5 -0
  5. package/dist_ts/index.js +8 -0
  6. package/dist_ts/ts_local/classes.localsmartdb.d.ts +78 -0
  7. package/dist_ts/ts_local/classes.localsmartdb.js +115 -0
  8. package/dist_ts/ts_local/index.d.ts +2 -0
  9. package/dist_ts/ts_local/index.js +2 -0
  10. package/dist_ts/ts_local/plugins.d.ts +2 -0
  11. package/dist_ts/ts_local/plugins.js +3 -0
  12. package/dist_ts/ts_smartdb/engine/AggregationEngine.d.ts +66 -0
  13. package/dist_ts/ts_smartdb/engine/AggregationEngine.js +189 -0
  14. package/dist_ts/ts_smartdb/engine/IndexEngine.d.ts +97 -0
  15. package/dist_ts/ts_smartdb/engine/IndexEngine.js +678 -0
  16. package/dist_ts/ts_smartdb/engine/QueryEngine.d.ts +54 -0
  17. package/dist_ts/ts_smartdb/engine/QueryEngine.js +271 -0
  18. package/dist_ts/ts_smartdb/engine/QueryPlanner.d.ts +64 -0
  19. package/dist_ts/ts_smartdb/engine/QueryPlanner.js +308 -0
  20. package/dist_ts/ts_smartdb/engine/SessionEngine.d.ts +117 -0
  21. package/dist_ts/ts_smartdb/engine/SessionEngine.js +232 -0
  22. package/dist_ts/ts_smartdb/engine/TransactionEngine.d.ts +85 -0
  23. package/dist_ts/ts_smartdb/engine/TransactionEngine.js +287 -0
  24. package/dist_ts/ts_smartdb/engine/UpdateEngine.d.ts +47 -0
  25. package/dist_ts/ts_smartdb/engine/UpdateEngine.js +461 -0
  26. package/dist_ts/ts_smartdb/errors/SmartdbErrors.d.ts +100 -0
  27. package/dist_ts/ts_smartdb/errors/SmartdbErrors.js +155 -0
  28. package/dist_ts/ts_smartdb/index.d.ts +26 -0
  29. package/dist_ts/ts_smartdb/index.js +31 -0
  30. package/dist_ts/ts_smartdb/plugins.d.ts +10 -0
  31. package/dist_ts/ts_smartdb/plugins.js +14 -0
  32. package/dist_ts/ts_smartdb/server/CommandRouter.d.ts +87 -0
  33. package/dist_ts/ts_smartdb/server/CommandRouter.js +222 -0
  34. package/dist_ts/ts_smartdb/server/SmartdbServer.d.ts +102 -0
  35. package/dist_ts/ts_smartdb/server/SmartdbServer.js +279 -0
  36. package/dist_ts/ts_smartdb/server/WireProtocol.d.ts +117 -0
  37. package/dist_ts/ts_smartdb/server/WireProtocol.js +298 -0
  38. package/dist_ts/ts_smartdb/server/handlers/AdminHandler.d.ts +100 -0
  39. package/dist_ts/ts_smartdb/server/handlers/AdminHandler.js +668 -0
  40. package/dist_ts/ts_smartdb/server/handlers/AggregateHandler.d.ts +31 -0
  41. package/dist_ts/ts_smartdb/server/handlers/AggregateHandler.js +277 -0
  42. package/dist_ts/ts_smartdb/server/handlers/DeleteHandler.d.ts +8 -0
  43. package/dist_ts/ts_smartdb/server/handlers/DeleteHandler.js +95 -0
  44. package/dist_ts/ts_smartdb/server/handlers/FindHandler.d.ts +31 -0
  45. package/dist_ts/ts_smartdb/server/handlers/FindHandler.js +291 -0
  46. package/dist_ts/ts_smartdb/server/handlers/HelloHandler.d.ts +11 -0
  47. package/dist_ts/ts_smartdb/server/handlers/HelloHandler.js +62 -0
  48. package/dist_ts/ts_smartdb/server/handlers/IndexHandler.d.ts +20 -0
  49. package/dist_ts/ts_smartdb/server/handlers/IndexHandler.js +183 -0
  50. package/dist_ts/ts_smartdb/server/handlers/InsertHandler.d.ts +8 -0
  51. package/dist_ts/ts_smartdb/server/handlers/InsertHandler.js +79 -0
  52. package/dist_ts/ts_smartdb/server/handlers/UpdateHandler.d.ts +24 -0
  53. package/dist_ts/ts_smartdb/server/handlers/UpdateHandler.js +296 -0
  54. package/dist_ts/ts_smartdb/server/handlers/index.d.ts +8 -0
  55. package/dist_ts/ts_smartdb/server/handlers/index.js +10 -0
  56. package/dist_ts/ts_smartdb/server/index.d.ts +6 -0
  57. package/dist_ts/ts_smartdb/server/index.js +7 -0
  58. package/dist_ts/ts_smartdb/storage/FileStorageAdapter.d.ts +85 -0
  59. package/dist_ts/ts_smartdb/storage/FileStorageAdapter.js +465 -0
  60. package/dist_ts/ts_smartdb/storage/IStorageAdapter.d.ts +145 -0
  61. package/dist_ts/ts_smartdb/storage/IStorageAdapter.js +2 -0
  62. package/dist_ts/ts_smartdb/storage/MemoryStorageAdapter.d.ts +67 -0
  63. package/dist_ts/ts_smartdb/storage/MemoryStorageAdapter.js +378 -0
  64. package/dist_ts/ts_smartdb/storage/OpLog.d.ts +93 -0
  65. package/dist_ts/ts_smartdb/storage/OpLog.js +221 -0
  66. package/dist_ts/ts_smartdb/storage/WAL.d.ts +117 -0
  67. package/dist_ts/ts_smartdb/storage/WAL.js +286 -0
  68. package/dist_ts/ts_smartdb/types/interfaces.d.ts +363 -0
  69. package/dist_ts/ts_smartdb/types/interfaces.js +2 -0
  70. package/dist_ts/ts_smartdb/utils/checksum.d.ts +30 -0
  71. package/dist_ts/ts_smartdb/utils/checksum.js +77 -0
  72. package/dist_ts/ts_smartdb/utils/index.d.ts +1 -0
  73. package/dist_ts/ts_smartdb/utils/index.js +2 -0
  74. package/license +19 -0
  75. package/package.json +69 -0
  76. package/readme.md +529 -0
  77. package/ts/00_commitinfo_data.ts +8 -0
  78. package/ts/index.ts +11 -0
  79. package/ts/ts_local/classes.localsmartdb.ts +143 -0
  80. package/ts/ts_local/index.ts +2 -0
  81. package/ts/ts_local/plugins.ts +3 -0
  82. package/ts/ts_smartdb/engine/AggregationEngine.ts +283 -0
  83. package/ts/ts_smartdb/engine/IndexEngine.ts +798 -0
  84. package/ts/ts_smartdb/engine/QueryEngine.ts +301 -0
  85. package/ts/ts_smartdb/engine/QueryPlanner.ts +393 -0
  86. package/ts/ts_smartdb/engine/SessionEngine.ts +292 -0
  87. package/ts/ts_smartdb/engine/TransactionEngine.ts +351 -0
  88. package/ts/ts_smartdb/engine/UpdateEngine.ts +506 -0
  89. package/ts/ts_smartdb/errors/SmartdbErrors.ts +181 -0
  90. package/ts/ts_smartdb/index.ts +46 -0
  91. package/ts/ts_smartdb/plugins.ts +17 -0
  92. package/ts/ts_smartdb/server/CommandRouter.ts +289 -0
  93. package/ts/ts_smartdb/server/SmartdbServer.ts +354 -0
  94. package/ts/ts_smartdb/server/WireProtocol.ts +416 -0
  95. package/ts/ts_smartdb/server/handlers/AdminHandler.ts +719 -0
  96. package/ts/ts_smartdb/server/handlers/AggregateHandler.ts +342 -0
  97. package/ts/ts_smartdb/server/handlers/DeleteHandler.ts +115 -0
  98. package/ts/ts_smartdb/server/handlers/FindHandler.ts +330 -0
  99. package/ts/ts_smartdb/server/handlers/HelloHandler.ts +78 -0
  100. package/ts/ts_smartdb/server/handlers/IndexHandler.ts +207 -0
  101. package/ts/ts_smartdb/server/handlers/InsertHandler.ts +97 -0
  102. package/ts/ts_smartdb/server/handlers/UpdateHandler.ts +344 -0
  103. package/ts/ts_smartdb/server/handlers/index.ts +10 -0
  104. package/ts/ts_smartdb/server/index.ts +10 -0
  105. package/ts/ts_smartdb/storage/FileStorageAdapter.ts +562 -0
  106. package/ts/ts_smartdb/storage/IStorageAdapter.ts +208 -0
  107. package/ts/ts_smartdb/storage/MemoryStorageAdapter.ts +455 -0
  108. package/ts/ts_smartdb/storage/OpLog.ts +282 -0
  109. package/ts/ts_smartdb/storage/WAL.ts +375 -0
  110. package/ts/ts_smartdb/types/interfaces.ts +433 -0
  111. package/ts/ts_smartdb/utils/checksum.ts +88 -0
  112. package/ts/ts_smartdb/utils/index.ts +1 -0
@@ -0,0 +1,46 @@
1
+ // SmartDB - MongoDB Wire Protocol compatible in-memory database server
2
+ // Use the official MongoDB driver to connect to SmartdbServer
3
+
4
+ // Re-export plugins for external use
5
+ import * as plugins from './plugins.js';
6
+ export { plugins };
7
+
8
+ // Export BSON types for convenience
9
+ export { ObjectId, Binary, Timestamp, Long, Decimal128, UUID } from 'bson';
10
+
11
+ // Export all types
12
+ export * from './types/interfaces.js';
13
+
14
+ // Export errors
15
+ export * from './errors/SmartdbErrors.js';
16
+
17
+ // Export storage adapters
18
+ export type { IStorageAdapter } from './storage/IStorageAdapter.js';
19
+ export { MemoryStorageAdapter } from './storage/MemoryStorageAdapter.js';
20
+ export { FileStorageAdapter } from './storage/FileStorageAdapter.js';
21
+ export { OpLog } from './storage/OpLog.js';
22
+ export { WAL } from './storage/WAL.js';
23
+ export type { IWalEntry, TWalOperation } from './storage/WAL.js';
24
+
25
+ // Export engines
26
+ export { QueryEngine } from './engine/QueryEngine.js';
27
+ export { UpdateEngine } from './engine/UpdateEngine.js';
28
+ export { AggregationEngine } from './engine/AggregationEngine.js';
29
+ export { IndexEngine } from './engine/IndexEngine.js';
30
+ export { TransactionEngine } from './engine/TransactionEngine.js';
31
+ export { QueryPlanner } from './engine/QueryPlanner.js';
32
+ export type { IQueryPlan, TQueryPlanType } from './engine/QueryPlanner.js';
33
+ export { SessionEngine } from './engine/SessionEngine.js';
34
+ export type { ISession, ISessionEngineOptions } from './engine/SessionEngine.js';
35
+
36
+ // Export server (the main entry point for using SmartDB)
37
+ export { SmartdbServer } from './server/SmartdbServer.js';
38
+ export type { ISmartdbServerOptions } from './server/SmartdbServer.js';
39
+
40
+ // Export wire protocol utilities (for advanced usage)
41
+ export { WireProtocol } from './server/WireProtocol.js';
42
+ export { CommandRouter } from './server/CommandRouter.js';
43
+ export type { ICommandHandler, IHandlerContext, ICursorState } from './server/CommandRouter.js';
44
+
45
+ // Export utilities
46
+ export * from './utils/checksum.js';
@@ -0,0 +1,17 @@
1
+ // @push.rocks scope
2
+ import * as smartfs from '@push.rocks/smartfs';
3
+ import * as smartpath from '@push.rocks/smartpath';
4
+ import * as smartpromise from '@push.rocks/smartpromise';
5
+ import * as smartrx from '@push.rocks/smartrx';
6
+
7
+ export { smartfs, smartpath, smartpromise, smartrx };
8
+
9
+ // thirdparty
10
+ import * as bson from 'bson';
11
+ import * as mingo from 'mingo';
12
+
13
+ export { bson, mingo };
14
+
15
+ // Re-export commonly used mingo classes
16
+ export { Query } from 'mingo';
17
+ export { Aggregator } from 'mingo';
@@ -0,0 +1,289 @@
1
+ import * as plugins from '../plugins.js';
2
+ import type { IStorageAdapter } from '../storage/IStorageAdapter.js';
3
+ import type { IParsedCommand } from './WireProtocol.js';
4
+ import type { SmartdbServer } from './SmartdbServer.js';
5
+ import { IndexEngine } from '../engine/IndexEngine.js';
6
+ import { TransactionEngine } from '../engine/TransactionEngine.js';
7
+ import { SessionEngine } from '../engine/SessionEngine.js';
8
+
9
+ // Import handlers
10
+ import { HelloHandler } from './handlers/HelloHandler.js';
11
+ import { InsertHandler } from './handlers/InsertHandler.js';
12
+ import { FindHandler } from './handlers/FindHandler.js';
13
+ import { UpdateHandler } from './handlers/UpdateHandler.js';
14
+ import { DeleteHandler } from './handlers/DeleteHandler.js';
15
+ import { AggregateHandler } from './handlers/AggregateHandler.js';
16
+ import { IndexHandler } from './handlers/IndexHandler.js';
17
+ import { AdminHandler } from './handlers/AdminHandler.js';
18
+
19
+ /**
20
+ * Handler context passed to command handlers
21
+ */
22
+ export interface IHandlerContext {
23
+ storage: IStorageAdapter;
24
+ server: SmartdbServer;
25
+ database: string;
26
+ command: plugins.bson.Document;
27
+ documentSequences?: Map<string, plugins.bson.Document[]>;
28
+ /** Get or create an IndexEngine for a collection */
29
+ getIndexEngine: (collName: string) => IndexEngine;
30
+ /** Transaction engine instance */
31
+ transactionEngine: TransactionEngine;
32
+ /** Current transaction ID (if in a transaction) */
33
+ txnId?: string;
34
+ /** Session ID (from lsid) */
35
+ sessionId?: string;
36
+ /** Session engine instance */
37
+ sessionEngine: SessionEngine;
38
+ }
39
+
40
+ /**
41
+ * Command handler interface
42
+ */
43
+ export interface ICommandHandler {
44
+ handle(context: IHandlerContext): Promise<plugins.bson.Document>;
45
+ }
46
+
47
+ /**
48
+ * CommandRouter - Routes incoming commands to appropriate handlers
49
+ */
50
+ export class CommandRouter {
51
+ private storage: IStorageAdapter;
52
+ private server: SmartdbServer;
53
+ private handlers: Map<string, ICommandHandler> = new Map();
54
+
55
+ // Cursor state for getMore operations
56
+ private cursors: Map<bigint, ICursorState> = new Map();
57
+ private cursorIdCounter: bigint = BigInt(1);
58
+
59
+ // Index engine cache: db.collection -> IndexEngine
60
+ private indexEngines: Map<string, IndexEngine> = new Map();
61
+
62
+ // Transaction engine (shared across all handlers)
63
+ private transactionEngine: TransactionEngine;
64
+
65
+ // Session engine (shared across all handlers)
66
+ private sessionEngine: SessionEngine;
67
+
68
+ constructor(storage: IStorageAdapter, server: SmartdbServer) {
69
+ this.storage = storage;
70
+ this.server = server;
71
+ this.transactionEngine = new TransactionEngine(storage);
72
+ this.sessionEngine = new SessionEngine();
73
+ // Link session engine to transaction engine for auto-abort on session expiry
74
+ this.sessionEngine.setTransactionEngine(this.transactionEngine);
75
+ this.registerHandlers();
76
+ }
77
+
78
+ /**
79
+ * Get or create an IndexEngine for a database.collection
80
+ */
81
+ getIndexEngine(dbName: string, collName: string): IndexEngine {
82
+ const key = `${dbName}.${collName}`;
83
+ let engine = this.indexEngines.get(key);
84
+ if (!engine) {
85
+ engine = new IndexEngine(dbName, collName, this.storage);
86
+ this.indexEngines.set(key, engine);
87
+ }
88
+ return engine;
89
+ }
90
+
91
+ /**
92
+ * Clear index engine cache for a collection (used when collection is dropped)
93
+ */
94
+ clearIndexEngineCache(dbName: string, collName?: string): void {
95
+ if (collName) {
96
+ this.indexEngines.delete(`${dbName}.${collName}`);
97
+ } else {
98
+ // Clear all engines for the database
99
+ for (const key of this.indexEngines.keys()) {
100
+ if (key.startsWith(`${dbName}.`)) {
101
+ this.indexEngines.delete(key);
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Register all command handlers
109
+ */
110
+ private registerHandlers(): void {
111
+ // Create handler instances with shared state
112
+ const helloHandler = new HelloHandler();
113
+ const findHandler = new FindHandler(this.cursors, () => this.cursorIdCounter++);
114
+ const insertHandler = new InsertHandler();
115
+ const updateHandler = new UpdateHandler();
116
+ const deleteHandler = new DeleteHandler();
117
+ const aggregateHandler = new AggregateHandler(this.cursors, () => this.cursorIdCounter++);
118
+ const indexHandler = new IndexHandler();
119
+ const adminHandler = new AdminHandler();
120
+
121
+ // Handshake commands
122
+ this.handlers.set('hello', helloHandler);
123
+ this.handlers.set('ismaster', helloHandler);
124
+ this.handlers.set('isMaster', helloHandler);
125
+
126
+ // CRUD commands
127
+ this.handlers.set('find', findHandler);
128
+ this.handlers.set('insert', insertHandler);
129
+ this.handlers.set('update', updateHandler);
130
+ this.handlers.set('delete', deleteHandler);
131
+ this.handlers.set('findAndModify', updateHandler);
132
+ this.handlers.set('getMore', findHandler);
133
+ this.handlers.set('killCursors', findHandler);
134
+
135
+ // Aggregation
136
+ this.handlers.set('aggregate', aggregateHandler);
137
+ this.handlers.set('count', findHandler);
138
+ this.handlers.set('distinct', findHandler);
139
+
140
+ // Index operations
141
+ this.handlers.set('createIndexes', indexHandler);
142
+ this.handlers.set('dropIndexes', indexHandler);
143
+ this.handlers.set('listIndexes', indexHandler);
144
+
145
+ // Admin/Database operations
146
+ this.handlers.set('ping', adminHandler);
147
+ this.handlers.set('listDatabases', adminHandler);
148
+ this.handlers.set('listCollections', adminHandler);
149
+ this.handlers.set('drop', adminHandler);
150
+ this.handlers.set('dropDatabase', adminHandler);
151
+ this.handlers.set('create', adminHandler);
152
+ this.handlers.set('serverStatus', adminHandler);
153
+ this.handlers.set('buildInfo', adminHandler);
154
+ this.handlers.set('whatsmyuri', adminHandler);
155
+ this.handlers.set('getLog', adminHandler);
156
+ this.handlers.set('hostInfo', adminHandler);
157
+ this.handlers.set('replSetGetStatus', adminHandler);
158
+ this.handlers.set('isMaster', helloHandler);
159
+ this.handlers.set('saslStart', adminHandler);
160
+ this.handlers.set('saslContinue', adminHandler);
161
+ this.handlers.set('endSessions', adminHandler);
162
+ this.handlers.set('abortTransaction', adminHandler);
163
+ this.handlers.set('commitTransaction', adminHandler);
164
+ this.handlers.set('collStats', adminHandler);
165
+ this.handlers.set('dbStats', adminHandler);
166
+ this.handlers.set('connectionStatus', adminHandler);
167
+ this.handlers.set('currentOp', adminHandler);
168
+ this.handlers.set('collMod', adminHandler);
169
+ this.handlers.set('renameCollection', adminHandler);
170
+ }
171
+
172
+ /**
173
+ * Route a command to its handler
174
+ */
175
+ async route(parsedCommand: IParsedCommand): Promise<plugins.bson.Document> {
176
+ const { commandName, command, database, documentSequences } = parsedCommand;
177
+
178
+ // Extract session ID from lsid using SessionEngine helper
179
+ let sessionId = SessionEngine.extractSessionId(command.lsid);
180
+ let txnId: string | undefined;
181
+
182
+ // If we have a session ID, register/touch the session
183
+ if (sessionId) {
184
+ this.sessionEngine.getOrCreateSession(sessionId);
185
+ }
186
+
187
+ // Check if this starts a new transaction
188
+ if (command.startTransaction && sessionId) {
189
+ txnId = this.transactionEngine.startTransaction(sessionId);
190
+ this.sessionEngine.startTransaction(sessionId, txnId, command.txnNumber);
191
+ } else if (sessionId && this.sessionEngine.isInTransaction(sessionId)) {
192
+ // Continue existing transaction
193
+ txnId = this.sessionEngine.getTransactionId(sessionId);
194
+ // Verify transaction is still active
195
+ if (txnId && !this.transactionEngine.isActive(txnId)) {
196
+ this.sessionEngine.endTransaction(sessionId);
197
+ txnId = undefined;
198
+ }
199
+ }
200
+
201
+ // Create handler context
202
+ const context: IHandlerContext = {
203
+ storage: this.storage,
204
+ server: this.server,
205
+ database,
206
+ command,
207
+ documentSequences,
208
+ getIndexEngine: (collName: string) => this.getIndexEngine(database, collName),
209
+ transactionEngine: this.transactionEngine,
210
+ sessionEngine: this.sessionEngine,
211
+ txnId,
212
+ sessionId,
213
+ };
214
+
215
+ // Find handler
216
+ const handler = this.handlers.get(commandName);
217
+
218
+ if (!handler) {
219
+ // Unknown command
220
+ return {
221
+ ok: 0,
222
+ errmsg: `no such command: '${commandName}'`,
223
+ code: 59,
224
+ codeName: 'CommandNotFound',
225
+ };
226
+ }
227
+
228
+ try {
229
+ return await handler.handle(context);
230
+ } catch (error: any) {
231
+ // Handle known error types
232
+ if (error.code) {
233
+ return {
234
+ ok: 0,
235
+ errmsg: error.message,
236
+ code: error.code,
237
+ codeName: error.codeName || 'UnknownError',
238
+ };
239
+ }
240
+
241
+ // Generic error
242
+ return {
243
+ ok: 0,
244
+ errmsg: error.message || 'Internal error',
245
+ code: 1,
246
+ codeName: 'InternalError',
247
+ };
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Close the command router and cleanup resources
253
+ */
254
+ close(): void {
255
+ // Close session engine (stops cleanup interval, clears sessions)
256
+ this.sessionEngine.close();
257
+ // Clear cursors
258
+ this.cursors.clear();
259
+ // Clear index engine cache
260
+ this.indexEngines.clear();
261
+ }
262
+
263
+ /**
264
+ * Get session engine (for administrative purposes)
265
+ */
266
+ getSessionEngine(): SessionEngine {
267
+ return this.sessionEngine;
268
+ }
269
+
270
+ /**
271
+ * Get transaction engine (for administrative purposes)
272
+ */
273
+ getTransactionEngine(): TransactionEngine {
274
+ return this.transactionEngine;
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Cursor state for multi-batch queries
280
+ */
281
+ export interface ICursorState {
282
+ id: bigint;
283
+ database: string;
284
+ collection: string;
285
+ documents: plugins.bson.Document[];
286
+ position: number;
287
+ batchSize: number;
288
+ createdAt: Date;
289
+ }
@@ -0,0 +1,354 @@
1
+ import * as net from 'net';
2
+ import * as fs from 'fs/promises';
3
+ import * as plugins from '../plugins.js';
4
+ import { WireProtocol, OP_QUERY } from './WireProtocol.js';
5
+ import { CommandRouter } from './CommandRouter.js';
6
+ import { MemoryStorageAdapter } from '../storage/MemoryStorageAdapter.js';
7
+ import { FileStorageAdapter } from '../storage/FileStorageAdapter.js';
8
+ import type { IStorageAdapter } from '../storage/IStorageAdapter.js';
9
+
10
+ /**
11
+ * Server configuration options
12
+ */
13
+ export interface ISmartdbServerOptions {
14
+ /** Port to listen on (default: 27017) - ignored if socketPath is set */
15
+ port?: number;
16
+ /** Host to bind to (default: 127.0.0.1) - ignored if socketPath is set */
17
+ host?: string;
18
+ /** Unix socket path - if set, server listens on socket instead of TCP */
19
+ socketPath?: string;
20
+ /** Storage type: 'memory' or 'file' (default: 'memory') */
21
+ storage?: 'memory' | 'file';
22
+ /** Path for file storage (required if storage is 'file') */
23
+ storagePath?: string;
24
+ /** Enable persistence for memory storage */
25
+ persistPath?: string;
26
+ /** Persistence interval in ms (default: 60000) */
27
+ persistIntervalMs?: number;
28
+ }
29
+
30
+ /**
31
+ * Connection state for each client
32
+ */
33
+ interface IConnectionState {
34
+ id: number;
35
+ socket: net.Socket;
36
+ buffer: Buffer;
37
+ authenticated: boolean;
38
+ database: string;
39
+ }
40
+
41
+ /**
42
+ * SmartdbServer - MongoDB Wire Protocol compatible server
43
+ *
44
+ * This server implements the MongoDB wire protocol (OP_MSG) to allow
45
+ * official MongoDB drivers to connect and perform operations.
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * import { SmartdbServer } from '@push.rocks/smartmongo/smartdb';
50
+ * import { MongoClient } from 'mongodb';
51
+ *
52
+ * const server = new SmartdbServer({ port: 27017 });
53
+ * await server.start();
54
+ *
55
+ * const client = new MongoClient('mongodb://127.0.0.1:27017');
56
+ * await client.connect();
57
+ * ```
58
+ */
59
+ export class SmartdbServer {
60
+ private options: Required<Omit<ISmartdbServerOptions, 'socketPath'>> & { socketPath: string };
61
+ private server: net.Server | null = null;
62
+ private storage: IStorageAdapter;
63
+ private commandRouter: CommandRouter;
64
+ private connections: Map<number, IConnectionState> = new Map();
65
+ private connectionIdCounter = 0;
66
+ private isRunning = false;
67
+ private startTime: Date = new Date();
68
+ private useSocket: boolean;
69
+
70
+ constructor(options: ISmartdbServerOptions = {}) {
71
+ this.useSocket = !!options.socketPath;
72
+ this.options = {
73
+ port: options.port ?? 27017,
74
+ host: options.host ?? '127.0.0.1',
75
+ socketPath: options.socketPath ?? '',
76
+ storage: options.storage ?? 'memory',
77
+ storagePath: options.storagePath ?? './data',
78
+ persistPath: options.persistPath ?? '',
79
+ persistIntervalMs: options.persistIntervalMs ?? 60000,
80
+ };
81
+
82
+ // Create storage adapter
83
+ if (this.options.storage === 'file') {
84
+ this.storage = new FileStorageAdapter(this.options.storagePath);
85
+ } else {
86
+ this.storage = new MemoryStorageAdapter({
87
+ persistPath: this.options.persistPath || undefined,
88
+ persistIntervalMs: this.options.persistPath ? this.options.persistIntervalMs : undefined,
89
+ });
90
+ }
91
+
92
+ // Create command router
93
+ this.commandRouter = new CommandRouter(this.storage, this);
94
+ }
95
+
96
+ /**
97
+ * Get the storage adapter (for testing/debugging)
98
+ */
99
+ getStorage(): IStorageAdapter {
100
+ return this.storage;
101
+ }
102
+
103
+ /**
104
+ * Get server uptime in seconds
105
+ */
106
+ getUptime(): number {
107
+ return Math.floor((Date.now() - this.startTime.getTime()) / 1000);
108
+ }
109
+
110
+ /**
111
+ * Get current connection count
112
+ */
113
+ getConnectionCount(): number {
114
+ return this.connections.size;
115
+ }
116
+
117
+ /**
118
+ * Start the server
119
+ */
120
+ async start(): Promise<void> {
121
+ if (this.isRunning) {
122
+ throw new Error('Server is already running');
123
+ }
124
+
125
+ // Initialize storage
126
+ await this.storage.initialize();
127
+
128
+ // Clean up stale socket file if using Unix socket
129
+ if (this.useSocket && this.options.socketPath) {
130
+ try {
131
+ await fs.unlink(this.options.socketPath);
132
+ } catch (err: any) {
133
+ // Ignore ENOENT (file doesn't exist)
134
+ if (err.code !== 'ENOENT') {
135
+ throw err;
136
+ }
137
+ }
138
+ }
139
+
140
+ return new Promise((resolve, reject) => {
141
+ this.server = net.createServer((socket) => {
142
+ this.handleConnection(socket);
143
+ });
144
+
145
+ this.server.on('error', (err) => {
146
+ if (!this.isRunning) {
147
+ reject(err);
148
+ } else {
149
+ console.error('Server error:', err);
150
+ }
151
+ });
152
+
153
+ if (this.useSocket && this.options.socketPath) {
154
+ // Listen on Unix socket
155
+ this.server.listen(this.options.socketPath, () => {
156
+ this.isRunning = true;
157
+ this.startTime = new Date();
158
+ resolve();
159
+ });
160
+ } else {
161
+ // Listen on TCP
162
+ this.server.listen(this.options.port, this.options.host, () => {
163
+ this.isRunning = true;
164
+ this.startTime = new Date();
165
+ resolve();
166
+ });
167
+ }
168
+ });
169
+ }
170
+
171
+ /**
172
+ * Stop the server
173
+ */
174
+ async stop(): Promise<void> {
175
+ if (!this.isRunning || !this.server) {
176
+ return;
177
+ }
178
+
179
+ // Close all connections
180
+ for (const conn of this.connections.values()) {
181
+ conn.socket.destroy();
182
+ }
183
+ this.connections.clear();
184
+
185
+ // Close command router (cleans up session engine, cursors, etc.)
186
+ this.commandRouter.close();
187
+
188
+ // Close storage
189
+ await this.storage.close();
190
+
191
+ return new Promise((resolve) => {
192
+ this.server!.close(async () => {
193
+ this.isRunning = false;
194
+ this.server = null;
195
+
196
+ // Clean up socket file if using Unix socket
197
+ if (this.useSocket && this.options.socketPath) {
198
+ try {
199
+ await fs.unlink(this.options.socketPath);
200
+ } catch (err: any) {
201
+ // Ignore ENOENT (file doesn't exist)
202
+ if (err.code !== 'ENOENT') {
203
+ console.error('Failed to remove socket file:', err);
204
+ }
205
+ }
206
+ }
207
+
208
+ resolve();
209
+ });
210
+ });
211
+ }
212
+
213
+ /**
214
+ * Handle a new client connection
215
+ */
216
+ private handleConnection(socket: net.Socket): void {
217
+ const connectionId = ++this.connectionIdCounter;
218
+
219
+ const state: IConnectionState = {
220
+ id: connectionId,
221
+ socket,
222
+ buffer: Buffer.alloc(0),
223
+ authenticated: true, // No auth required for now
224
+ database: 'test',
225
+ };
226
+
227
+ this.connections.set(connectionId, state);
228
+
229
+ socket.on('data', (data) => {
230
+ this.handleData(state, Buffer.isBuffer(data) ? data : Buffer.from(data));
231
+ });
232
+
233
+ socket.on('close', () => {
234
+ this.connections.delete(connectionId);
235
+ });
236
+
237
+ socket.on('error', (err) => {
238
+ // Connection errors are expected when clients disconnect
239
+ this.connections.delete(connectionId);
240
+ });
241
+ }
242
+
243
+ /**
244
+ * Handle incoming data from a client
245
+ */
246
+ private handleData(state: IConnectionState, data: Buffer): void {
247
+ // Append new data to buffer
248
+ state.buffer = Buffer.concat([state.buffer, data]);
249
+
250
+ // Process messages from buffer
251
+ this.processMessages(state);
252
+ }
253
+
254
+ /**
255
+ * Process complete messages from the buffer
256
+ */
257
+ private async processMessages(state: IConnectionState): Promise<void> {
258
+ while (state.buffer.length >= 16) {
259
+ try {
260
+ const result = WireProtocol.parseMessage(state.buffer);
261
+
262
+ if (!result) {
263
+ // Not enough data for a complete message
264
+ break;
265
+ }
266
+
267
+ const { command, bytesConsumed } = result;
268
+
269
+ // Remove processed bytes from buffer
270
+ state.buffer = state.buffer.subarray(bytesConsumed);
271
+
272
+ // Process the command
273
+ const response = await this.commandRouter.route(command);
274
+
275
+ // Encode and send response
276
+ let responseBuffer: Buffer;
277
+ if (command.opCode === OP_QUERY) {
278
+ // Legacy OP_QUERY gets OP_REPLY response
279
+ responseBuffer = WireProtocol.encodeOpReplyResponse(
280
+ command.requestID,
281
+ [response]
282
+ );
283
+ } else {
284
+ // OP_MSG gets OP_MSG response
285
+ responseBuffer = WireProtocol.encodeOpMsgResponse(
286
+ command.requestID,
287
+ response
288
+ );
289
+ }
290
+
291
+ if (!state.socket.destroyed) {
292
+ state.socket.write(responseBuffer);
293
+ }
294
+ } catch (error: any) {
295
+ // Send error response
296
+ const errorResponse = WireProtocol.encodeErrorResponse(
297
+ 0, // We don't have the requestID at this point
298
+ 1,
299
+ error.message || 'Internal error'
300
+ );
301
+
302
+ if (!state.socket.destroyed) {
303
+ state.socket.write(errorResponse);
304
+ }
305
+
306
+ // Clear buffer on parse errors to avoid infinite loops
307
+ if (error.message?.includes('opCode') || error.message?.includes('section')) {
308
+ state.buffer = Buffer.alloc(0);
309
+ }
310
+ break;
311
+ }
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Get the connection URI for this server
317
+ */
318
+ getConnectionUri(): string {
319
+ if (this.useSocket && this.options.socketPath) {
320
+ // URL-encode the socket path (replace / with %2F)
321
+ const encodedPath = encodeURIComponent(this.options.socketPath);
322
+ return `mongodb://${encodedPath}`;
323
+ }
324
+ return `mongodb://${this.options.host}:${this.options.port}`;
325
+ }
326
+
327
+ /**
328
+ * Get the socket path (if using Unix socket mode)
329
+ */
330
+ get socketPath(): string | undefined {
331
+ return this.useSocket ? this.options.socketPath : undefined;
332
+ }
333
+
334
+ /**
335
+ * Check if the server is running
336
+ */
337
+ get running(): boolean {
338
+ return this.isRunning;
339
+ }
340
+
341
+ /**
342
+ * Get the port the server is listening on
343
+ */
344
+ get port(): number {
345
+ return this.options.port;
346
+ }
347
+
348
+ /**
349
+ * Get the host the server is bound to
350
+ */
351
+ get host(): string {
352
+ return this.options.host;
353
+ }
354
+ }