blaze-aura69 commited on
Commit
cae8d9e
·
verified ·
1 Parent(s): e38da2c

Update index.js

Browse files
Files changed (1) hide show
  1. index.js +503 -575
index.js CHANGED
@@ -3,186 +3,162 @@ const { Worker: ThreadWorker, isMainThread, parentPort, workerData } = require('
3
 
4
  // ============ DATABASE WORKER THREAD ============
5
  if (!isMainThread) {
6
- const Database = require('better-sqlite3');
7
- const path = require('path');
8
- const fs = require('fs');
9
-
10
- const DATA_DIR = workerData.dataDir || "/data";
11
- if (!fs.existsSync(DATA_DIR)) {
12
- fs.mkdirSync(DATA_DIR, { recursive: true });
13
- }
14
-
15
- const dbPath = path.join(DATA_DIR, 'users.db');
16
- const db = new Database(dbPath);
17
-
18
- db.pragma('journal_mode = WAL');
19
- db.pragma('synchronous = OFF');
20
- db.pragma('cache_size = -2000000');
21
- db.pragma('temp_store = MEMORY');
22
- db.pragma('mmap_size = 268435456');
23
-
24
- db.exec(`
25
- CREATE TABLE IF NOT EXISTS users (
26
- id TEXT PRIMARY KEY,
27
- username TEXT UNIQUE NOT NULL,
28
- password_hash TEXT NOT NULL,
29
- name TEXT NOT NULL,
30
- created_at TEXT NOT NULL,
31
- is_login INTEGER DEFAULT 0,
32
- bio TEXT DEFAULT '',
33
- pic_url TEXT DEFAULT '',
34
- date_of_birth TEXT DEFAULT '',
35
- is_profile_public TEXT DEFAULT 'true'
36
- )
37
- `);
38
-
39
- db.exec(`CREATE INDEX IF NOT EXISTS idx_username ON users(username);`);
40
-
41
- const stmts = {
42
- getAllUsers: db.prepare('SELECT * FROM users'),
43
- getUserById: db.prepare('SELECT * FROM users WHERE id = ?'),
44
- getUserByUsername: db.prepare('SELECT * FROM users WHERE username = ?'),
45
- insertUser: db.prepare(`
46
- INSERT INTO users (
47
- id, username, password_hash, name, created_at, is_login,
48
- bio, pic_url, date_of_birth, is_profile_public
49
- ) VALUES (
50
- @id, @username, @password_hash, @name, @created_at, @is_login,
51
- @bio, @pic_url, @date_of_birth, @is_profile_public
52
- )
53
- `),
54
- updateLogin: db.prepare('UPDATE users SET is_login = ? WHERE id = ?'),
55
- updateProfileWithUsername: db.prepare(`
56
- UPDATE users SET
57
- username = @username,
58
- name = @name,
59
- bio = @bio,
60
- date_of_birth = @date_of_birth,
61
- is_profile_public = @is_profile_public
62
- WHERE id = @id
63
- `),
64
- updatePhoto: db.prepare('UPDATE users SET pic_url = ? WHERE id = ?'),
65
- getCount: db.prepare('SELECT COUNT(*) as count FROM users')
66
- };
67
-
68
- parentPort.on('message', async (message) => {
69
- const { id, action, data } = message;
70
- try {
71
- let result;
72
- switch (action) {
73
- case 'getAllUsers':
74
- result = stmts.getAllUsers.all();
75
- break;
76
- case 'getUserById':
77
- result = stmts.getUserById.get(data.userId);
78
- break;
79
- case 'getUserByUsername':
80
- result = stmts.getUserByUsername.get(data.username);
81
- break;
82
- case 'insertUser':
83
- stmts.insertUser.run(data.user);
84
- result = { success: true };
85
- break;
86
- case 'updateLogin':
87
- stmts.updateLogin.run(data.isLogin ? 1 : 0, data.userId);
88
- result = { success: true };
89
- break;
90
- case 'updateProfile':
91
- if (data.username) {
92
- stmts.updateProfileWithUsername.run({
93
- id: data.userId,
94
- username: data.username,
95
- name: data.name,
96
- bio: data.bio,
97
- date_of_birth: data.date_of_birth,
98
- is_profile_public: data.is_profile_public
99
- });
100
- } else {
101
- stmts.updateProfile.run({
102
- id: data.userId,
103
- name: data.name,
104
- bio: data.bio,
105
- date_of_birth: data.date_of_birth,
106
- is_profile_public: data.is_profile_public
107
- });
108
  }
109
- result = { success: true };
110
- break;
111
- case 'updatePhoto':
112
- stmts.updatePhoto.run(data.picUrl, data.userId);
113
- result = { success: true };
114
- break;
115
- case 'batchOperation':
116
- result = processBatch(data.operations);
117
- break;
118
- case 'getCount':
119
- result = stmts.getCount.get().count;
120
- break;
121
- default:
122
- throw new Error(`Unknown action: ${action}`);
123
- }
124
- parentPort.postMessage({ id, success: true, result });
125
- } catch (error) {
126
- parentPort.postMessage({ id, success: false, error: error.message });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  }
 
128
  });
129
-
130
- function processBatch(operations) {
131
- const transaction = db.transaction(() => {
132
- for (const op of operations) {
133
- switch (op.action) {
134
- case 'signup':
135
- try {
136
- stmts.insertUser.run(op.user);
137
- } catch (e) {}
138
- break;
139
- case 'login':
140
- stmts.updateLogin.run(1, op.userId);
141
- break;
142
- case 'logout':
143
- stmts.updateLogin.run(0, op.userId);
144
- break;
145
- case 'updateProfile':
146
- if (op.data && op.data.username) {
147
- stmts.updateProfileWithUsername.run({
148
- id: op.data.id,
149
- username: op.data.username,
150
- name: op.data.name,
151
- bio: op.data.bio,
152
- date_of_birth: op.data.date_of_birth,
153
- is_profile_public: op.data.is_profile_public
154
- });
155
- } else {
156
- stmts.updateProfile.run({
157
- id: op.data.id,
158
- name: op.data.name,
159
- bio: op.data.bio,
160
- date_of_birth: op.data.date_of_birth,
161
- is_profile_public: op.data.is_profile_public
162
- });
163
  }
164
- break;
165
- case 'updatePhoto':
166
- stmts.updatePhoto.run(op.picUrl, op.userId);
167
- break;
168
- }
169
- }
170
- });
171
- transaction();
172
- return { processed: operations.length };
173
- }
174
-
175
- return;
176
  }
177
 
178
  // ============ MAIN THREAD ============
179
- const fastify = require('fastify')({
180
- logger: false,
181
- bodyLimit: 1048576,
182
- keepAliveTimeout: 61000,
183
- connectionTimeout: 61000,
184
- maxRequestsPerSocket: 0,
185
- requestTimeout: 30000
186
  });
187
 
188
  const cors = require('@fastify/cors');
@@ -194,9 +170,7 @@ const fsp = require('fs').promises;
194
  const path = require('path');
195
 
196
  let sharp;
197
- try {
198
- sharp = require('sharp');
199
- } catch (e) {}
200
 
201
  const MAX_BUCKET_SIZE = 60 * 1024 * 1024 * 1024;
202
  const MAX_USERS = 30000;
@@ -223,81 +197,79 @@ const FULL_FILE = path.join(STATUS_DIR, "full.txt");
223
  const SIZE_FILE = path.join(STATUS_DIR, "size.txt");
224
 
225
  [DATA_DIR, PHOTOS_DIR, STATUS_DIR].forEach(dir => {
226
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
227
  });
228
 
229
  if (!fs.existsSync(FULL_FILE)) fs.writeFileSync(FULL_FILE, '', 'utf8');
230
  if (!fs.existsSync(SIZE_FILE)) fs.writeFileSync(SIZE_FILE, '', 'utf8');
231
 
232
  async function getFullSpaces() {
233
- try {
234
- const content = await fsp.readFile(FULL_FILE, 'utf8');
235
- return content.trim() ? content.trim().split('|') : [];
236
- } catch (e) {
237
- return [];
238
- }
239
  }
240
 
241
  async function addSpaceToFull() {
242
- try {
243
- let content = await fsp.readFile(FULL_FILE, 'utf8').catch(() => '');
244
- content = content.trim();
245
- let spaces = content ? content.split('|') : [];
246
- if (!spaces.includes(SPACE_NAME)) {
247
- spaces.push(SPACE_NAME);
248
- await fsp.writeFile(FULL_FILE, spaces.join('|'), 'utf8');
249
- }
250
- } catch (e) {}
251
  }
252
 
253
  async function removeSpaceFromFull() {
254
- try {
255
- let content = await fsp.readFile(FULL_FILE, 'utf8').catch(() => '');
256
- content = content.trim();
257
- let spaces = content ? content.split('|') : [];
258
- const newSpaces = spaces.filter(s => s !== SPACE_NAME);
259
- if (newSpaces.length !== spaces.length) {
260
- await fsp.writeFile(FULL_FILE, newSpaces.join('|'), 'utf8');
261
- }
262
- } catch (e) {}
263
  }
264
 
265
  async function addSpaceToSize() {
266
- try {
267
- let content = await fsp.readFile(SIZE_FILE, 'utf8').catch(() => '');
268
- content = content.trim();
269
- let spaces = content ? content.split('|') : [];
270
- if (!spaces.includes(SPACE_NAME)) {
271
- spaces.push(SPACE_NAME);
272
- await fsp.writeFile(SIZE_FILE, spaces.join('|'), 'utf8');
273
- }
274
- } catch (e) {}
275
  }
276
 
277
  function checkRPSStatus() {
278
- const now = Date.now();
279
- if (now - rpsWindowStart >= 1000) {
280
- if (currentRPS >= MAX_RPS * 0.9 && !isSpaceFull_RPS) {
281
- isSpaceFull_RPS = true;
282
- addSpaceToFull();
283
- } else if (currentRPS < MAX_RPS * 0.7 && isSpaceFull_RPS) {
284
- isSpaceFull_RPS = false;
285
- removeSpaceFromFull();
286
- }
287
- currentRPS = 0;
288
- rpsWindowStart = now;
289
  }
 
 
 
290
  }
291
 
292
  async function checkUserCountStatus() {
293
- try {
294
- const count = await dbRequest('getCount');
295
- if (count >= MAX_USERS && !isSpaceFull_Users) {
296
- isSpaceFull_Users = true;
297
- await addSpaceToSize();
298
- await addSpaceToFull();
299
- }
300
- } catch (e) {}
301
  }
302
 
303
  let dbWorker;
@@ -305,408 +277,364 @@ let dbRequestId = 0;
305
  const dbPromises = new Map();
306
 
307
  function initDBWorker() {
308
- return new Promise((resolve, reject) => {
309
- dbWorker = new ThreadWorker(__filename, {
310
- workerData: { dataDir: DATA_DIR }
311
- });
312
- dbWorker.on('message', (message) => {
313
- const { id, success, result, error } = message;
314
- const promise = dbPromises.get(id);
315
- if (promise) {
316
- dbPromises.delete(id);
317
- if (success) promise.resolve(result);
318
- else promise.reject(new Error(error));
319
- }
320
- });
321
- dbWorker.on('error', reject);
322
- dbWorker.on('online', resolve);
323
  });
 
 
 
324
  }
325
 
326
  function dbRequest(action, data = {}) {
327
- return new Promise((resolve, reject) => {
328
- const id = ++dbRequestId;
329
- dbPromises.set(id, { resolve, reject });
330
- dbWorker.postMessage({ id, action, data });
331
- });
332
  }
333
 
334
  class FastRateLimiter {
335
- constructor(maxRequests = 15000, windowMs = 1000) {
336
- this.maxRequests = maxRequests;
337
- this.windowMs = windowMs;
338
- this.requests = 0;
339
- this.windowStart = Date.now();
340
- }
341
-
342
- isAllowed() {
343
- const now = Date.now();
344
- if (now - this.windowStart >= this.windowMs) {
345
- this.requests = 0;
346
- this.windowStart = now;
347
- }
348
- if (this.requests >= this.maxRequests) return false;
349
- this.requests++;
350
- currentRPS++;
351
- return true;
352
  }
 
 
 
 
 
353
  }
354
 
355
  const rateLimiter = new FastRateLimiter(15000, 1000);
356
 
357
  function hashPassword(password) {
358
- return crypto.createHash('sha256').update(HASH + password + HASH).digest('hex');
359
  }
360
 
361
  function verifyPassword(password, hash) {
362
- return hashPassword(password) === hash;
363
  }
364
 
365
  function generateId() {
366
- return crypto.randomBytes(16).toString('base64url').substring(0, 21);
367
  }
368
 
369
  function getBucketSize() {
370
- let totalSize = 0;
371
- try {
372
- const files = fs.readdirSync(PHOTOS_DIR);
373
- for (const file of files) {
374
- try {
375
- totalSize += fs.statSync(path.join(PHOTOS_DIR, file)).size;
376
- } catch (e) {}
377
- }
378
- try {
379
- totalSize += fs.statSync(path.join(DATA_DIR, 'users.db')).size;
380
- } catch (e) {}
381
- } catch (e) {}
382
- return totalSize;
383
  }
384
 
385
  async function compressToWebp(fileData, maxSizeKb = 20) {
386
- if (!sharp) return fileData;
387
- try {
388
- let image = sharp(fileData);
389
- const metadata = await image.metadata();
390
- const currentSizeKb = fileData.length / 1024;
391
- if (metadata.format === 'webp' && currentSizeKb <= maxSizeKb) return fileData;
392
-
393
- let quality = 80;
394
- let outputBuffer;
395
- while (quality >= 10) {
396
- outputBuffer = await image
397
- .resize(800, 800, { fit: 'inside', withoutEnlargement: true })
398
- .webp({ quality })
399
- .toBuffer();
400
- if (outputBuffer.length / 1024 <= maxSizeKb) break;
401
- quality -= 10;
402
- }
403
-
404
- if (outputBuffer.length / 1024 > maxSizeKb) {
405
- outputBuffer = await image
406
- .resize(400, 400, { fit: 'inside', withoutEnlargement: true })
407
- .webp({ quality: 30 })
408
- .toBuffer();
409
- }
410
- return outputBuffer;
411
- } catch (e) {
412
- return fileData;
413
  }
 
 
414
  }
415
 
416
  async function setupFastify() {
417
- await fastify.register(cors, { origin: '*' });
418
- await fastify.register(multipart, { limits: { fileSize: 5 * 1024 * 1024 } });
419
- await fastify.register(fastifyStatic, { root: PHOTOS_DIR, prefix: '/photos/', maxAge: 86400000 });
420
-
421
- fastify.addHook('onRequest', async (request, reply) => {
422
- if (!rateLimiter.isAllowed()) {
423
- reply.status(503).send({ error: 'rate_limit' });
424
- return;
425
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
427
 
428
- fastify.post('/signup', async (request, reply) => {
429
- const { username, password, name } = request.body || {};
430
- if (!username || !password || !name) {
431
- reply.status(400).send({ detail: 'Missing fields' });
432
- return;
433
- }
434
-
435
- const cleanUsername = username.trim().toLowerCase();
436
- if (cleanUsername.length < 3 || cleanUsername.length > MAX_USERNAME_LENGTH) {
437
- reply.status(400).send({ detail: `Username must be 3-${MAX_USERNAME_LENGTH} chars` });
438
- return;
439
- }
440
- if (password.length < 6 || password.length > MAX_PASSWORD_LENGTH) {
441
- reply.status(400).send({ detail: `Password must be 6-${MAX_PASSWORD_LENGTH} chars` });
442
- return;
443
- }
444
- if (name.trim().length > MAX_NAME_LENGTH) {
445
- reply.status(400).send({ detail: `Name max ${MAX_NAME_LENGTH} chars` });
446
- return;
447
- }
448
-
449
- // Check if username exists directly in DB
 
 
 
 
 
 
 
 
 
450
  const existingUser = await dbRequest('getUserByUsername', { username: cleanUsername });
451
  if (existingUser) {
452
- reply.status(409).send({ detail: 'Username exists' });
453
- return;
454
- }
455
-
456
- const count = await dbRequest('getCount');
457
- if (count >= MAX_USERS) {
458
- reply.status(507).send({ detail: 'Server full' });
459
- return;
460
- }
461
-
462
- const userId = generateId();
463
- const newUser = {
464
- id: userId,
465
- username: cleanUsername,
466
- password_hash: hashPassword(password),
467
- name: name.trim(),
468
- created_at: new Date().toISOString(),
469
- is_login: 0,
470
- bio: '',
471
- pic_url: '',
472
- date_of_birth: '',
473
- is_profile_public: 'true'
474
- };
475
-
476
- try {
477
- await dbRequest('insertUser', { user: newUser });
478
- totalUsers = count + 1;
479
- await checkUserCountStatus();
480
- reply.send({ success: true, user_id: userId, username: cleanUsername });
481
- } catch (e) {
482
- reply.status(500).send({ detail: 'Signup failed' });
483
  }
484
- });
 
485
 
486
- fastify.post('/login', async (request, reply) => {
487
- const { username, password } = request.body || {};
488
- if (!username || !password) {
489
- reply.status(400).send({ detail: 'Missing fields' });
490
- return;
491
- }
492
-
493
- const cleanUsername = username.trim().toLowerCase();
494
- const user = await dbRequest('getUserByUsername', { username: cleanUsername });
495
-
496
- if (!user) {
497
- reply.status(401).send({ detail: 'Invalid credentials' });
498
- return;
499
- }
500
-
501
- if (!verifyPassword(password, user.password_hash)) {
502
- reply.status(401).send({ detail: 'Invalid credentials' });
503
- return;
504
- }
505
-
506
- await dbRequest('updateLogin', { userId: user.id, isLogin: true });
507
-
508
- reply.send({
509
- success: true,
510
- id: user.id,
511
- username: user.username,
512
- name: user.name,
513
- bio: user.bio || '',
514
- pic_url: user.pic_url || '',
515
- date_of_birth: user.date_of_birth || '',
516
- is_profile_public: user.is_profile_public || 'true',
517
- created_at: user.created_at
518
- });
519
  });
520
 
521
- fastify.post('/logout', async (request, reply) => {
522
- const { id } = request.body || {};
523
- if (!id) {
524
- reply.status(400).send({ detail: 'Missing ID' });
525
- return;
526
- }
527
-
528
- const user = await dbRequest('getUserById', { userId: id });
529
- if (!user) {
530
- reply.status(404).send({ detail: 'Not found' });
531
- return;
532
- }
533
-
534
- await dbRequest('updateLogin', { userId: id, isLogin: false });
535
- reply.send({ success: true });
536
- });
537
-
538
- fastify.put('/updateprofiledetails', async (request, reply) => {
539
- const { id, username, name, bio, date_of_birth, is_profile_public } = request.body || {};
540
-
541
- if (!id) {
542
- reply.status(400).send({ detail: 'Missing ID' });
543
- return;
544
- }
545
-
546
- const user = await dbRequest('getUserById', { userId: id });
547
- if (!user) {
548
- reply.status(404).send({ detail: 'Not found' });
549
- return;
550
- }
551
-
552
- if (name && name.length > MAX_NAME_LENGTH) {
553
- reply.status(400).send({ detail: `Name max ${MAX_NAME_LENGTH} chars` });
554
- return;
555
- }
556
-
557
- if (bio && bio.length > MAX_BIO_LENGTH) {
558
- reply.status(400).send({ detail: `Bio max ${MAX_BIO_LENGTH} chars` });
559
- return;
560
- }
561
 
562
- let cleanUsername = null;
563
- if (username && username.trim() !== '') {
564
- cleanUsername = username.trim().toLowerCase();
565
-
566
- if (cleanUsername.length < 3 || cleanUsername.length > MAX_USERNAME_LENGTH) {
567
- reply.status(400).send({ detail: `Username must be 3-${MAX_USERNAME_LENGTH} chars` });
568
  return;
569
  }
570
-
571
- if (cleanUsername !== user.username) {
572
- const existingUser = await dbRequest('getUserByUsername', { username: cleanUsername });
573
- if (existingUser) {
574
- reply.status(409).send({ detail: 'Username exists' });
575
  return;
576
  }
577
- } else {
578
- cleanUsername = null;
 
 
 
 
 
 
 
 
 
 
 
 
579
  }
580
- }
581
-
582
- await dbRequest('updateProfile', {
583
- userId: id,
584
- username: cleanUsername,
585
- name: name || user.name,
586
- bio: bio !== undefined ? bio : user.bio,
587
- date_of_birth: date_of_birth || user.date_of_birth,
588
- is_profile_public: is_profile_public || user.is_profile_public
589
  });
590
 
591
- reply.send({ success: true });
592
- });
593
-
594
- fastify.post('/uploadphoto', async (request, reply) => {
595
- if (getBucketSize() >= MAX_BUCKET_SIZE) {
596
- reply.status(507).send({ detail: 'Storage full' });
597
- return;
598
- }
599
-
600
- try {
601
- const data = await request.file();
602
- if (!data || !data.mimetype.startsWith('image/')) {
603
- reply.status(400).send({ detail: 'Invalid image' });
604
- return;
605
- }
606
-
607
- const fileBuffer = await data.toBuffer();
608
- const id = (data.fields && data.fields.id) ? data.fields.id.value : null;
609
-
610
- const user = await dbRequest('getUserById', { userId: id });
611
- if (!id || !user) {
612
- reply.status(404).send({ detail: 'Not found' });
613
- return;
614
- }
615
-
616
- const compressed = await compressToWebp(fileBuffer, MAX_PHOTO_SIZE_KB);
617
- const filename = `${id}_${Math.floor(Date.now() / 1000)}.webp`;
618
- await fsp.writeFile(path.join(PHOTOS_DIR, filename), compressed);
619
-
620
- await dbRequest('updatePhoto', { userId: id, picUrl: `/photos/${filename}` });
621
- reply.send({ success: true });
622
- } catch (e) {
623
- reply.status(500).send({ detail: 'Failed' });
624
- }
625
- });
626
-
627
- fastify.get('/getuser', async (request, reply) => {
628
- const { id, username } = request.query || {};
629
- let user;
630
-
631
- if (username) {
632
- user = await dbRequest('getUserByUsername', { username: username.toLowerCase() });
633
- } else if (id) {
634
- user = await dbRequest('getUserById', { userId: id });
635
- } else {
636
- reply.status(400).send({ detail: 'Missing params' });
637
- return;
638
- }
639
-
640
- if (!user) {
641
- reply.status(404).send({ detail: 'Not found' });
642
- return;
643
- }
644
-
645
- reply.send({
646
- id: user.id,
647
- username: user.username,
648
- name: user.name,
649
- bio: user.bio || '',
650
- pic_url: user.pic_url || '',
651
- date_of_birth: user.date_of_birth || '',
652
- is_profile_public: user.is_profile_public || 'true',
653
- created_at: user.created_at,
654
- is_login: Boolean(user.is_login)
655
- });
656
  });
657
-
658
- fastify.get('/check', async (request, reply) => {
659
- const count = await dbRequest('getCount');
660
- reply.send({
661
- status: 'ok',
662
- count: count,
663
- maxUsers: MAX_USERS,
664
- isFull: count >= MAX_USERS,
665
- spaceName: SPACE_NAME
666
- });
667
  });
668
-
669
- return fastify;
 
670
  }
671
 
672
  async function start() {
673
- try {
674
- await initDBWorker();
675
-
676
- // Get initial count
677
- totalUsers = await dbRequest('getCount');
678
-
679
- await setupFastify();
680
-
681
- setInterval(() => {
682
- checkRPSStatus();
683
- }, 100);
684
-
685
- setInterval(async () => {
686
- await checkUserCountStatus();
687
- }, 5000);
688
-
689
- const port = process.env.PORT || 7860;
690
- await fastify.listen({ port, host: '0.0.0.0' });
691
- console.log(`Server running on port ${port}`);
692
- } catch (err) {
693
- console.error('Startup error:', err.message);
694
- process.exit(1);
695
- }
696
  }
697
 
698
  process.on('SIGTERM', async () => {
699
- await removeSpaceFromFull();
700
- await fastify.close();
701
- if (dbWorker) await dbWorker.terminate();
702
- process.exit(0);
703
  });
704
 
705
  process.on('SIGINT', async () => {
706
- await removeSpaceFromFull();
707
- await fastify.close();
708
- if (dbWorker) await dbWorker.terminate();
709
- process.exit(0);
710
  });
711
 
712
  start().catch(err => process.exit(1));
 
3
 
4
  // ============ DATABASE WORKER THREAD ============
5
  if (!isMainThread) {
6
+ const Database = require('better-sqlite3');
7
+ const path = require('path');
8
+ const fs = require('fs');
9
+
10
+ const DATA_DIR = workerData.dataDir || "/data";
11
+ if (!fs.existsSync(DATA_DIR)) {
12
+ fs.mkdirSync(DATA_DIR, { recursive: true });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  }
14
+
15
+ const dbPath = path.join(DATA_DIR, 'users.db');
16
+ const db = new Database(dbPath);
17
+
18
+ db.pragma('journal_mode = WAL');
19
+ db.pragma('synchronous = OFF');
20
+ db.pragma('cache_size = -2000000');
21
+ db.pragma('temp_store = MEMORY');
22
+ db.pragma('mmap_size = 268435456');
23
+
24
+ db.exec(`
25
+ CREATE TABLE IF NOT EXISTS users (
26
+ id TEXT PRIMARY KEY,
27
+ username TEXT UNIQUE NOT NULL,
28
+ password_hash TEXT NOT NULL,
29
+ name TEXT NOT NULL,
30
+ created_at TEXT NOT NULL,
31
+ is_login INTEGER DEFAULT 0,
32
+ bio TEXT DEFAULT '',
33
+ pic_url TEXT DEFAULT '',
34
+ date_of_birth TEXT DEFAULT '',
35
+ is_profile_public TEXT DEFAULT 'true'
36
+ )
37
+ `);
38
+
39
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_username ON users(username);`);
40
+
41
+ const stmts = {
42
+ getAllUsers: db.prepare('SELECT * FROM users'),
43
+ getUserById: db.prepare('SELECT * FROM users WHERE id = ?'),
44
+ getUserByUsername: db.prepare('SELECT * FROM users WHERE username = ?'),
45
+ insertUser: db.prepare(`
46
+ INSERT INTO users (
47
+ id, username, password_hash, name, created_at, is_login, bio, pic_url, date_of_birth, is_profile_public
48
+ ) VALUES (
49
+ @id, @username, @password_hash, @name, @created_at, @is_login, @bio, @pic_url, @date_of_birth, @is_profile_public
50
+ )
51
+ `),
52
+ updateLogin: db.prepare('UPDATE users SET is_login = ? WHERE id = ?'),
53
+ updateProfile: db.prepare(`
54
+ UPDATE users SET
55
+ username = COALESCE(NULLIF(@username, ''), username),
56
+ name = COALESCE(NULLIF(@name, ''), name),
57
+ bio = COALESCE(NULLIF(@bio, ''), bio),
58
+ date_of_birth = COALESCE(NULLIF(@date_of_birth, ''), date_of_birth),
59
+ is_profile_public = COALESCE(NULLIF(@is_profile_public, ''), is_profile_public)
60
+ WHERE id = @id
61
+ `),
62
+ updatePhoto: db.prepare('UPDATE users SET pic_url = ? WHERE id = ?'),
63
+ getCount: db.prepare('SELECT COUNT(*) as count FROM users')
64
+ };
65
+
66
+ parentPort.on('message', async (message) => {
67
+ const { id, action, data } = message;
68
+ try {
69
+ let result;
70
+ switch (action) {
71
+ case 'getAllUsers':
72
+ result = stmts.getAllUsers.all();
73
+ break;
74
+ case 'getUserById':
75
+ result = stmts.getUserById.get(data.userId);
76
+ break;
77
+ case 'getUserByUsername':
78
+ result = stmts.getUserByUsername.get(data.username);
79
+ break;
80
+ case 'insertUser':
81
+ stmts.insertUser.run(data.user);
82
+ result = { success: true };
83
+ break;
84
+ case 'updateLogin':
85
+ stmts.updateLogin.run(data.isLogin ? 1 : 0, data.userId);
86
+ result = { success: true };
87
+ break;
88
+ case 'updateProfile':
89
+ stmts.updateProfile.run({
90
+ id: data.userId,
91
+ username: data.username || '',
92
+ name: data.name || '',
93
+ bio: data.bio || '',
94
+ date_of_birth: data.date_of_birth || '',
95
+ is_profile_public: data.is_profile_public || 'true'
96
+ });
97
+ result = { success: true };
98
+ break;
99
+ case 'updatePhoto':
100
+ stmts.updatePhoto.run(data.picUrl, data.userId);
101
+ result = { success: true };
102
+ break;
103
+ case 'batchOperation':
104
+ result = processBatch(data.operations);
105
+ break;
106
+ case 'getCount':
107
+ result = stmts.getCount.get().count;
108
+ break;
109
+ default:
110
+ throw new Error(`Unknown action: ${action}`);
111
+ }
112
+ parentPort.postMessage({ id, success: true, result });
113
+ } catch (error) {
114
+ parentPort.postMessage({ id, success: false, error: error.message });
115
+ }
116
+ });
117
+
118
+ function processBatch(operations) {
119
+ const transaction = db.transaction(() => {
120
+ for (const op of operations) {
121
+ switch (op.action) {
122
+ case 'signup':
123
+ try { stmts.insertUser.run(op.user); } catch (e) {}
124
+ break;
125
+ case 'login':
126
+ stmts.updateLogin.run(1, op.userId);
127
+ break;
128
+ case 'logout':
129
+ stmts.updateLogin.run(0, op.userId);
130
+ break;
131
+ case 'updateProfile':
132
+ stmts.updateProfile.run({
133
+ id: op.data.id || op.data.userId,
134
+ username: op.data.username || '',
135
+ name: op.data.name || '',
136
+ bio: op.data.bio || '',
137
+ date_of_birth: op.data.date_of_birth || '',
138
+ is_profile_public: op.data.is_profile_public || 'true'
139
+ });
140
+ break;
141
+ case 'updatePhoto':
142
+ stmts.updatePhoto.run(op.picUrl, op.userId);
143
+ break;
144
  }
145
+ }
146
  });
147
+ transaction();
148
+ return { processed: operations.length };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  }
150
+
151
+ return;
 
 
 
 
 
 
 
 
 
 
152
  }
153
 
154
  // ============ MAIN THREAD ============
155
+ const fastify = require('fastify')({
156
+ logger: false,
157
+ bodyLimit: 1048576,
158
+ keepAliveTimeout: 61000,
159
+ connectionTimeout: 61000,
160
+ maxRequestsPerSocket: 0,
161
+ requestTimeout: 30000
162
  });
163
 
164
  const cors = require('@fastify/cors');
 
170
  const path = require('path');
171
 
172
  let sharp;
173
+ try { sharp = require('sharp'); } catch (e) {}
 
 
174
 
175
  const MAX_BUCKET_SIZE = 60 * 1024 * 1024 * 1024;
176
  const MAX_USERS = 30000;
 
197
  const SIZE_FILE = path.join(STATUS_DIR, "size.txt");
198
 
199
  [DATA_DIR, PHOTOS_DIR, STATUS_DIR].forEach(dir => {
200
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
201
  });
202
 
203
  if (!fs.existsSync(FULL_FILE)) fs.writeFileSync(FULL_FILE, '', 'utf8');
204
  if (!fs.existsSync(SIZE_FILE)) fs.writeFileSync(SIZE_FILE, '', 'utf8');
205
 
206
  async function getFullSpaces() {
207
+ try {
208
+ const content = await fsp.readFile(FULL_FILE, 'utf8');
209
+ return content.trim() ? content.trim().split('|') : [];
210
+ } catch (e) { return []; }
 
 
211
  }
212
 
213
  async function addSpaceToFull() {
214
+ try {
215
+ let content = await fsp.readFile(FULL_FILE, 'utf8').catch(() => '');
216
+ content = content.trim();
217
+ let spaces = content ? content.split('|') : [];
218
+ if (!spaces.includes(SPACE_NAME)) {
219
+ spaces.push(SPACE_NAME);
220
+ await fsp.writeFile(FULL_FILE, spaces.join('|'), 'utf8');
221
+ }
222
+ } catch (e) {}
223
  }
224
 
225
  async function removeSpaceFromFull() {
226
+ try {
227
+ let content = await fsp.readFile(FULL_FILE, 'utf8').catch(() => '');
228
+ content = content.trim();
229
+ let spaces = content ? content.split('|') : [];
230
+ const newSpaces = spaces.filter(s => s !== SPACE_NAME);
231
+ if (newSpaces.length !== spaces.length) {
232
+ await fsp.writeFile(FULL_FILE, newSpaces.join('|'), 'utf8');
233
+ }
234
+ } catch (e) {}
235
  }
236
 
237
  async function addSpaceToSize() {
238
+ try {
239
+ let content = await fsp.readFile(SIZE_FILE, 'utf8').catch(() => '');
240
+ content = content.trim();
241
+ let spaces = content ? content.split('|') : [];
242
+ if (!spaces.includes(SPACE_NAME)) {
243
+ spaces.push(SPACE_NAME);
244
+ await fsp.writeFile(SIZE_FILE, spaces.join('|'), 'utf8');
245
+ }
246
+ } catch (e) {}
247
  }
248
 
249
  function checkRPSStatus() {
250
+ const now = Date.now();
251
+ if (now - rpsWindowStart >= 1000) {
252
+ if (currentRPS >= MAX_RPS * 0.9 && !isSpaceFull_RPS) {
253
+ isSpaceFull_RPS = true;
254
+ addSpaceToFull();
255
+ } else if (currentRPS < MAX_RPS * 0.7 && isSpaceFull_RPS) {
256
+ isSpaceFull_RPS = false;
257
+ removeSpaceFromFull();
 
 
 
258
  }
259
+ currentRPS = 0;
260
+ rpsWindowStart = now;
261
+ }
262
  }
263
 
264
  async function checkUserCountStatus() {
265
+ try {
266
+ const count = await dbRequest('getCount');
267
+ if (count >= MAX_USERS && !isSpaceFull_Users) {
268
+ isSpaceFull_Users = true;
269
+ await addSpaceToSize();
270
+ await addSpaceToFull();
271
+ }
272
+ } catch (e) {}
273
  }
274
 
275
  let dbWorker;
 
277
  const dbPromises = new Map();
278
 
279
  function initDBWorker() {
280
+ return new Promise((resolve, reject) => {
281
+ dbWorker = new ThreadWorker(__filename, { workerData: { dataDir: DATA_DIR } });
282
+ dbWorker.on('message', (message) => {
283
+ const { id, success, result, error } = message;
284
+ const promise = dbPromises.get(id);
285
+ if (promise) {
286
+ dbPromises.delete(id);
287
+ if (success) promise.resolve(result);
288
+ else promise.reject(new Error(error));
289
+ }
 
 
 
 
 
290
  });
291
+ dbWorker.on('error', reject);
292
+ dbWorker.on('online', resolve);
293
+ });
294
  }
295
 
296
  function dbRequest(action, data = {}) {
297
+ return new Promise((resolve, reject) => {
298
+ const id = ++dbRequestId;
299
+ dbPromises.set(id, { resolve, reject });
300
+ dbWorker.postMessage({ id, action, data });
301
+ });
302
  }
303
 
304
  class FastRateLimiter {
305
+ constructor(maxRequests = 15000, windowMs = 1000) {
306
+ this.maxRequests = maxRequests;
307
+ this.windowMs = windowMs;
308
+ this.requests = 0;
309
+ this.windowStart = Date.now();
310
+ }
311
+ isAllowed() {
312
+ const now = Date.now();
313
+ if (now - this.windowStart >= this.windowMs) {
314
+ this.requests = 0;
315
+ this.windowStart = now;
 
 
 
 
 
 
316
  }
317
+ if (this.requests >= this.maxRequests) return false;
318
+ this.requests++;
319
+ currentRPS++;
320
+ return true;
321
+ }
322
  }
323
 
324
  const rateLimiter = new FastRateLimiter(15000, 1000);
325
 
326
  function hashPassword(password) {
327
+ return crypto.createHash('sha256').update(HASH + password + HASH).digest('hex');
328
  }
329
 
330
  function verifyPassword(password, hash) {
331
+ return hashPassword(password) === hash;
332
  }
333
 
334
  function generateId() {
335
+ return crypto.randomBytes(16).toString('base64url').substring(0, 21);
336
  }
337
 
338
  function getBucketSize() {
339
+ let totalSize = 0;
340
+ try {
341
+ const files = fs.readdirSync(PHOTOS_DIR);
342
+ for (const file of files) {
343
+ try { totalSize += fs.statSync(path.join(PHOTOS_DIR, file)).size; } catch (e) {}
344
+ }
345
+ try { totalSize += fs.statSync(path.join(DATA_DIR, 'users.db')).size; } catch (e) {}
346
+ } catch (e) {}
347
+ return totalSize;
 
 
 
 
348
  }
349
 
350
  async function compressToWebp(fileData, maxSizeKb = 20) {
351
+ if (!sharp) return fileData;
352
+ try {
353
+ let image = sharp(fileData);
354
+ const metadata = await image.metadata();
355
+ const currentSizeKb = fileData.length / 1024;
356
+ if (metadata.format === 'webp' && currentSizeKb <= maxSizeKb) return fileData;
357
+ let quality = 80;
358
+ let outputBuffer;
359
+ while (quality >= 10) {
360
+ outputBuffer = await image
361
+ .resize(800, 800, { fit: 'inside', withoutEnlargement: true })
362
+ .webp({ quality })
363
+ .toBuffer();
364
+ if (outputBuffer.length / 1024 <= maxSizeKb) break;
365
+ quality -= 10;
366
+ }
367
+ if (outputBuffer.length / 1024 > maxSizeKb) {
368
+ outputBuffer = await image
369
+ .resize(400, 400, { fit: 'inside', withoutEnlargement: true })
370
+ .webp({ quality: 30 })
371
+ .toBuffer();
 
 
 
 
 
 
372
  }
373
+ return outputBuffer;
374
+ } catch (e) { return fileData; }
375
  }
376
 
377
  async function setupFastify() {
378
+ await fastify.register(cors, { origin: '*' });
379
+ await fastify.register(multipart, { limits: { fileSize: 5 * 1024 * 1024 } });
380
+ await fastify.register(fastifyStatic, { root: PHOTOS_DIR, prefix: '/photos/', maxAge: 86400000 });
381
+
382
+ fastify.addHook('onRequest', async (request, reply) => {
383
+ if (!rateLimiter.isAllowed()) {
384
+ reply.status(503).send({ error: 'rate_limit' });
385
+ return;
386
+ }
387
+ });
388
+
389
+ fastify.post('/signup', async (request, reply) => {
390
+ const { username, password, name } = request.body || {};
391
+ if (!username || !password || !name) {
392
+ reply.status(400).send({ detail: 'Missing fields' });
393
+ return;
394
+ }
395
+ const cleanUsername = username.trim().toLowerCase();
396
+ if (cleanUsername.length < 3 || cleanUsername.length > MAX_USERNAME_LENGTH) {
397
+ reply.status(400).send({ detail: `Username must be 3-${MAX_USERNAME_LENGTH} chars` });
398
+ return;
399
+ }
400
+ if (password.length < 6 || password.length > MAX_PASSWORD_LENGTH) {
401
+ reply.status(400).send({ detail: `Password must be 6-${MAX_PASSWORD_LENGTH} chars` });
402
+ return;
403
+ }
404
+ if (name.trim().length > MAX_NAME_LENGTH) {
405
+ reply.status(400).send({ detail: `Name max ${MAX_NAME_LENGTH} chars` });
406
+ return;
407
+ }
408
+ const existingUser = await dbRequest('getUserByUsername', { username: cleanUsername });
409
+ if (existingUser) {
410
+ reply.status(409).send({ detail: 'Username exists' });
411
+ return;
412
+ }
413
+ const count = await dbRequest('getCount');
414
+ if (count >= MAX_USERS) {
415
+ reply.status(507).send({ detail: 'Server full' });
416
+ return;
417
+ }
418
+ const userId = generateId();
419
+ const newUser = {
420
+ id: userId,
421
+ username: cleanUsername,
422
+ password_hash: hashPassword(password),
423
+ name: name.trim(),
424
+ created_at: new Date().toISOString(),
425
+ is_login: 0,
426
+ bio: '',
427
+ pic_url: '',
428
+ date_of_birth: '',
429
+ is_profile_public: 'true'
430
+ };
431
+ try {
432
+ await dbRequest('insertUser', { user: newUser });
433
+ totalUsers = count + 1;
434
+ await checkUserCountStatus();
435
+ reply.send({ success: true, user_id: userId, username: cleanUsername });
436
+ } catch (e) {
437
+ reply.status(500).send({ detail: 'Signup failed' });
438
+ }
439
+ });
440
+
441
+ fastify.post('/login', async (request, reply) => {
442
+ const { username, password } = request.body || {};
443
+ if (!username || !password) {
444
+ reply.status(400).send({ detail: 'Missing fields' });
445
+ return;
446
+ }
447
+ const cleanUsername = username.trim().toLowerCase();
448
+ const user = await dbRequest('getUserByUsername', { username: cleanUsername });
449
+ if (!user) {
450
+ reply.status(401).send({ detail: 'Invalid credentials' });
451
+ return;
452
+ }
453
+ if (!verifyPassword(password, user.password_hash)) {
454
+ reply.status(401).send({ detail: 'Invalid credentials' });
455
+ return;
456
+ }
457
+ await dbRequest('updateLogin', { userId: user.id, isLogin: true });
458
+ reply.send({
459
+ success: true,
460
+ id: user.id,
461
+ username: user.username,
462
+ name: user.name,
463
+ bio: user.bio || '',
464
+ pic_url: user.pic_url || '',
465
+ date_of_birth: user.date_of_birth || '',
466
+ is_profile_public: user.is_profile_public || 'true',
467
+ created_at: user.created_at
468
  });
469
+ });
470
+
471
+ fastify.post('/logout', async (request, reply) => {
472
+ const { id } = request.body || {};
473
+ if (!id) {
474
+ reply.status(400).send({ detail: 'Missing ID' });
475
+ return;
476
+ }
477
+ const user = await dbRequest('getUserById', { userId: id });
478
+ if (!user) {
479
+ reply.status(404).send({ detail: 'Not found' });
480
+ return;
481
+ }
482
+ await dbRequest('updateLogin', { userId: id, isLogin: false });
483
+ reply.send({ success: true });
484
+ });
485
+
486
+ fastify.put('/updateprofiledetails', async (request, reply) => {
487
+ const { id, username, name, bio, date_of_birth, is_profile_public } = request.body || {};
488
 
489
+ if (!id) {
490
+ reply.status(400).send({ detail: 'Missing ID' });
491
+ return;
492
+ }
493
+
494
+ const user = await dbRequest('getUserById', { userId: id });
495
+ if (!user) {
496
+ reply.status(404).send({ detail: 'Not found' });
497
+ return;
498
+ }
499
+
500
+ if (name && name.length > MAX_NAME_LENGTH) {
501
+ reply.status(400).send({ detail: `Name max ${MAX_NAME_LENGTH} chars` });
502
+ return;
503
+ }
504
+
505
+ if (bio && bio.length > MAX_BIO_LENGTH) {
506
+ reply.status(400).send({ detail: `Bio max ${MAX_BIO_LENGTH} chars` });
507
+ return;
508
+ }
509
+
510
+ let cleanUsername = '';
511
+ if (username && username.trim() !== '') {
512
+ cleanUsername = username.trim().toLowerCase();
513
+
514
+ if (cleanUsername.length < 3 || cleanUsername.length > MAX_USERNAME_LENGTH) {
515
+ reply.status(400).send({ detail: `Username must be 3-${MAX_USERNAME_LENGTH} chars` });
516
+ return;
517
+ }
518
+
519
+ if (cleanUsername !== user.username) {
520
  const existingUser = await dbRequest('getUserByUsername', { username: cleanUsername });
521
  if (existingUser) {
522
+ reply.status(409).send({ detail: 'Username exists' });
523
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
524
  }
525
+ }
526
+ }
527
 
528
+ await dbRequest('updateProfile', {
529
+ userId: id,
530
+ username: cleanUsername,
531
+ name: name || '',
532
+ bio: bio !== undefined ? bio : '',
533
+ date_of_birth: date_of_birth || '',
534
+ is_profile_public: is_profile_public || 'true'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
535
  });
536
 
537
+ reply.send({ success: true });
538
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
539
 
540
+ fastify.post('/uploadphoto', async (request, reply) => {
541
+ if (getBucketSize() >= MAX_BUCKET_SIZE) {
542
+ reply.status(507).send({ detail: 'Storage full' });
 
 
 
543
  return;
544
  }
545
+ try {
546
+ const data = await request.file();
547
+ if (!data || !data.mimetype.startsWith('image/')) {
548
+ reply.status(400).send({ detail: 'Invalid image' });
 
549
  return;
550
  }
551
+ const fileBuffer = await data.toBuffer();
552
+ const id = (data.fields && data.fields.id) ? data.fields.id.value : null;
553
+ const user = await dbRequest('getUserById', { userId: id });
554
+ if (!id || !user) {
555
+ reply.status(404).send({ detail: 'Not found' });
556
+ return;
557
+ }
558
+ const compressed = await compressToWebp(fileBuffer, MAX_PHOTO_SIZE_KB);
559
+ const filename = `${id}_${Math.floor(Date.now() / 1000)}.webp`;
560
+ await fsp.writeFile(path.join(PHOTOS_DIR, filename), compressed);
561
+ await dbRequest('updatePhoto', { userId: id, picUrl: `/photos/${filename}` });
562
+ reply.send({ success: true });
563
+ } catch (e) {
564
+ reply.status(500).send({ detail: 'Failed' });
565
  }
 
 
 
 
 
 
 
 
 
566
  });
567
 
568
+ fastify.get('/getuser', async (request, reply) => {
569
+ const { id, username } = request.query || {};
570
+ let user;
571
+ if (username) {
572
+ user = await dbRequest('getUserByUsername', { username: username.toLowerCase() });
573
+ } else if (id) {
574
+ user = await dbRequest('getUserById', { userId: id });
575
+ } else {
576
+ reply.status(400).send({ detail: 'Missing params' });
577
+ return;
578
+ }
579
+ if (!user) {
580
+ reply.status(404).send({ detail: 'Not found' });
581
+ return;
582
+ }
583
+ reply.send({
584
+ id: user.id,
585
+ username: user.username,
586
+ name: user.name,
587
+ bio: user.bio || '',
588
+ pic_url: user.pic_url || '',
589
+ date_of_birth: user.date_of_birth || '',
590
+ is_profile_public: user.is_profile_public || 'true',
591
+ created_at: user.created_at,
592
+ is_login: Boolean(user.is_login)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
593
  });
594
+ });
595
+
596
+ fastify.get('/check', async (request, reply) => {
597
+ const count = await dbRequest('getCount');
598
+ reply.send({
599
+ status: 'ok',
600
+ count: count,
601
+ maxUsers: MAX_USERS,
602
+ isFull: count >= MAX_USERS,
603
+ spaceName: SPACE_NAME
604
  });
605
+ });
606
+
607
+ return fastify;
608
  }
609
 
610
  async function start() {
611
+ try {
612
+ await initDBWorker();
613
+ totalUsers = await dbRequest('getCount');
614
+ await setupFastify();
615
+ setInterval(() => { checkRPSStatus(); }, 100);
616
+ setInterval(async () => { await checkUserCountStatus(); }, 5000);
617
+ const port = process.env.PORT || 7860;
618
+ await fastify.listen({ port, host: '0.0.0.0' });
619
+ console.log(`Server running on port ${port}`);
620
+ } catch (err) {
621
+ console.error('Startup error:', err.message);
622
+ process.exit(1);
623
+ }
 
 
 
 
 
 
 
 
 
 
624
  }
625
 
626
  process.on('SIGTERM', async () => {
627
+ await removeSpaceFromFull();
628
+ await fastify.close();
629
+ if (dbWorker) await dbWorker.terminate();
630
+ process.exit(0);
631
  });
632
 
633
  process.on('SIGINT', async () => {
634
+ await removeSpaceFromFull();
635
+ await fastify.close();
636
+ if (dbWorker) await dbWorker.terminate();
637
+ process.exit(0);
638
  });
639
 
640
  start().catch(err => process.exit(1));