@alizadefaig/pos-server 1.2.24 → 1.2.25

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.
@@ -6,9 +6,30 @@ const {getConfig} = require("../config");
6
6
  const {getLogger} = require("../logger/Log");
7
7
  const {socketSendByFilter} = require("../../utils");
8
8
 
9
- // Keep-alive agent — переиспользуем TCP-соединения к Laravel вместо новых на каждый запрос
10
- const httpAgent = new http.Agent({ keepAlive: true, maxSockets: 64 });
11
- const httpsAgent = new https.Agent({ keepAlive: true, maxSockets: 64 });
9
+ // Keep-alive agent — переиспользуем TCP-соединения к Laravel вместо новых на каждый запрос.
10
+ // БАГ C: maxSockets 64→128 (запас; после async-печати длинных холдов почти нет). env-настраиваемо.
11
+ const MAX_SOCKETS = Number(process.env.UPSTREAM_MAX_SOCKETS || 128);
12
+ const httpAgent = new http.Agent({ keepAlive: true, maxSockets: MAX_SOCKETS });
13
+ const httpsAgent = new https.Agent({ keepAlive: true, maxSockets: MAX_SOCKETS });
14
+
15
+ // БАГ D: горячие per-request логи за флагом (в проде ENABLE_LOGGER=false убирает оверхед).
16
+ const HOT_LOGS = (process.env.ENABLE_LOGGER ?? 'true') !== 'false';
17
+
18
+ // БАГ B: heartbeat за флагом (Шаг 5). По умолчанию OFF → нативные socket.io 25000/20000
19
+ // (поведение как сейчас в проде — дефолт обязан быть no-op). При WS_HEARTBEAT_ENABLED=true:
20
+ // 10000/5000 → «зомби»-сокет детектится ~15с вместо ~45с. Откат — снять флаг.
21
+ const WS_HEARTBEAT_ENABLED = (process.env.WS_HEARTBEAT_ENABLED ?? 'false') === 'true';
22
+ const WS_PING_INTERVAL = Number(process.env.WS_PING_INTERVAL || 10000);
23
+ const WS_PING_TIMEOUT = Number(process.env.WS_PING_TIMEOUT || 5000);
24
+ // connectionStateRecovery меняет адаптер на SessionAware + буферизует бродкасты компании
25
+ // (память) и ре-фаер 'connection' при recovery. За ОТДЕЛЬНЫМ флагом, по умолчанию OFF.
26
+ const WS_RECOVERY_ENABLED = (process.env.WS_RECOVERY_ENABLED ?? 'false') === 'true';
27
+ const WS_RECOVERY_MS = Number(process.env.WS_RECOVERY_MS || 30000);
28
+ // БАГ D: межрепличный бродкаст. Выкл по умолчанию → одиночный инстанс работает как сейчас.
29
+ // ВНИМАНИЕ: перед включением нужен `npm i @socket.io/redis-adapter` (пакет не в deps,
30
+ // чтобы не ломать `npm ci`); lazy-require ниже деградирует в одиночный режим, если его нет.
31
+ const REDIS_ADAPTER_ENABLED = (process.env.REDIS_ADAPTER_ENABLED ?? 'false') === 'true';
32
+ const REDIS_URL = process.env.REDIS_URL || 'redis://127.0.0.1:6379';
12
33
 
13
34
  // Таймаут axios по классу события (ms)
14
35
  const PRINT_EVENTS = new Set(['orders.sendToWorkshops', 'orders.printBillReceipt']);
@@ -25,14 +46,45 @@ function getUpstreamTimeout(event) {
25
46
  let serverSocket = null;
26
47
  module.exports = (server = null) => {
27
48
  if (server && !serverSocket) {
28
- serverSocket = new Server(server, {
29
- cors: {
30
- origin: '*'
31
- }
32
- });
49
+ // Дефолт = поведение как сейчас (нативные heartbeat/адаптер). Опции добавляются
50
+ // только при явном включении флагов — это и есть «правка под флагом с откатом».
51
+ const serverOptions = { cors: { origin: '*' } };
52
+ if (WS_HEARTBEAT_ENABLED) {
53
+ serverOptions.pingInterval = WS_PING_INTERVAL; // быстрый детект мёртвого сокета (~15с)
54
+ serverOptions.pingTimeout = WS_PING_TIMEOUT;
55
+ }
56
+ if (WS_RECOVERY_ENABLED) {
57
+ // Восстановление сессии/комнат при кратком разрыве (Wi-Fi-флап) без полного reconnect.
58
+ serverOptions.connectionStateRecovery = {
59
+ maxDisconnectionDuration: WS_RECOVERY_MS,
60
+ skipMiddlewares: true,
61
+ };
62
+ }
63
+ serverSocket = new Server(server, serverOptions);
33
64
 
34
65
  const logger = getLogger();
35
66
 
67
+ // БАГ D: межрепличный бродкаст для нескольких инстансов Node. За флагом и lazy-require —
68
+ // при выключенном флаге (по умолчанию) пакет не требуется, одиночный инстанс работает
69
+ // как раньше. При сбое подключения деградируем в одиночный режим, не падаем.
70
+ if (REDIS_ADAPTER_ENABLED) {
71
+ (async () => {
72
+ try {
73
+ const { createAdapter } = require('@socket.io/redis-adapter');
74
+ const { createClient } = require('redis');
75
+ const pubClient = createClient({ url: REDIS_URL });
76
+ const subClient = pubClient.duplicate();
77
+ pubClient.on('error', (e) => logger.error('redis pub error: ' + e.message));
78
+ subClient.on('error', (e) => logger.error('redis sub error: ' + e.message));
79
+ await Promise.all([pubClient.connect(), subClient.connect()]);
80
+ serverSocket.adapter(createAdapter(pubClient, subClient));
81
+ console.log('[socket] redis-adapter enabled (multi-replica broadcast)');
82
+ } catch (e) {
83
+ console.error('[socket] redis-adapter init failed → single-instance mode:', e.message);
84
+ }
85
+ })();
86
+ }
87
+
36
88
 
37
89
  serverSocket.on('connection', (socket) => {
38
90
  const companyId = socket.handshake.query.company_id;
@@ -49,13 +101,19 @@ module.exports = (server = null) => {
49
101
  socket.isProxy = isProxy;
50
102
 
51
103
  if (!isProxy) {
52
- axios.post(`${PROXY_TARGET}/api/socket/connection`, {
53
- companyId,
54
- terminalId,
55
- userId,
56
- }).catch(error => {
57
- logger.error(error);
58
- });
104
+ // При connectionStateRecovery 'connection' ре-фаерится на восстановленном
105
+ // сокете (socket.recovered=true) — повторный presence-POST не нужен (терминал
106
+ // уже зарегистрирован, комнаты восстановлены). Обработчики ниже регистрируем
107
+ // всегда (объект сокета новый).
108
+ if (!socket.recovered) {
109
+ axios.post(`${PROXY_TARGET}/api/socket/connection`, {
110
+ companyId,
111
+ terminalId,
112
+ userId,
113
+ }).catch(error => {
114
+ logger.error(error);
115
+ });
116
+ }
59
117
 
60
118
  // API-over-socket: frontend sends API calls over the existing WS connection
61
119
  socket.on('api:request', async ({ request_id, event, data, headers }) => {
@@ -63,7 +121,7 @@ module.exports = (server = null) => {
63
121
  const ackEvent = `api:ack:${request_id}`;
64
122
  const responseEvent = `api:response:${request_id}`;
65
123
  const t0 = Date.now();
66
- console.log(`[api:req] ${event} id=${request_id.slice(0, 8)} terminal=${terminalId?.slice(0, 8)}`);
124
+ if (HOT_LOGS) console.log(`[api:req] ${event} id=${request_id.slice(0, 8)} terminal=${terminalId?.slice(0, 8)}`);
67
125
 
68
126
  // Немедленно подтверждаем получение — фронт знает что запрос дошёл
69
127
  socket.emit(ackEvent);
@@ -88,11 +146,11 @@ module.exports = (server = null) => {
88
146
  }
89
147
  );
90
148
  const ms = Date.now() - t0;
91
- console.log(`[api:res] ${event} ✓ ${ms}ms id=${request_id.slice(0, 8)}`);
149
+ if (HOT_LOGS) console.log(`[api:res] ${event} ✓ ${ms}ms id=${request_id.slice(0, 8)}`);
92
150
  socket.emit(responseEvent, { success: true, data: response.data });
93
151
  } catch (err) {
94
152
  const ms = Date.now() - t0;
95
- console.log(`[api:res] ${event} ✗ ${ms}ms status=${err?.response?.status} id=${request_id.slice(0, 8)}`);
153
+ if (HOT_LOGS) console.log(`[api:res] ${event} ✗ ${ms}ms status=${err?.response?.status} id=${request_id.slice(0, 8)}`);
96
154
  socket.emit(responseEvent, { success: false, error: err?.response?.data ?? { message: err.message } });
97
155
  }
98
156
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alizadefaig/pos-server",
3
- "version": "1.2.24",
3
+ "version": "1.2.25",
4
4
  "description": "Server in express js with Proxy to Main server if need and will work with Electron JS",
5
5
  "homepage": "https://gitlab.com/faigalizade/pos-server#readme",
6
6
  "bugs": {