Clients.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. import mongoose from 'mongoose';
  2. import path from 'path';
  3. import { fileURLToPath } from 'url';
  4. import Cache from '../libs/cache.js';
  5. const { Schema } = mongoose;
  6. const __filename = fileURLToPath(import.meta.url);
  7. const __dirname = path.dirname(__filename);
  8. const ClientsCacheFile = path.join(__dirname, '../data/clients.cache');
  9. const clientSettingSchema = new Schema({
  10. _id: {
  11. required: true,
  12. type: String,
  13. },
  14. deviceId: {
  15. required: true,
  16. type: Number,
  17. },
  18. }, { timestamps: true });
  19. const ClientSetting = mongoose.model('ClientSetting', clientSettingSchema);
  20. const CLIENT_HEADER_FIELDS = [
  21. 'X-Device',
  22. 'X-Version',
  23. 'X-Data-Type',
  24. 'X-Market-Type',
  25. 'X-Group-Sequence',
  26. 'X-Platform',
  27. ];
  28. const CLIENT_KEY_FIELDS = [
  29. 'X-Data-Type',
  30. 'X-Market-Type',
  31. 'X-Group-Sequence',
  32. 'X-Platform',
  33. ];
  34. const CLIENT_FIELD_NAMES = {
  35. 'X-Data-Type': 'dataType',
  36. 'X-Device': 'device',
  37. 'X-Group-Sequence': 'groupSequence',
  38. 'X-Market-Type': 'marketType',
  39. 'X-Platform': 'platform',
  40. 'X-Version': 'version',
  41. };
  42. const CLIENTS = {
  43. Items: {},
  44. LegacySettings: {},
  45. Settings: {},
  46. };
  47. let saveTimer = null;
  48. const normalizeHeaderValue = (value) => {
  49. if (Array.isArray(value)) {
  50. return value.join(',');
  51. }
  52. return value == null ? '' : String(value).trim();
  53. }
  54. const getRequestHeader = (req, field) => {
  55. return normalizeHeaderValue(req.get?.(field));
  56. }
  57. const getClientHeaders = (req) => {
  58. return CLIENT_HEADER_FIELDS.reduce((headers, field) => {
  59. headers[field] = getRequestHeader(req, field);
  60. return headers;
  61. }, {});
  62. }
  63. const formatClientFields = (headers) => {
  64. return CLIENT_HEADER_FIELDS.reduce((clientFields, field) => {
  65. clientFields[CLIENT_FIELD_NAMES[field]] = headers[field];
  66. return clientFields;
  67. }, {});
  68. }
  69. const getClientKey = (headers) => {
  70. return CLIENT_KEY_FIELDS
  71. .map(field => `${field}:${headers[field] ?? ''}`)
  72. .join('|');
  73. }
  74. const getClientIp = (req) => {
  75. const ip = getRequestHeader(req, 'X-Real-IP') ||
  76. normalizeHeaderValue(req.ip) ||
  77. normalizeHeaderValue(req.socket?.remoteAddress) ||
  78. normalizeHeaderValue(req.connection?.remoteAddress);
  79. return ip.replace(/^::ffff:/, '');
  80. }
  81. const scheduleSaveClientsToCache = () => {
  82. if (saveTimer) {
  83. clearTimeout(saveTimer);
  84. }
  85. saveTimer = setTimeout(saveClientsToCache, 1000);
  86. }
  87. const recordRequest = (req) => {
  88. const route = req.path;
  89. const headers = getClientHeaders(req);
  90. const clientFields = formatClientFields(headers);
  91. const key = getClientKey(headers);
  92. const now = Date.now();
  93. const current = CLIENTS.Items[key] ?? {};
  94. const setting = CLIENTS.Settings[key] ?? {};
  95. CLIENTS.Items[key] = {
  96. key,
  97. ...clientFields,
  98. deviceId: setting.deviceId ?? current.deviceId,
  99. ip: getClientIp(req),
  100. route,
  101. firstRequestTime: current.firstRequestTime ?? now,
  102. lastRequestTime: now,
  103. requestCount: (current.requestCount ?? 0) + 1,
  104. };
  105. scheduleSaveClientsToCache();
  106. return CLIENTS.Items[key];
  107. }
  108. const getClients = () => {
  109. return Object.values(CLIENTS.Items)
  110. .map(client => ({
  111. ...client,
  112. deviceId: CLIENTS.Settings[client.key]?.deviceId ?? client.deviceId,
  113. }))
  114. .sort((a, b) => (b.lastRequestTime ?? 0) - (a.lastRequestTime ?? 0));
  115. }
  116. const updateClient = async ({ key, deviceId } = {}) => {
  117. if (!key || !CLIENTS.Items[key]) {
  118. return Promise.reject(new Error('CLIENT_NOT_FOUND'));
  119. }
  120. if (deviceId === undefined || deviceId === null || deviceId === '') {
  121. delete CLIENTS.Settings[key];
  122. delete CLIENTS.Items[key].deviceId;
  123. await ClientSetting.deleteOne({ _id: key });
  124. saveClientsToCache();
  125. return CLIENTS.Items[key];
  126. }
  127. const parsedDeviceId = Number(deviceId);
  128. if (!Number.isInteger(parsedDeviceId)) {
  129. return Promise.reject(new Error('DEVICE_ID_INVALID'));
  130. }
  131. await ClientSetting.findByIdAndUpdate(
  132. key,
  133. { $set: { deviceId: parsedDeviceId } },
  134. { new: true, upsert: true },
  135. );
  136. CLIENTS.Settings[key] = { deviceId: parsedDeviceId };
  137. CLIENTS.Items[key] = {
  138. ...CLIENTS.Items[key],
  139. deviceId: parsedDeviceId,
  140. };
  141. saveClientsToCache();
  142. return CLIENTS.Items[key];
  143. }
  144. const deleteClient = async (key) => {
  145. if (!key || !CLIENTS.Items[key]) {
  146. return Promise.reject(new Error('CLIENT_NOT_FOUND'));
  147. }
  148. delete CLIENTS.Items[key];
  149. delete CLIENTS.Settings[key];
  150. await ClientSetting.deleteOne({ _id: key });
  151. saveClientsToCache();
  152. }
  153. const loadClientSettings = async () => {
  154. const legacyEntries = Object.entries(CLIENTS.LegacySettings);
  155. if (legacyEntries.length) {
  156. await Promise.all(legacyEntries.map(([key, setting]) => {
  157. return ClientSetting.updateOne(
  158. { _id: key },
  159. { $setOnInsert: { deviceId: setting.deviceId } },
  160. { upsert: true },
  161. );
  162. }));
  163. CLIENTS.LegacySettings = {};
  164. }
  165. const settings = await ClientSetting.find().lean();
  166. CLIENTS.Settings = settings.reduce((map, setting) => {
  167. map[setting._id] = { deviceId: setting.deviceId };
  168. if (CLIENTS.Items[setting._id]) {
  169. CLIENTS.Items[setting._id].deviceId = setting.deviceId;
  170. }
  171. return map;
  172. }, {});
  173. return CLIENTS.Settings;
  174. }
  175. function saveClientsToCache() {
  176. if (saveTimer) {
  177. clearTimeout(saveTimer);
  178. saveTimer = null;
  179. }
  180. Cache.setData(ClientsCacheFile, { Items: CLIENTS.Items });
  181. }
  182. function loadClientsFromCache() {
  183. const cachedClients = Cache.getData(ClientsCacheFile, true);
  184. if (!cachedClients?.Items) {
  185. return;
  186. }
  187. CLIENTS.Items = cachedClients.Items;
  188. Object.values(CLIENTS.Items).forEach(client => {
  189. if (Number.isInteger(client.deviceId)) {
  190. CLIENTS.LegacySettings[client.key] = { deviceId: client.deviceId };
  191. }
  192. delete client.deviceId;
  193. });
  194. }
  195. loadClientsFromCache();
  196. loadClientSettings().catch(() => {});
  197. process.on('exit', saveClientsToCache);
  198. process.on('SIGINT', () => {
  199. process.exit(0);
  200. });
  201. process.on('SIGTERM', () => {
  202. process.exit(0);
  203. });
  204. process.on('SIGUSR2', () => {
  205. process.exit(0);
  206. });
  207. const Clients = {
  208. recordRequest,
  209. getClients,
  210. updateClient,
  211. deleteClient,
  212. loadClientSettings,
  213. };
  214. export { recordRequest, getClients, updateClient, deleteClient, loadClientSettings };
  215. export default Clients;