| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246 |
- import mongoose from 'mongoose';
- import path from 'path';
- import { fileURLToPath } from 'url';
- import Cache from '../libs/cache.js';
- const { Schema } = mongoose;
- const __filename = fileURLToPath(import.meta.url);
- const __dirname = path.dirname(__filename);
- const ClientsCacheFile = path.join(__dirname, '../data/clients.cache');
- const clientSettingSchema = new Schema({
- _id: {
- required: true,
- type: String,
- },
- deviceId: {
- required: true,
- type: Number,
- },
- }, { timestamps: true });
- const ClientSetting = mongoose.model('ClientSetting', clientSettingSchema);
- const CLIENT_HEADER_FIELDS = [
- 'X-Device',
- 'X-Version',
- 'X-Data-Type',
- 'X-Market-Type',
- 'X-Group-Sequence',
- 'X-Platform',
- ];
- const CLIENT_KEY_FIELDS = [
- 'X-Data-Type',
- 'X-Market-Type',
- 'X-Group-Sequence',
- 'X-Platform',
- ];
- const CLIENT_FIELD_NAMES = {
- 'X-Data-Type': 'dataType',
- 'X-Device': 'device',
- 'X-Group-Sequence': 'groupSequence',
- 'X-Market-Type': 'marketType',
- 'X-Platform': 'platform',
- 'X-Version': 'version',
- };
- const CLIENTS = {
- Items: {},
- LegacySettings: {},
- Settings: {},
- };
- let saveTimer = null;
- const normalizeHeaderValue = (value) => {
- if (Array.isArray(value)) {
- return value.join(',');
- }
- return value == null ? '' : String(value).trim();
- }
- const getRequestHeader = (req, field) => {
- return normalizeHeaderValue(req.get?.(field));
- }
- const getClientHeaders = (req) => {
- return CLIENT_HEADER_FIELDS.reduce((headers, field) => {
- headers[field] = getRequestHeader(req, field);
- return headers;
- }, {});
- }
- const formatClientFields = (headers) => {
- return CLIENT_HEADER_FIELDS.reduce((clientFields, field) => {
- clientFields[CLIENT_FIELD_NAMES[field]] = headers[field];
- return clientFields;
- }, {});
- }
- const getClientKey = (headers) => {
- return CLIENT_KEY_FIELDS
- .map(field => `${field}:${headers[field] ?? ''}`)
- .join('|');
- }
- const getClientIp = (req) => {
- const ip = getRequestHeader(req, 'X-Real-IP') ||
- normalizeHeaderValue(req.ip) ||
- normalizeHeaderValue(req.socket?.remoteAddress) ||
- normalizeHeaderValue(req.connection?.remoteAddress);
- return ip.replace(/^::ffff:/, '');
- }
- const scheduleSaveClientsToCache = () => {
- if (saveTimer) {
- clearTimeout(saveTimer);
- }
- saveTimer = setTimeout(saveClientsToCache, 1000);
- }
- const recordRequest = (req) => {
- const route = req.path;
- const headers = getClientHeaders(req);
- const clientFields = formatClientFields(headers);
- const key = getClientKey(headers);
- const now = Date.now();
- const current = CLIENTS.Items[key] ?? {};
- const setting = CLIENTS.Settings[key] ?? {};
- CLIENTS.Items[key] = {
- key,
- ...clientFields,
- deviceId: setting.deviceId ?? current.deviceId,
- ip: getClientIp(req),
- route,
- firstRequestTime: current.firstRequestTime ?? now,
- lastRequestTime: now,
- requestCount: (current.requestCount ?? 0) + 1,
- };
- scheduleSaveClientsToCache();
- return CLIENTS.Items[key];
- }
- const getClients = () => {
- return Object.values(CLIENTS.Items)
- .map(client => ({
- ...client,
- deviceId: CLIENTS.Settings[client.key]?.deviceId ?? client.deviceId,
- }))
- .sort((a, b) => (b.lastRequestTime ?? 0) - (a.lastRequestTime ?? 0));
- }
- const updateClient = async ({ key, deviceId } = {}) => {
- if (!key || !CLIENTS.Items[key]) {
- return Promise.reject(new Error('CLIENT_NOT_FOUND'));
- }
- if (deviceId === undefined || deviceId === null || deviceId === '') {
- delete CLIENTS.Settings[key];
- delete CLIENTS.Items[key].deviceId;
- await ClientSetting.deleteOne({ _id: key });
- saveClientsToCache();
- return CLIENTS.Items[key];
- }
- const parsedDeviceId = Number(deviceId);
- if (!Number.isInteger(parsedDeviceId)) {
- return Promise.reject(new Error('DEVICE_ID_INVALID'));
- }
- await ClientSetting.findByIdAndUpdate(
- key,
- { $set: { deviceId: parsedDeviceId } },
- { new: true, upsert: true },
- );
- CLIENTS.Settings[key] = { deviceId: parsedDeviceId };
- CLIENTS.Items[key] = {
- ...CLIENTS.Items[key],
- deviceId: parsedDeviceId,
- };
- saveClientsToCache();
- return CLIENTS.Items[key];
- }
- const deleteClient = async (key) => {
- if (!key || !CLIENTS.Items[key]) {
- return Promise.reject(new Error('CLIENT_NOT_FOUND'));
- }
- delete CLIENTS.Items[key];
- delete CLIENTS.Settings[key];
- await ClientSetting.deleteOne({ _id: key });
- saveClientsToCache();
- }
- const loadClientSettings = async () => {
- const legacyEntries = Object.entries(CLIENTS.LegacySettings);
- if (legacyEntries.length) {
- await Promise.all(legacyEntries.map(([key, setting]) => {
- return ClientSetting.updateOne(
- { _id: key },
- { $setOnInsert: { deviceId: setting.deviceId } },
- { upsert: true },
- );
- }));
- CLIENTS.LegacySettings = {};
- }
- const settings = await ClientSetting.find().lean();
- CLIENTS.Settings = settings.reduce((map, setting) => {
- map[setting._id] = { deviceId: setting.deviceId };
- if (CLIENTS.Items[setting._id]) {
- CLIENTS.Items[setting._id].deviceId = setting.deviceId;
- }
- return map;
- }, {});
- return CLIENTS.Settings;
- }
- function saveClientsToCache() {
- if (saveTimer) {
- clearTimeout(saveTimer);
- saveTimer = null;
- }
- Cache.setData(ClientsCacheFile, { Items: CLIENTS.Items });
- }
- function loadClientsFromCache() {
- const cachedClients = Cache.getData(ClientsCacheFile, true);
- if (!cachedClients?.Items) {
- return;
- }
- CLIENTS.Items = cachedClients.Items;
- Object.values(CLIENTS.Items).forEach(client => {
- if (Number.isInteger(client.deviceId)) {
- CLIENTS.LegacySettings[client.key] = { deviceId: client.deviceId };
- }
- delete client.deviceId;
- });
- }
- loadClientsFromCache();
- loadClientSettings().catch(() => {});
- process.on('exit', saveClientsToCache);
- process.on('SIGINT', () => {
- process.exit(0);
- });
- process.on('SIGTERM', () => {
- process.exit(0);
- });
- process.on('SIGUSR2', () => {
- process.exit(0);
- });
- const Clients = {
- recordRequest,
- getClients,
- updateClient,
- deleteClient,
- loadClientSettings,
- };
- export { recordRequest, getClients, updateClient, deleteClient, loadClientSettings };
- export default Clients;
|