@alizadefaig/pos-server 1.2.34 → 1.2.35
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.
- package/app/db/driver.js +6 -0
- package/app/relay/masters.js +22 -3
- package/app/sync/index.js +10 -2
- package/package.json +43 -43
- package/server.js +12 -0
package/app/db/driver.js
CHANGED
|
@@ -179,6 +179,12 @@ const openPostgres = () => {
|
|
|
179
179
|
driverName = 'pg';
|
|
180
180
|
|
|
181
181
|
const pool = new pg.Pool(pgConfig());
|
|
182
|
+
// Простаивающее соединение оборвалось (рестарт Postgres, сеть): pg шлёт 'error' на пул, и без
|
|
183
|
+
// слушателя это необработанное исключение — процесс облачного Node падал. Пул сам выбросит
|
|
184
|
+
// мёртвое соединение и откроет новое на следующем запросе.
|
|
185
|
+
pool.on('error', (e) => {
|
|
186
|
+
console.error('[db] idle pg client error:', e && e.message);
|
|
187
|
+
});
|
|
182
188
|
|
|
183
189
|
/** Обёртка над клиентом/пулом — одинаковая, чтобы транзакция давала тот же интерфейс. */
|
|
184
190
|
const wrap = (executor, isTransaction = false) => ({
|
package/app/relay/masters.js
CHANGED
|
@@ -59,14 +59,24 @@ const verify = async (auth) => {
|
|
|
59
59
|
validateStatus: () => true,
|
|
60
60
|
});
|
|
61
61
|
} catch (e) {
|
|
62
|
-
return {ok: false, reason: 'verify_unavailable'};
|
|
62
|
+
return {ok: false, reason: 'verify_unavailable', transient: true};
|
|
63
63
|
}
|
|
64
64
|
if (response.status !== 200 || !response.data || response.data.token_matches !== true) {
|
|
65
|
-
|
|
65
|
+
// Laravel недоступен / в деплое / перегружен / лицензия — пройдёт само; 401/403 и
|
|
66
|
+
// чужой токен — нет (касса переподключится с новой сессией сама).
|
|
67
|
+
const transient = response.status >= 500 || [402, 404, 408, 429].includes(response.status);
|
|
68
|
+
return {ok: false, reason: `verify_failed_${response.status}`, transient};
|
|
66
69
|
}
|
|
67
70
|
return {ok: true};
|
|
68
71
|
};
|
|
69
72
|
|
|
73
|
+
// MASTER_RELAY_VERIFY_RETRY (по умолчанию выкл. = как раньше): разовый сбой проверки больше не
|
|
74
|
+
// оставляет подключённый сокет мастера вне реестра до его следующего обрыва — проверка
|
|
75
|
+
// повторяется с паузой, пока сокет жив. Без этого после рестарта облака или деплоя Laravel
|
|
76
|
+
// все клиенты заведения через облако получали master_offline часами.
|
|
77
|
+
const VERIFY_RETRY_DELAYS_MS = [3000, 10000, 30000, 60000];
|
|
78
|
+
const verifyRetryEnabled = () => (process.env.MASTER_RELAY_VERIFY_RETRY ?? 'false') === 'true';
|
|
79
|
+
|
|
70
80
|
/**
|
|
71
81
|
* Сигнальный сокет с handshake.auth.master. company_id в query сокета обязан совпасть с
|
|
72
82
|
* тем, что проверено токеном, иначе касса слушала бы комнату чужой компании.
|
|
@@ -78,9 +88,18 @@ const register = (socket) => {
|
|
|
78
88
|
socket.on('disconnect', () => unregister(socket));
|
|
79
89
|
|
|
80
90
|
(async () => {
|
|
81
|
-
|
|
91
|
+
let result = String(auth.company_id || '') === queryCompanyId
|
|
82
92
|
? await verify(auth)
|
|
83
93
|
: {ok: false, reason: 'company_mismatch'};
|
|
94
|
+
for (let attempt = 0; !result.ok && result.transient && verifyRetryEnabled() && socket.connected; attempt++) {
|
|
95
|
+
const delay = VERIFY_RETRY_DELAYS_MS[Math.min(attempt, VERIFY_RETRY_DELAYS_MS.length - 1)];
|
|
96
|
+
console.warn(`[relay] master verify failed (${result.reason}) terminal=${auth.terminal_id}, retry in ${delay / 1000}s`);
|
|
97
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
98
|
+
if (!socket.connected) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
result = await verify(auth);
|
|
102
|
+
}
|
|
84
103
|
if (!socket.connected) {
|
|
85
104
|
return;
|
|
86
105
|
}
|
package/app/sync/index.js
CHANGED
|
@@ -580,6 +580,8 @@ const applyCompanyMode = async (data) => {
|
|
|
580
580
|
await state.store.block((s) => kv.set(s, 'company_mode', state.company));
|
|
581
581
|
};
|
|
582
582
|
|
|
583
|
+
const MODE_NOT_FOUND_TO_ONLINE = 3;
|
|
584
|
+
|
|
583
585
|
const pollMode = async () => {
|
|
584
586
|
state.lastModePoll = Date.now();
|
|
585
587
|
state.modeRequested = false;
|
|
@@ -589,12 +591,18 @@ const pollMode = async () => {
|
|
|
589
591
|
} catch (e) {
|
|
590
592
|
state.lastModeError = e.message;
|
|
591
593
|
if (e.name === 'CloudRequestError' && e.status === 404) {
|
|
592
|
-
// Старый Laravel или Sync API выключен: модели M нет — заведение в онлайне.
|
|
593
|
-
|
|
594
|
+
// Старый Laravel или Sync API выключен: модели M нет — заведение в онлайне. Но 404 отдаёт
|
|
595
|
+
// и Traefik, пока контейнер облака пересоздаётся при деплое: разовый 404 снимал мастера
|
|
596
|
+
// на ~10 с (фронт уходил в облако, relay — not_master). Решаем по нескольким подряд.
|
|
597
|
+
state.modeNotFound = (state.modeNotFound || 0) + 1;
|
|
598
|
+
if (state.modeNotFound >= MODE_NOT_FOUND_TO_ONLINE) {
|
|
599
|
+
await applyCompanyMode({state: 'online', master_terminal_id: null, epoch: 0, enabled: false});
|
|
600
|
+
}
|
|
594
601
|
return;
|
|
595
602
|
}
|
|
596
603
|
throw e;
|
|
597
604
|
}
|
|
605
|
+
state.modeNotFound = 0;
|
|
598
606
|
state.lastModeError = null;
|
|
599
607
|
await applyCompanyMode(response);
|
|
600
608
|
};
|
package/package.json
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
2
|
+
"name": "@alizadefaig/pos-server",
|
|
3
|
+
"version": "1.2.35",
|
|
4
|
+
"description": "Server in express js with Proxy to Main server if need and will work with Electron JS",
|
|
5
|
+
"homepage": "https://gitlab.com/faigalizade/pos-server#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://gitlab.com/faigalizade/pos-server/issues"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+ssh://git@gitlab.com/faigalizade/pos-server.git"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@alizadefaig/cafetap-escpos-usb": "^1.0.1",
|
|
15
|
+
"axios": "^1.13.2",
|
|
16
|
+
"bcryptjs": "^3.0.3",
|
|
17
|
+
"cors": "^2.8.5",
|
|
18
|
+
"escpos": "^3.0.0-alpha.6",
|
|
19
|
+
"escpos-network": "^3.0.0-alpha.5",
|
|
20
|
+
"express": "^5.1.0",
|
|
21
|
+
"http-proxy-middleware": "^3.0.5",
|
|
22
|
+
"lodash": "^4.17.21",
|
|
23
|
+
"moment": "^2.30.1",
|
|
24
|
+
"multer": "^2.0.2",
|
|
25
|
+
"pg": "^8.22.0",
|
|
26
|
+
"redis": "^5.10.0",
|
|
27
|
+
"sequelize": "^6.37.7",
|
|
28
|
+
"sharp": "^0.34.5",
|
|
29
|
+
"socket.io": "^4.8.1",
|
|
30
|
+
"socket.io-client": "^4.8.1",
|
|
31
|
+
"uuid": "^13.0.0"
|
|
32
|
+
},
|
|
33
|
+
"license": "ISC",
|
|
34
|
+
"author": "faigalizade@newave.az",
|
|
35
|
+
"type": "commonjs",
|
|
36
|
+
"main": "server.js",
|
|
37
|
+
"directories": {
|
|
38
|
+
"lib": "lib"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"test": "node --test --test-concurrency=1 \"test/**/*.test.js\"",
|
|
42
|
+
"test:watch": "node --test --watch --test-concurrency=1 \"test/**/*.test.js\"",
|
|
43
|
+
"start": "node server.js"
|
|
44
|
+
}
|
|
45
45
|
}
|
package/server.js
CHANGED
|
@@ -182,6 +182,18 @@ const runExpress = (conf = null) => {
|
|
|
182
182
|
// ==== Start Listen ====
|
|
183
183
|
const hostname = appConfig.host || 'localhost';
|
|
184
184
|
netstats.attach(server, {host: hostname, port});
|
|
185
|
+
// Порт занят (прежний процесс ещё не отпустил его после аварийного выхода, вторая копия) —
|
|
186
|
+
// раньше необработанное исключение: на кассе диалог Electron, локального Node нет до ручного
|
|
187
|
+
// перезапуска. Теперь повторяем, пока порт не освободится.
|
|
188
|
+
const LISTEN_RETRY_MS = 3000;
|
|
189
|
+
server.on('error', (e) => {
|
|
190
|
+
if (e && (e.code === 'EADDRINUSE' || e.code === 'EACCES') && !server.listening) {
|
|
191
|
+
console.error(`[server] port ${port} unavailable (${e.code}), retry in ${LISTEN_RETRY_MS / 1000}s`);
|
|
192
|
+
setTimeout(() => server.listen(port, hostname), LISTEN_RETRY_MS);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
console.error('[server] error:', e && e.message);
|
|
196
|
+
});
|
|
185
197
|
server.listen(port, hostname, async () => {
|
|
186
198
|
if (hostname === 'localhost') {
|
|
187
199
|
console.log(`Server started on ${hostname} http://${hostname}:${port}`);
|