@similie/hyphen-command-server-types 1.0.0
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/CHANGELOG.md +5 -0
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/models/base-model.d.ts +13 -0
- package/dist/models/base-model.d.ts.map +1 -0
- package/dist/models/base-model.js +2 -0
- package/dist/models/base-model.js.map +1 -0
- package/dist/models/index.d.ts +2 -0
- package/dist/models/index.d.ts.map +1 -0
- package/dist/models/index.js +2 -0
- package/dist/models/index.js.map +1 -0
- package/dist/modules/index.d.ts +3 -0
- package/dist/modules/index.d.ts.map +1 -0
- package/dist/modules/index.js +3 -0
- package/dist/modules/index.js.map +1 -0
- package/dist/modules/loader.d.ts +4 -0
- package/dist/modules/loader.d.ts.map +1 -0
- package/dist/modules/loader.js +53 -0
- package/dist/modules/loader.js.map +1 -0
- package/dist/modules/types.d.ts +29 -0
- package/dist/modules/types.d.ts.map +1 -0
- package/dist/modules/types.js +2 -0
- package/dist/modules/types.js.map +1 -0
- package/dist/services/index.d.ts +3 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +3 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/leader-lock.d.ts +31 -0
- package/dist/services/leader-lock.d.ts.map +1 -0
- package/dist/services/leader-lock.js +103 -0
- package/dist/services/leader-lock.js.map +1 -0
- package/dist/services/redis.d.ts +27 -0
- package/dist/services/redis.d.ts.map +1 -0
- package/dist/services/redis.js +62 -0
- package/dist/services/redis.js.map +1 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +2 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/utils.d.ts +44 -0
- package/dist/tools/utils.d.ts.map +1 -0
- package/dist/tools/utils.js +283 -0
- package/dist/tools/utils.js.map +1 -0
- package/package.json +34 -0
- package/src/index.ts +4 -0
- package/src/models/base-model.ts +13 -0
- package/src/models/index.ts +1 -0
- package/src/modules/index.ts +2 -0
- package/src/modules/loader.ts +67 -0
- package/src/modules/types.ts +40 -0
- package/src/services/index.ts +2 -0
- package/src/services/leader-lock.ts +113 -0
- package/src/services/redis.ts +69 -0
- package/src/tools/index.ts +1 -0
- package/src/tools/utils.ts +349 -0
- package/tsconfig.json +46 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import CryptoJS from "crypto-js";
|
|
2
|
+
import { v4 } from "uuid";
|
|
3
|
+
export const mqttMessageIdentity = (payload) => {
|
|
4
|
+
try {
|
|
5
|
+
const value = JSON.parse(payload.toString());
|
|
6
|
+
value._uid = generateUniqueUUID();
|
|
7
|
+
return JSON.stringify(value);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return payload.toString();
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
export const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
|
14
|
+
export const wrapModelSet = (models, wrapOn = "uid") => {
|
|
15
|
+
const map = new Map();
|
|
16
|
+
models.forEach((m) => map.set(m[wrapOn], m));
|
|
17
|
+
return (key) => {
|
|
18
|
+
if (!map.has(key)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return map.get(key);
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
export const URL_HOST_PATTERN_FOR_INPUT = /^(https?:\/\/)?((([a-z0-9]([a-z0-9\-]*[a-z0-9])*)\.)+[a-z]{2,}|(([0-9]{1,3}\.){3}[0-9]{1,3}))(:[0-9]+)?(\/[\-a-z0-9%_.~+]*)*(\?[;&a-z0-9%_.~+=\-]*)?(#[-a-z0-9_]*)?$/;
|
|
25
|
+
export const DOMAIN_PATTERN = /^(?:\*\.)?(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i;
|
|
26
|
+
export function isNumeric(str) {
|
|
27
|
+
if (typeof str !== "string")
|
|
28
|
+
return false; // we only process strings
|
|
29
|
+
const trimmed = str.trim();
|
|
30
|
+
if (trimmed === "")
|
|
31
|
+
return false;
|
|
32
|
+
return !isNaN(Number(trimmed));
|
|
33
|
+
}
|
|
34
|
+
export const generateUniqueUUID = () => {
|
|
35
|
+
return v4();
|
|
36
|
+
};
|
|
37
|
+
export function formatDate(date, locale = "en-AU", options = {
|
|
38
|
+
year: "numeric",
|
|
39
|
+
month: "2-digit",
|
|
40
|
+
day: "2-digit",
|
|
41
|
+
}) {
|
|
42
|
+
if (typeof date === "string") {
|
|
43
|
+
date = new Date(date);
|
|
44
|
+
}
|
|
45
|
+
return date.toLocaleDateString(locale, options);
|
|
46
|
+
}
|
|
47
|
+
export function formatMoney(value, currencySymbol = "$") {
|
|
48
|
+
let formattedValue = value.toString();
|
|
49
|
+
if (value.toString().includes(".")) {
|
|
50
|
+
formattedValue = value.toFixed(2);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
formattedValue = value.toString() + ".00";
|
|
54
|
+
}
|
|
55
|
+
return `${currencySymbol}${formattedValue}`;
|
|
56
|
+
}
|
|
57
|
+
export const siteMoneyFormatConfig = (value, currencyDivisor = 100, currencySymbol = "$") => {
|
|
58
|
+
if (typeof value !== "number" && isNaN(value)) {
|
|
59
|
+
return "0.00";
|
|
60
|
+
}
|
|
61
|
+
const divisor = currencyDivisor;
|
|
62
|
+
const formattedValue = (value / divisor).toFixed(2);
|
|
63
|
+
return `${currencySymbol}${formattedValue}`;
|
|
64
|
+
};
|
|
65
|
+
export const errorMessageWithCode = (message) => {
|
|
66
|
+
if (!message) {
|
|
67
|
+
return { code: 500, message: message };
|
|
68
|
+
}
|
|
69
|
+
const errorCode = message.split(":");
|
|
70
|
+
if (errorCode.length > 1) {
|
|
71
|
+
const code = errorCode[0] || "500";
|
|
72
|
+
const intValue = parseInt(code);
|
|
73
|
+
return {
|
|
74
|
+
code: Number.isNaN(intValue) ? 500 : intValue,
|
|
75
|
+
message: errorCode[1],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return { code: 500, message: message };
|
|
79
|
+
};
|
|
80
|
+
export const isDomainPattern = (str) => {
|
|
81
|
+
if (!str || typeof str !== "string")
|
|
82
|
+
return false;
|
|
83
|
+
return DOMAIN_PATTERN.test(str);
|
|
84
|
+
};
|
|
85
|
+
export const extractFormToJson = (form) => {
|
|
86
|
+
const asObject = Object.fromEntries(form.entries());
|
|
87
|
+
const data = {};
|
|
88
|
+
for (const key of Object.keys(asObject)) {
|
|
89
|
+
try {
|
|
90
|
+
data[key] = JSON.parse(asObject[key]);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
data[key] = asObject[key];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return data;
|
|
97
|
+
};
|
|
98
|
+
export const delay = (timeout = 1000) => new Promise((resolve) => setTimeout(resolve, timeout));
|
|
99
|
+
export const generateRandomInt = (min, max) => {
|
|
100
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
101
|
+
};
|
|
102
|
+
export const generateUniqueId = (numBytes = 16) => {
|
|
103
|
+
const wordArray = CryptoJS.lib.WordArray.random(numBytes);
|
|
104
|
+
// Convert the WordArray to a hexadecimal string
|
|
105
|
+
return wordArray.toString(CryptoJS.enc.Hex);
|
|
106
|
+
};
|
|
107
|
+
export const isUUID = (value) => {
|
|
108
|
+
const regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/i;
|
|
109
|
+
return regex.test(value);
|
|
110
|
+
};
|
|
111
|
+
export const validateEmail = (email) => {
|
|
112
|
+
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
|
113
|
+
return emailRegex.test(email);
|
|
114
|
+
};
|
|
115
|
+
export const generateRandomPassword = (length) => {
|
|
116
|
+
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+[]{}|;:,.<>?";
|
|
117
|
+
let password = "";
|
|
118
|
+
for (let i = 0; i < length; i++) {
|
|
119
|
+
const randomIndex = Math.floor(Math.random() * characters.length);
|
|
120
|
+
password += characters[randomIndex];
|
|
121
|
+
}
|
|
122
|
+
return password;
|
|
123
|
+
};
|
|
124
|
+
export const fileToUint8Array = (file) => {
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
const reader = new FileReader();
|
|
127
|
+
reader.onload = () => {
|
|
128
|
+
const arrayBuffer = reader.result;
|
|
129
|
+
const uint8Array = new Uint8Array(arrayBuffer);
|
|
130
|
+
resolve(uint8Array);
|
|
131
|
+
};
|
|
132
|
+
reader.onerror = () => {
|
|
133
|
+
reject(new Error("Error reading file as ArrayBuffer"));
|
|
134
|
+
};
|
|
135
|
+
reader.readAsArrayBuffer(file);
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
function readFileAsArrayBuffer(file) {
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
const reader = new FileReader();
|
|
141
|
+
reader.onload = function (event) {
|
|
142
|
+
const arrayBuffer = event.target?.result;
|
|
143
|
+
resolve(arrayBuffer);
|
|
144
|
+
};
|
|
145
|
+
reader.onerror = function () {
|
|
146
|
+
reject(new Error("Error reading file"));
|
|
147
|
+
};
|
|
148
|
+
reader.readAsArrayBuffer(file);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
async function sha256Fallback(arrayBuffer) {
|
|
152
|
+
const wordArray = CryptoJS.lib.WordArray.create(arrayBuffer);
|
|
153
|
+
const hash = CryptoJS.SHA256(wordArray);
|
|
154
|
+
return hash.toString(CryptoJS.enc.Hex);
|
|
155
|
+
}
|
|
156
|
+
async function sha256(arrayBuffer) {
|
|
157
|
+
if (crypto.subtle) {
|
|
158
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", arrayBuffer);
|
|
159
|
+
return Array.from(new Uint8Array(hashBuffer))
|
|
160
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
161
|
+
.join("");
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
// Use fallback if crypto.subtle isn't available
|
|
165
|
+
return sha256Fallback(arrayBuffer);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export function sha256String(values) {
|
|
169
|
+
const encoder = new TextEncoder();
|
|
170
|
+
const uint8Array = encoder.encode(values);
|
|
171
|
+
const wordArray = CryptoJS.lib.WordArray.create(uint8Array);
|
|
172
|
+
const hash = CryptoJS.SHA256(wordArray);
|
|
173
|
+
return hash.toString(CryptoJS.enc.Hex);
|
|
174
|
+
}
|
|
175
|
+
export const ParseSocketMessage = (message) => {
|
|
176
|
+
try {
|
|
177
|
+
return JSON.parse(message);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return message;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
export const stripStateFromObject = (obj) => {
|
|
184
|
+
try {
|
|
185
|
+
return JSON.parse(JSON.stringify(obj));
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return obj;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
export const buildShaValueString = (data) => {
|
|
192
|
+
return sha256String(JSON.stringify(data));
|
|
193
|
+
};
|
|
194
|
+
async function computeHash(arrayBuffer) {
|
|
195
|
+
const hashBuffer = await sha256(arrayBuffer);
|
|
196
|
+
return hashBuffer;
|
|
197
|
+
}
|
|
198
|
+
export const hashFileContents = async (file) => {
|
|
199
|
+
const arrayBuffer = await readFileAsArrayBuffer(file);
|
|
200
|
+
return computeHash(arrayBuffer);
|
|
201
|
+
};
|
|
202
|
+
export const fileListToFile = async (file) => {
|
|
203
|
+
const uint8Array = await fileToUint8Array(file);
|
|
204
|
+
return new File([uint8Array], file.name, {
|
|
205
|
+
type: file.type,
|
|
206
|
+
lastModified: file.lastModified,
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
export const formatMessageServerMessage = (message) => {
|
|
210
|
+
const backup = "Failed to send verification email";
|
|
211
|
+
if (!message) {
|
|
212
|
+
return backup;
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
const errMessage = JSON.parse(message);
|
|
216
|
+
if (errMessage?.error) {
|
|
217
|
+
return errMessage.error;
|
|
218
|
+
}
|
|
219
|
+
if (errMessage?.message) {
|
|
220
|
+
return errMessage.message;
|
|
221
|
+
}
|
|
222
|
+
throw new Error("Not known json");
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return message;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
export const isOlderThanInDays = (created_at, days) => {
|
|
229
|
+
// Get the current date and time
|
|
230
|
+
const currentDate = new Date();
|
|
231
|
+
// Calculate the target date by subtracting `days` from the current date
|
|
232
|
+
const targetDate = new Date();
|
|
233
|
+
targetDate.setDate(currentDate.getDate() - days);
|
|
234
|
+
// Compare created_at with the target date
|
|
235
|
+
return created_at < targetDate;
|
|
236
|
+
};
|
|
237
|
+
export const removeDoubleSpaces = (text) => {
|
|
238
|
+
return text.replace(/\s\s+/g, " ");
|
|
239
|
+
};
|
|
240
|
+
export const removeContentSpace = (text) => {
|
|
241
|
+
return removeDoubleSpaces(text.replace(/[\n\t]/g, " ")); // new lines and tabs
|
|
242
|
+
};
|
|
243
|
+
export const removeHtmlEntities = (text) => {
|
|
244
|
+
return text.replace(/(?:&#x[a-f0-9]{2};|&#[0-9]+;)/g, " ");
|
|
245
|
+
};
|
|
246
|
+
export function getLast30DaysLabels(locale = "en-US") {
|
|
247
|
+
const labels = [];
|
|
248
|
+
const today = new Date();
|
|
249
|
+
for (let daysAgo = 30; daysAgo >= 0; daysAgo--) {
|
|
250
|
+
const d = new Date(today);
|
|
251
|
+
d.setDate(d.getDate() - daysAgo);
|
|
252
|
+
// e.g. "Jan"
|
|
253
|
+
const month = d.toLocaleString(locale, { month: "short" });
|
|
254
|
+
// day of month, no leading zero: "1", "31", etc.
|
|
255
|
+
const day = d.getDate();
|
|
256
|
+
labels.push(`${month}, ${day}`);
|
|
257
|
+
}
|
|
258
|
+
return labels;
|
|
259
|
+
}
|
|
260
|
+
export function humanizeNumber(value, precision = 1) {
|
|
261
|
+
if (value === 0)
|
|
262
|
+
return "0";
|
|
263
|
+
const suffixes = [
|
|
264
|
+
"",
|
|
265
|
+
" thousand",
|
|
266
|
+
" million",
|
|
267
|
+
" billion",
|
|
268
|
+
" trillion",
|
|
269
|
+
" quadrillion",
|
|
270
|
+
];
|
|
271
|
+
const abs = Math.abs(value);
|
|
272
|
+
const tier = Math.floor(Math.log10(abs) / 3);
|
|
273
|
+
// if in the ones (no suffix)
|
|
274
|
+
if (tier === 0) {
|
|
275
|
+
return value.toString();
|
|
276
|
+
}
|
|
277
|
+
const scale = 10 ** (tier * 3);
|
|
278
|
+
const scaled = value / scale;
|
|
279
|
+
// format to `precision` decimals, then drop any trailing “.0”
|
|
280
|
+
const formatted = scaled.toFixed(precision).replace(/\.0+$/, "");
|
|
281
|
+
return `${formatted}${suffixes[tier]}`;
|
|
282
|
+
}
|
|
283
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/tools/utils.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@similie/hyphen-command-server-types",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Adds common types for Hyphen Command Center modules.",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"release": "npm run build && standard-version"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [],
|
|
12
|
+
"author": "guernica0131 (Similie)",
|
|
13
|
+
"license": "ISC",
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@similie/ellipsies": "^1.0.16",
|
|
19
|
+
"@similie/http-connector": "^0.0.17",
|
|
20
|
+
"@similie/model-connect-entities": "^1.1.4",
|
|
21
|
+
"crypto-js": "^4.2.0",
|
|
22
|
+
"ioredis": "^5.8.2",
|
|
23
|
+
"redis": "^5.10.0",
|
|
24
|
+
"redlock": "5.0.0-beta.2",
|
|
25
|
+
"ts-node-dev": "^2.0.0",
|
|
26
|
+
"uuid": "^13.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/crypto-js": "^4.2.2",
|
|
30
|
+
"@types/node": "^24.7.0",
|
|
31
|
+
"standard-version": "^9.5.0",
|
|
32
|
+
"typescript": "^5.9.3"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type UUID as _UUID } from "@similie/ellipsies";
|
|
2
|
+
export type UUID = _UUID;
|
|
3
|
+
export interface BaseModel {
|
|
4
|
+
id: number;
|
|
5
|
+
createdAt: Date;
|
|
6
|
+
updatedAt: Date;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface BaseUIDModel {
|
|
10
|
+
uid: UUID; // TODO: needs to be UUID
|
|
11
|
+
createdAt: Date;
|
|
12
|
+
updatedAt: Date;
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./base-model";
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// src/modules/loader.ts
|
|
2
|
+
import type { HyphenModule, ModuleContext, LoadedModule } from "./types";
|
|
3
|
+
|
|
4
|
+
export async function loadModulesFromEnv(
|
|
5
|
+
ctx: ModuleContext,
|
|
6
|
+
): Promise<LoadedModule[]> {
|
|
7
|
+
const raw = process.env.HYPHEN_MODULES?.trim();
|
|
8
|
+
if (!raw) return [];
|
|
9
|
+
|
|
10
|
+
const specList = raw
|
|
11
|
+
.split(",")
|
|
12
|
+
.map((s) => s.trim())
|
|
13
|
+
.filter(Boolean);
|
|
14
|
+
|
|
15
|
+
const loaded: LoadedModule[] = [];
|
|
16
|
+
|
|
17
|
+
for (const spec of specList) {
|
|
18
|
+
// spec can be:
|
|
19
|
+
// - "hyphen-rtsp-module"
|
|
20
|
+
// - "@similie/hyphen-rtsp-module"
|
|
21
|
+
// - "file:./dist/my-module.js" (dev)
|
|
22
|
+
try {
|
|
23
|
+
const imported = await import(spec);
|
|
24
|
+
const candidate: HyphenModule =
|
|
25
|
+
imported.default ?? imported.module ?? imported;
|
|
26
|
+
|
|
27
|
+
if (
|
|
28
|
+
!candidate ||
|
|
29
|
+
typeof candidate.init !== "function" ||
|
|
30
|
+
!candidate.name
|
|
31
|
+
) {
|
|
32
|
+
ctx.log(`[modules] invalid module export: ${spec}`, { spec });
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
ctx.log(`[modules] init ${candidate.name}`, {
|
|
37
|
+
spec,
|
|
38
|
+
version: candidate.version,
|
|
39
|
+
});
|
|
40
|
+
const res = await candidate.init(ctx);
|
|
41
|
+
loaded.push({ mod: candidate, res });
|
|
42
|
+
ctx.log(`[modules] ready ${candidate.name}`, { spec });
|
|
43
|
+
} catch (e: any) {
|
|
44
|
+
ctx.log(`[modules] failed to load: ${spec}`, {
|
|
45
|
+
error: e?.message ?? String(e),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return loaded;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function shutdownModules(
|
|
54
|
+
ctx: ModuleContext,
|
|
55
|
+
loaded: LoadedModule[],
|
|
56
|
+
) {
|
|
57
|
+
for (const { mod, res } of loaded) {
|
|
58
|
+
try {
|
|
59
|
+
res && (await res?.shutdown?.());
|
|
60
|
+
ctx.log(`[modules] shutdown ${mod.name}`);
|
|
61
|
+
} catch (e: any) {
|
|
62
|
+
ctx.log(`[modules] shutdown failed ${mod.name}`, {
|
|
63
|
+
error: e?.message ?? String(e),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// src/modules/types.ts
|
|
2
|
+
import type { RedisCache, LeaderElector } from "../services"; // or your Redis wrapper type
|
|
3
|
+
import type { Ellipsies } from "@similie/ellipsies";
|
|
4
|
+
// If you have an Ellipsies instance type, use it. Otherwise keep as `any` and tighten later.
|
|
5
|
+
export type EllipsiesInstance = Ellipsies;
|
|
6
|
+
|
|
7
|
+
export type CommandCenterSystemIdentity = {
|
|
8
|
+
name: string;
|
|
9
|
+
identity: string;
|
|
10
|
+
host: string;
|
|
11
|
+
port: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type ModuleContext = {
|
|
15
|
+
ellipsies: EllipsiesInstance;
|
|
16
|
+
redis: typeof RedisCache;
|
|
17
|
+
leader: LeaderElector;
|
|
18
|
+
identity?: CommandCenterSystemIdentity;
|
|
19
|
+
log: (msg: string, extra?: Record<string, any>) => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type ModuleInitResult = {
|
|
23
|
+
// Optional cleanup hook called on shutdown
|
|
24
|
+
shutdown?: () => Promise<void> | void;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export interface HyphenModule {
|
|
28
|
+
name: string;
|
|
29
|
+
version?: string;
|
|
30
|
+
|
|
31
|
+
// Called once per process on boot
|
|
32
|
+
init(
|
|
33
|
+
ctx: ModuleContext,
|
|
34
|
+
): Promise<ModuleInitResult | void> | (ModuleInitResult | void);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type LoadedModule = {
|
|
38
|
+
mod: HyphenModule;
|
|
39
|
+
res?: ModuleInitResult | void;
|
|
40
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
import { Redis } from "ioredis";
|
|
3
|
+
import Redlock, { Lock } from "redlock";
|
|
4
|
+
import { randomUUID } from "crypto";
|
|
5
|
+
|
|
6
|
+
export type LeaderEvents = {
|
|
7
|
+
elected: () => void;
|
|
8
|
+
revoked: () => void;
|
|
9
|
+
error: (err: Error) => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export class LeaderElector extends EventEmitter {
|
|
13
|
+
private static instance: LeaderElector;
|
|
14
|
+
private redlock!: Redlock;
|
|
15
|
+
private redis!: Redis;
|
|
16
|
+
private renewTimer?: NodeJS.Timeout;
|
|
17
|
+
private acquireTimer?: NodeJS.Timeout;
|
|
18
|
+
private lock?: Lock | undefined;
|
|
19
|
+
|
|
20
|
+
private readonly lockKey = "mqtt:leader:lock";
|
|
21
|
+
private readonly ttlMs = 10000; // lock TTL
|
|
22
|
+
private readonly renewEveryMs = 5000; // extend interval (< TTL)
|
|
23
|
+
private readonly retryDelayMs = 1500; // backoff when not leader
|
|
24
|
+
private readonly instanceId = randomUUID(); // for logging/diagnostics
|
|
25
|
+
|
|
26
|
+
private constructor() {
|
|
27
|
+
super();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static get(): LeaderElector {
|
|
31
|
+
if (!this.instance) this.instance = new LeaderElector();
|
|
32
|
+
return this.instance;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
init(redisUrl: string) {
|
|
36
|
+
this.redis = new Redis(redisUrl);
|
|
37
|
+
this.redlock = new Redlock([this.redis], {
|
|
38
|
+
// Redlock will keep retrying for us when we explicitly request it,
|
|
39
|
+
// but we’ll manage our own retry loop here.
|
|
40
|
+
driftFactor: 0.01,
|
|
41
|
+
retryCount: 0,
|
|
42
|
+
retryDelay: 0,
|
|
43
|
+
});
|
|
44
|
+
this.startAcquireLoop();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Pure check: true only if we *currently* hold a live lock. */
|
|
48
|
+
amLeader(): boolean {
|
|
49
|
+
return !!this.lock;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Stop loops and release if we are leader. */
|
|
53
|
+
async shutdown() {
|
|
54
|
+
clearTimeout(this.acquireTimer as any);
|
|
55
|
+
clearInterval(this.renewTimer as any);
|
|
56
|
+
await this.safeRelease();
|
|
57
|
+
await this.redis?.quit();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// -------- internal --------
|
|
61
|
+
|
|
62
|
+
private startAcquireLoop() {
|
|
63
|
+
const tryAcquire = async () => {
|
|
64
|
+
if (this.lock) return; // already leader
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
// Acquire without retries; we run our own loop.
|
|
68
|
+
const lock = await this.redlock.acquire([this.lockKey], this.ttlMs);
|
|
69
|
+
this.lock = lock;
|
|
70
|
+
this.emit("elected");
|
|
71
|
+
this.startRenewLoop();
|
|
72
|
+
} catch {
|
|
73
|
+
// Not acquired (someone else is leader). Back off a bit (with jitter).
|
|
74
|
+
} finally {
|
|
75
|
+
const jitter = Math.floor(Math.random() * 500);
|
|
76
|
+
this.acquireTimer = setTimeout(tryAcquire, this.retryDelayMs + jitter);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
tryAcquire().catch((err) => this.emit("error", err as Error));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private startRenewLoop() {
|
|
84
|
+
clearInterval(this.renewTimer as any);
|
|
85
|
+
this.renewTimer = setInterval(async () => {
|
|
86
|
+
if (!this.lock) return;
|
|
87
|
+
try {
|
|
88
|
+
this.lock = await this.lock.extend(this.ttlMs);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
// Couldn’t extend: we lost leadership (expired or Redis issue)
|
|
91
|
+
await this.safeRevoke();
|
|
92
|
+
// acquisition loop is already running, it’ll try to reacquire
|
|
93
|
+
}
|
|
94
|
+
}, this.renewEveryMs);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private async safeRevoke() {
|
|
98
|
+
clearInterval(this.renewTimer as any);
|
|
99
|
+
this.lock = undefined;
|
|
100
|
+
this.emit("revoked");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private async safeRelease() {
|
|
104
|
+
if (!this.lock) return;
|
|
105
|
+
try {
|
|
106
|
+
await this.lock.release();
|
|
107
|
+
} catch {
|
|
108
|
+
// If already expired or released, ignore.
|
|
109
|
+
} finally {
|
|
110
|
+
await this.safeRevoke();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// src/services/redis.ts
|
|
2
|
+
import { createClient } from "redis";
|
|
3
|
+
const getRedisConfig = () => {
|
|
4
|
+
return process.env.REDIS_CONFIG_URL || "redis://localhost:6379/1";
|
|
5
|
+
};
|
|
6
|
+
export class RedisCache {
|
|
7
|
+
// Create a Redis client instance.
|
|
8
|
+
private static client = createClient({ url: getRedisConfig() });
|
|
9
|
+
private static DEFAULT_EXPIRATION_SECONDS = 900; // 15 minutes
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Initializes the Redis client. Call this once on server startup.
|
|
13
|
+
*/
|
|
14
|
+
public static async init(): Promise<void> {
|
|
15
|
+
if (this.client.isOpen) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
await this.client.connect();
|
|
20
|
+
console.log("Connected to Redis");
|
|
21
|
+
} catch (err) {
|
|
22
|
+
console.error("Error connecting to Redis:", err);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Caches an object under a specified key.
|
|
28
|
+
* @param key The cache key.
|
|
29
|
+
* @param value The object to cache.
|
|
30
|
+
* @param expirationSeconds Optional expiration time in seconds (defaults to 15 minutes).
|
|
31
|
+
*/
|
|
32
|
+
public static async set(
|
|
33
|
+
key: string,
|
|
34
|
+
value: any,
|
|
35
|
+
expirationSeconds?: number,
|
|
36
|
+
): Promise<void> {
|
|
37
|
+
const exp = expirationSeconds || this.DEFAULT_EXPIRATION_SECONDS;
|
|
38
|
+
const stringValue = JSON.stringify(value);
|
|
39
|
+
await this.client.set(key, stringValue, { EX: exp });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Retrieves a cached object by its key.
|
|
44
|
+
* @param key The cache key.
|
|
45
|
+
* @returns The cached object, or null if not found.
|
|
46
|
+
*/
|
|
47
|
+
public static async get<T>(key: string): Promise<T | null> {
|
|
48
|
+
const data = await this.client.get(key);
|
|
49
|
+
if (data) {
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(
|
|
52
|
+
typeof data === "string" ? data : JSON.stringify(data),
|
|
53
|
+
);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error("Error parsing cached data:", err);
|
|
56
|
+
return data as unknown as T | null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Deletes a cache entry.
|
|
64
|
+
* @param key The cache key.
|
|
65
|
+
*/
|
|
66
|
+
public static async del(key: string): Promise<void> {
|
|
67
|
+
await this.client.del(key);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./utils";
|