@wowoengine/sawitdb 2.4.0 → 2.6.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.
Files changed (38) hide show
  1. package/README.md +86 -25
  2. package/bin/sawit-server.js +12 -1
  3. package/cli/benchmark.js +318 -0
  4. package/cli/local.js +98 -20
  5. package/cli/remote.js +65 -11
  6. package/cli/test.js +203 -0
  7. package/cli/test_security.js +140 -0
  8. package/docs/DB Event.md +32 -0
  9. package/docs/index.html +699 -336
  10. package/package.json +10 -7
  11. package/src/SawitClient.js +122 -98
  12. package/src/SawitServer.js +78 -450
  13. package/src/SawitWorker.js +55 -0
  14. package/src/WowoEngine.js +360 -449
  15. package/src/modules/BTreeIndex.js +114 -43
  16. package/src/modules/ClusterManager.js +78 -0
  17. package/src/modules/Env.js +33 -0
  18. package/src/modules/Pager.js +215 -6
  19. package/src/modules/QueryParser.js +310 -82
  20. package/src/modules/ThreadManager.js +84 -0
  21. package/src/modules/ThreadPool.js +154 -0
  22. package/src/modules/WAL.js +340 -0
  23. package/src/server/DatabaseRegistry.js +92 -0
  24. package/src/server/auth/AuthManager.js +92 -0
  25. package/src/server/router/RequestRouter.js +278 -0
  26. package/src/server/session/ClientSession.js +19 -0
  27. package/src/services/IndexManager.js +183 -0
  28. package/src/services/QueryExecutor.js +11 -0
  29. package/src/services/TableManager.js +162 -0
  30. package/src/services/event/DBEvent.js +61 -0
  31. package/src/services/event/DBEventHandler.js +39 -0
  32. package/src/services/executors/AggregateExecutor.js +153 -0
  33. package/src/services/executors/DeleteExecutor.js +134 -0
  34. package/src/services/executors/InsertExecutor.js +113 -0
  35. package/src/services/executors/SelectExecutor.js +130 -0
  36. package/src/services/executors/UpdateExecutor.js +156 -0
  37. package/src/services/logic/ConditionEvaluator.js +75 -0
  38. package/src/services/logic/JoinProcessor.js +230 -0
@@ -1,25 +1,31 @@
1
1
  const net = require('net');
2
- const SawitDB = require('./WowoEngine');
3
2
  const path = require('path');
4
3
  const fs = require('fs');
5
4
 
5
+ // Modular Components
6
+ const AuthManager = require('./server/auth/AuthManager');
7
+ const DatabaseRegistry = require('./server/DatabaseRegistry');
8
+ const RequestRouter = require('./server/router/RequestRouter');
9
+ const ClientSession = require('./server/session/ClientSession');
10
+
6
11
  /**
7
12
  * SawitDB Server - Network Database Server
8
13
  * Supports sawitdb:// protocol connections
14
+ * Refactored to use modular components.
9
15
  */
10
16
  class SawitServer {
11
17
  constructor(config = {}) {
12
- // Validate and set configuration
13
- this.port = this._validatePort(config.port || 7878);
18
+ // Configuration
19
+ this.port = this.validatePort(config.port || 7878);
14
20
  this.host = config.host || '0.0.0.0';
15
- this.dataDir = config.dataDir || path.join(__dirname, '../data');
16
- this.databases = new Map(); // Map of database name -> SawitDB instance
21
+ this.maxConnections = config.maxConnections || 100;
22
+ this.queryTimeout = config.queryTimeout || 30000;
23
+ this.logLevel = config.logLevel || 'info';
24
+
25
+ // State
26
+ this.config = config;
17
27
  this.clients = new Set();
18
28
  this.server = null;
19
- this.auth = config.auth || null; // { username: 'password' }
20
- this.maxConnections = config.maxConnections || 100;
21
- this.queryTimeout = config.queryTimeout || 30000; // 30 seconds
22
- this.logLevel = config.logLevel || 'info'; // 'debug', 'info', 'warn', 'error'
23
29
  this.stats = {
24
30
  totalConnections: 0,
25
31
  activeConnections: 0,
@@ -28,24 +34,27 @@ class SawitServer {
28
34
  startTime: Date.now()
29
35
  };
30
36
 
31
- // Ensure data directory exists
32
- if (!fs.existsSync(this.dataDir)) {
33
- fs.mkdirSync(this.dataDir, { recursive: true });
34
- }
37
+ // Initialize Managers
38
+ this.dbRegistry = new DatabaseRegistry(
39
+ config.dataDir || path.join(__dirname, '../data'),
40
+ config
41
+ );
42
+ this.authManager = new AuthManager(this);
43
+ this.router = new RequestRouter(this);
35
44
 
36
- this._log('info', `Data directory: ${this.dataDir}`);
37
- this._log('info', `Max connections: ${this.maxConnections}`);
45
+ this.log('info', `Data directory: ${this.dbRegistry.dataDir}`);
46
+ this.log('info', `Max connections: ${this.maxConnections}`);
38
47
  }
39
48
 
40
- _validatePort(port) {
41
- const p = parseInt(port);
49
+ validatePort(port) {
50
+ const p = parseInt(port, 10);
42
51
  if (isNaN(p) || p < 1 || p > 65535) {
43
52
  throw new Error(`Invalid port: ${port}. Must be between 1-65535`);
44
53
  }
45
54
  return p;
46
55
  }
47
56
 
48
- _log(level, message, data = null) {
57
+ log(level, message, data = null) {
49
58
  const levels = { debug: 0, info: 1, warn: 2, error: 3 };
50
59
  const currentLevel = levels[this.logLevel] || 1;
51
60
  const msgLevel = levels[level] || 1;
@@ -61,15 +70,22 @@ class SawitServer {
61
70
  }
62
71
 
63
72
  start() {
64
- this.server = net.createServer((socket) => this._handleConnection(socket));
73
+ this.server = net.createServer((socket) => this.handleConnection(socket));
65
74
 
66
75
  this.server.listen(this.port, this.host, () => {
67
- console.log(`╔══════════════════════════════════════════════════╗`);
68
- console.log(`║ 🌴 SawitDB Server - Version 2.4 ║`);
69
- console.log(`╚══════════════════════════════════════════════════╝`);
70
- console.log(`[Server] Listening on ${this.host}:${this.port}`);
71
- console.log(`[Server] Protocol: sawitdb://${this.host}:${this.port}/[database]`);
72
- console.log(`[Server] Ready to accept connections...`);
76
+ const cluster = require('cluster');
77
+ const prefix = cluster.isWorker ? `[Worker ${cluster.worker.id}]` : '[Server]';
78
+
79
+ if (!cluster.isWorker) {
80
+ console.log(`╔══════════════════════════════════════════════════╗`);
81
+ console.log(`║ 🌴 SawitDB Server - Version 2.6.0 ║`);
82
+ console.log(`╚══════════════════════════════════════════════════╝`);
83
+ }
84
+ console.log(`${prefix} Listening on ${this.host}:${this.port}`);
85
+ console.log(
86
+ `${prefix} Protocol: sawitdb://${this.host}:${this.port}/[database]`
87
+ );
88
+ console.log(`${prefix} Ready to accept connections...`);
73
89
  });
74
90
 
75
91
  this.server.on('error', (err) => {
@@ -82,9 +98,12 @@ class SawitServer {
82
98
 
83
99
  // Close all client connections
84
100
  for (const client of this.clients) {
85
- client.destroy();
101
+ client.end();
86
102
  }
87
103
 
104
+ // Close all open databases
105
+ this.dbRegistry.closeAll();
106
+
88
107
  // Close server
89
108
  if (this.server) {
90
109
  this.server.close(() => {
@@ -93,12 +112,11 @@ class SawitServer {
93
112
  }
94
113
  }
95
114
 
96
- _handleConnection(socket) {
115
+ handleConnection(socket) {
97
116
  const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
98
117
 
99
- // Check connection limit
100
118
  if (this.clients.size >= this.maxConnections) {
101
- this._log('warn', `Connection limit reached. Rejecting ${clientId}`);
119
+ this.log('warn', `Connection limit reached. Rejecting ${clientId}`);
102
120
  socket.write(JSON.stringify({
103
121
  type: 'error',
104
122
  error: 'Server connection limit reached. Please try again later.'
@@ -107,30 +125,27 @@ class SawitServer {
107
125
  return;
108
126
  }
109
127
 
110
- this._log('info', `Client connected: ${clientId}`);
128
+ this.log('info', `Client connected: ${clientId}`);
111
129
  this.stats.totalConnections++;
112
130
  this.stats.activeConnections++;
113
-
114
131
  this.clients.add(socket);
115
- socket.clientId = clientId;
116
- socket.connectedAt = Date.now();
117
132
 
118
- let authenticated = !this.auth; // If no auth required, auto-authenticate
119
- let currentDatabase = null;
133
+ // Attach session to socket for cleanup if needed, but best to keep separate
134
+ // Just use local var for session, pass to router
135
+ const session = new ClientSession(socket, clientId);
136
+
120
137
  let buffer = '';
121
138
 
122
139
  socket.on('data', (data) => {
123
140
  buffer += data.toString();
124
141
 
125
- // Prevent buffer overflow attacks
126
142
  if (buffer.length > 1048576) { // 1MB limit
127
- this._log('warn', `Buffer overflow attempt from ${clientId}`);
128
- this._sendError(socket, 'Request too large');
143
+ this.log('warn', `Buffer overflow attempt from ${clientId}`);
144
+ this.sendError(socket, 'Request too large');
129
145
  socket.destroy();
130
146
  return;
131
147
  }
132
148
 
133
- // Process complete messages (delimited by newline)
134
149
  let newlineIndex;
135
150
  while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
136
151
  const message = buffer.substring(0, newlineIndex);
@@ -138,113 +153,62 @@ class SawitServer {
138
153
 
139
154
  try {
140
155
  const request = JSON.parse(message);
141
- this._log('debug', `Request from ${clientId}`, request);
142
-
143
- // Handle with timeout
144
- this._handleRequest(socket, request, {
145
- authenticated,
146
- currentDatabase,
147
- setAuth: (val) => { authenticated = val; },
148
- setDatabase: (db) => { currentDatabase = db; }
149
- });
156
+ this.log('debug', `Request from ${clientId}`, request);
157
+
158
+ this.router.handle(socket, request, session);
150
159
  } catch (err) {
151
- this._log('error', `Invalid request from ${clientId}: ${err.message}`);
160
+ this.log('error', `Invalid request from ${clientId}: ${err.message}`);
152
161
  this.stats.errors++;
153
- this._sendError(socket, `Invalid request format: ${err.message}`);
162
+ this.sendError(socket, `Invalid request format: ${err.message}`);
154
163
  }
155
164
  }
156
165
  });
157
166
 
158
167
  socket.on('end', () => {
159
- const duration = Date.now() - (socket.connectedAt || Date.now());
160
- this._log('info', `Client disconnected: ${clientId} (duration: ${duration}ms)`);
168
+ const duration = Date.now() - session.connectedAt;
169
+ this.log('info', `Client disconnected: ${clientId} (duration: ${duration}ms)`);
161
170
  this.clients.delete(socket);
162
171
  this.stats.activeConnections--;
163
172
  });
164
173
 
165
174
  socket.on('error', (err) => {
166
- this._log('error', `Client error: ${clientId} - ${err.message}`);
175
+ this.log('error', `Client error: ${clientId} - ${err.message}`);
167
176
  this.clients.delete(socket);
168
177
  this.stats.activeConnections--;
169
178
  this.stats.errors++;
170
179
  });
171
180
 
172
181
  socket.on('timeout', () => {
173
- this._log('warn', `Client timeout: ${clientId}`);
182
+ this.log('warn', `Client timeout: ${clientId}`);
174
183
  socket.destroy();
175
184
  });
176
185
 
177
- // Set socket timeout
178
186
  socket.setTimeout(this.queryTimeout);
179
187
 
180
- // Send welcome message
181
- this._sendResponse(socket, {
188
+ this.sendResponse(socket, {
182
189
  type: 'welcome',
183
190
  message: 'SawitDB Server',
184
- version: '2.4',
191
+ version: '2.6.0',
185
192
  protocol: 'sawitdb'
186
193
  });
187
194
  }
188
195
 
189
- _handleRequest(socket, request, context) {
190
- const { type, payload } = request;
191
-
192
- // Authentication check
193
- if (this.auth && !context.authenticated && type !== 'auth') {
194
- return this._sendError(socket, 'Authentication required');
195
- }
196
-
197
- switch (type) {
198
- case 'auth':
199
- this._handleAuth(socket, payload, context);
200
- break;
201
-
202
- case 'use':
203
- this._handleUseDatabase(socket, payload, context);
204
- break;
205
-
206
- case 'query':
207
- this._handleQuery(socket, payload, context);
208
- break;
209
-
210
- case 'ping':
211
- this._sendResponse(socket, { type: 'pong', timestamp: Date.now() });
212
- break;
213
-
214
- case 'list_databases':
215
- this._handleListDatabases(socket);
216
- break;
217
-
218
- case 'drop_database':
219
- this._handleDropDatabase(socket, payload, context);
220
- break;
221
-
222
- case 'stats':
223
- this._handleStats(socket);
224
- break;
225
-
226
- default:
227
- this._sendError(socket, `Unknown request type: ${type}`);
196
+ sendResponse(socket, data) {
197
+ try {
198
+ if (socket.writable) {
199
+ socket.write(JSON.stringify(data) + '\n');
200
+ }
201
+ } catch (err) {
202
+ console.error('[Server] Failed to send response:', err.message);
228
203
  }
229
204
  }
230
205
 
231
- _handleStats(socket) {
232
- const uptime = Date.now() - this.stats.startTime;
233
- const stats = {
234
- ...this.stats,
235
- uptime,
236
- uptimeFormatted: this._formatUptime(uptime),
237
- databases: this.databases.size,
238
- memoryUsage: process.memoryUsage()
239
- };
240
-
241
- this._sendResponse(socket, {
242
- type: 'stats',
243
- stats
244
- });
206
+ sendError(socket, message) {
207
+ this.sendResponse(socket, { type: 'error', error: message });
245
208
  }
246
209
 
247
- _formatUptime(ms) {
210
+ // Utiliy for Stats
211
+ formatUptime(ms) {
248
212
  const seconds = Math.floor(ms / 1000);
249
213
  const minutes = Math.floor(seconds / 60);
250
214
  const hours = Math.floor(minutes / 60);
@@ -255,348 +219,12 @@ class SawitServer {
255
219
  if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
256
220
  return `${seconds}s`;
257
221
  }
258
-
259
- _handleAuth(socket, payload, context) {
260
- const { username, password } = payload;
261
-
262
- if (!this.auth) {
263
- context.setAuth(true);
264
- return this._sendResponse(socket, { type: 'auth_success', message: 'No authentication required' });
265
- }
266
-
267
- if (this.auth[username] === password) {
268
- context.setAuth(true);
269
- this._sendResponse(socket, { type: 'auth_success', message: 'Authentication successful' });
270
- } else {
271
- this._sendError(socket, 'Invalid credentials');
272
- }
273
- }
274
-
275
- _handleUseDatabase(socket, payload, context) {
276
- const { database } = payload;
277
-
278
- if (!database || typeof database !== 'string') {
279
- return this._sendError(socket, 'Invalid database name');
280
- }
281
-
282
- // Validate database name (alphanumeric, underscore, dash)
283
- if (!/^[a-zA-Z0-9_-]+$/.test(database)) {
284
- return this._sendError(socket, 'Database name can only contain letters, numbers, underscore, and dash');
285
- }
286
-
287
- try {
288
- const db = this._getOrCreateDatabase(database);
289
- context.setDatabase(database);
290
- this._sendResponse(socket, {
291
- type: 'use_success',
292
- database,
293
- message: `Switched to database '${database}'`
294
- });
295
- } catch (err) {
296
- this._sendError(socket, `Failed to use database: ${err.message}`);
297
- }
298
- }
299
-
300
- _handleQuery(socket, payload, context) {
301
- const { query, params } = payload;
302
- const startTime = Date.now();
303
-
304
- // --- Intercept Server-Level Commands (Wilayah Management) ---
305
-
306
- const qUpper = query.trim().toUpperCase();
307
-
308
- // 1. LIHAT WILAYAH
309
- if (qUpper === 'LIHAT WILAYAH') {
310
- try {
311
- const databases = fs.readdirSync(this.dataDir)
312
- .filter(file => file.endsWith('.sawit'));
313
-
314
- // Format nicely as a table-like string or JSON
315
- const list = databases.map(f => `- ${f.replace('.sawit', '')}`).join('\n');
316
- const result = `Daftar Wilayah:\n${list}`;
317
-
318
- return this._sendResponse(socket, {
319
- type: 'query_result',
320
- result,
321
- query,
322
- executionTime: Date.now() - startTime
323
- });
324
- } catch (err) {
325
- return this._sendError(socket, `Gagal melihat wilayah: ${err.message}`);
326
- }
327
- }
328
-
329
- // 2. BUKA WILAYAH [nama]
330
- if (qUpper.startsWith('BUKA WILAYAH')) {
331
- const parts = query.trim().split(/\s+/);
332
- if (parts.length < 3) {
333
- return this._sendError(socket, 'Syntax: BUKA WILAYAH [nama_wilayah]');
334
- }
335
- const dbName = parts[2];
336
- // Reuse logic from internal handler if possible, otherwise implement here
337
- // Implementing directly to match the "create empty sawit file" logic
338
- try {
339
- // Validation
340
- if (!/^[a-zA-Z0-9_-]+$/.test(dbName)) {
341
- return this._sendError(socket, 'Nama wilayah hanya boleh huruf, angka, _ dan -');
342
- }
343
-
344
- const dbPath = path.join(this.dataDir, `${dbName}.sawit`);
345
- if (fs.existsSync(dbPath)) {
346
- // It's technically fine if it exists, just say it's opened/available
347
- // But strict "Create" usually expects new. Let's follow "Open/Create" semantics of key-value stores or similar
348
- // Design doc says: "Membuat file ... kosong".
349
- // If exists, let's just say it exists.
350
- return this._sendResponse(socket, {
351
- type: 'query_result',
352
- result: `Wilayah '${dbName}' sudah ada.`,
353
- query,
354
- executionTime: Date.now() - startTime
355
- });
356
- }
357
-
358
- // Create empty file (via SawitDB constructor which initializes it)
359
- new SawitDB(dbPath);
360
-
361
- return this._sendResponse(socket, {
362
- type: 'query_result',
363
- result: `Wilayah '${dbName}' berhasil dibuka.`,
364
- query,
365
- executionTime: Date.now() - startTime
366
- });
367
- } catch (err) {
368
- return this._sendError(socket, `Gagal membuka wilayah: ${err.message}`);
369
- }
370
- }
371
-
372
- // 3. MASUK WILAYAH [nama]
373
- if (qUpper.startsWith('MASUK WILAYAH')) {
374
- const parts = query.trim().split(/\s+/);
375
- if (parts.length < 3) {
376
- return this._sendError(socket, 'Syntax: MASUK WILAYAH [nama_wilayah]');
377
- }
378
- const dbName = parts[2];
379
-
380
- // Allow "MASUK WILAYAH DEFAULT" case-insensitive match for file? No, usually case sensitive filesystems.
381
- // But let's assume case-sensitive for ID.
382
-
383
- const dbPath = path.join(this.dataDir, `${dbName}.sawit`);
384
- if (!fs.existsSync(dbPath)) {
385
- return this._sendError(socket, `Wilayah '${dbName}' tidak ditemukan.`);
386
- }
387
-
388
- context.setDatabase(dbName);
389
- return this._sendResponse(socket, {
390
- type: 'query_result', // Return as query result so CLI prints it normally
391
- result: `Selamat datang di wilayah '${dbName}'.`,
392
- query,
393
- executionTime: Date.now() - startTime
394
- });
395
- }
396
-
397
- // 4. BAKAR WILAYAH [nama]
398
- if (qUpper.startsWith('BAKAR WILAYAH')) {
399
- const parts = query.trim().split(/\s+/);
400
- if (parts.length < 3) {
401
- return this._sendError(socket, 'Syntax: BAKAR WILAYAH [nama_wilayah]');
402
- }
403
- const dbName = parts[2];
404
-
405
- // Reuse existing logic
406
- const payload = { database: dbName };
407
- // We need to adapt the existing _handleDropDatabase to generic usage or call it.
408
- // _handleDropDatabase sends its own response. We want query_result format preferably for consistency in CLI?
409
- // The existing _handleDropDatabase sends 'drop_success'. CLI might not print that as a query result string.
410
- // Let's reimplement logic here for 'query' flow to ensure 'query_result' type.
411
-
412
- try {
413
- const dbPath = path.join(this.dataDir, `${dbName}.sawit`);
414
- if (!fs.existsSync(dbPath)) {
415
- return this._sendError(socket, `Wilayah '${dbName}' tidak ditemukan.`);
416
- }
417
-
418
- if (this.databases.has(dbName)) {
419
- this.databases.delete(dbName);
420
- }
421
-
422
- try { fs.unlinkSync(dbPath); } catch (e) { }
423
-
424
- if (context.currentDatabase === dbName) {
425
- context.setDatabase(null);
426
- }
427
-
428
- return this._sendResponse(socket, {
429
- type: 'query_result',
430
- result: `Wilayah '${dbName}' telah hangus terbakar.`,
431
- query,
432
- executionTime: Date.now() - startTime
433
- });
434
-
435
- } catch (err) {
436
- return this._sendError(socket, `Gagal membakar wilayah: ${err.message}`);
437
- }
438
- }
439
-
440
- // --- Generic Syntax Aliases (Server Level) ---
441
-
442
- // 5. CREATE DATABASE [name] -> BUKA WILAYAH
443
- if (qUpper.startsWith('CREATE DATABASE')) {
444
- const parts = query.trim().split(/\s+/);
445
- if (parts.length < 3) return this._sendError(socket, 'Syntax: CREATE DATABASE [name]');
446
- // Recruit BUKA WILAYAH logic
447
- const newQuery = `BUKA WILAYAH ${parts[2]}`;
448
- return this._handleQuery(socket, { ...payload, query: newQuery }, context);
449
- }
450
-
451
- // 6. USE [name] -> MASUK WILAYAH
452
- if (qUpper.startsWith('USE ')) {
453
- const parts = query.trim().split(/\s+/);
454
- if (parts.length < 2) return this._sendError(socket, 'Syntax: USE [name]');
455
- // Recruit MASUK WILAYAH logic
456
- const newQuery = `MASUK WILAYAH ${parts[1]}`;
457
- return this._handleQuery(socket, { ...payload, query: newQuery }, context);
458
- }
459
-
460
- // 7. SHOW DATABASES -> LIHAT WILAYAH
461
- if (qUpper === 'SHOW DATABASES') {
462
- const newQuery = `LIHAT WILAYAH`;
463
- return this._handleQuery(socket, { ...payload, query: newQuery }, context);
464
- }
465
-
466
- // 8. DROP DATABASE [name] -> BAKAR WILAYAH
467
- if (qUpper.startsWith('DROP DATABASE')) {
468
- const parts = query.trim().split(/\s+/);
469
- if (parts.length < 3) return this._sendError(socket, 'Syntax: DROP DATABASE [name]');
470
- // Recruit BAKAR WILAYAH logic
471
- const newQuery = `BAKAR WILAYAH ${parts[2]}`;
472
- return this._handleQuery(socket, { ...payload, query: newQuery }, context);
473
- }
474
-
475
- // --- End Intercept ---
476
-
477
- if (!context.currentDatabase) {
478
- return this._sendError(socket, 'Anda belum masuk wilayah manapun. Gunakan: MASUK WILAYAH [nama]');
479
- }
480
-
481
- try {
482
- const db = this._getOrCreateDatabase(context.currentDatabase);
483
- const result = db.query(query, params);
484
- const duration = Date.now() - startTime;
485
-
486
- this.stats.totalQueries++;
487
- this._log('debug', `Query executed in ${duration}ms: ${query.substring(0, 50)}...`);
488
-
489
- this._sendResponse(socket, {
490
- type: 'query_result',
491
- result,
492
- query,
493
- executionTime: duration
494
- });
495
- } catch (err) {
496
- this._log('error', `Query failed: ${err.message} - Query: ${query}`);
497
- this.stats.errors++;
498
- this._sendError(socket, `Query error: ${err.message}`);
499
- }
500
- }
501
-
502
- _handleListDatabases(socket) {
503
- try {
504
- const databases = fs.readdirSync(this.dataDir)
505
- .filter(file => file.endsWith('.sawit'))
506
- .map(file => file.replace('.sawit', ''));
507
-
508
- this._sendResponse(socket, {
509
- type: 'database_list',
510
- databases,
511
- count: databases.length
512
- });
513
- } catch (err) {
514
- this._sendError(socket, `Failed to list databases: ${err.message}`);
515
- }
516
- }
517
-
518
- _handleDropDatabase(socket, payload, context) {
519
- const { database } = payload;
520
-
521
- if (!database) {
522
- return this._sendError(socket, 'Database name required');
523
- }
524
-
525
- try {
526
- const dbPath = path.join(this.dataDir, `${database}.sawit`);
527
-
528
- if (!fs.existsSync(dbPath)) {
529
- return this._sendError(socket, `Database '${database}' does not exist`);
530
- }
531
-
532
- // Close database if open
533
- if (this.databases.has(database)) {
534
- this.databases.delete(database);
535
- }
536
-
537
- // Delete file
538
- fs.unlinkSync(dbPath);
539
-
540
- // Clear current database if it was dropped
541
- if (context.currentDatabase === database) {
542
- context.setDatabase(null);
543
- }
544
-
545
- this._sendResponse(socket, {
546
- type: 'drop_success',
547
- database,
548
- message: `Database '${database}' has been burned (dropped)`
549
- });
550
- } catch (err) {
551
- this._sendError(socket, `Failed to drop database: ${err.message}`);
552
- }
553
- }
554
-
555
- _getOrCreateDatabase(name) {
556
- if (!this.databases.has(name)) {
557
- const dbPath = path.join(this.dataDir, `${name}.sawit`);
558
- const db = new SawitDB(dbPath);
559
- this.databases.set(name, db);
560
- }
561
- return this.databases.get(name);
562
- }
563
-
564
- _sendResponse(socket, data) {
565
- try {
566
- socket.write(JSON.stringify(data) + '\n');
567
- } catch (err) {
568
- console.error('[Server] Failed to send response:', err.message);
569
- }
570
- }
571
-
572
- _sendError(socket, message) {
573
- this._sendResponse(socket, { type: 'error', error: message });
574
- }
575
222
  }
576
223
 
577
224
  module.exports = SawitServer;
578
225
 
579
226
  // Allow running as standalone server
580
227
  if (require.main === module) {
581
- const server = new SawitServer({
582
- port: process.env.SAWIT_PORT || 7878,
583
- host: process.env.SAWIT_HOST || '0.0.0.0',
584
- dataDir: process.env.SAWIT_DATA_DIR || path.join(__dirname, '../data'),
585
- // Optional auth: { username: 'petani', password: 'sawit123' }
586
- });
587
-
588
- server.start();
589
-
590
- // Graceful shutdown
591
- process.on('SIGINT', () => {
592
- console.log('\n[Server] Received SIGINT, shutting down gracefully...');
593
- server.stop();
594
- process.exit(0);
595
- });
596
-
597
- process.on('SIGTERM', () => {
598
- console.log('\n[Server] Received SIGTERM, shutting down gracefully...');
599
- server.stop();
600
- process.exit(0);
601
- });
228
+ const ClusterManager = require('./modules/ClusterManager');
229
+ ClusterManager.start(SawitServer);
602
230
  }